Skip to content

Commit bc868ca

Browse files
author
Guilherme Souza
committed
Add sign in with magic link
1 parent def1d75 commit bc868ca

File tree

9 files changed

+236
-63
lines changed

9 files changed

+236
-63
lines changed

Examples/Examples.xcodeproj/project.pbxproj

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
794EF1222955F26A008C9526 /* AddTodoListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794EF1212955F26A008C9526 /* AddTodoListView.swift */; };
2020
794EF1242955F3DE008C9526 /* TodoListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794EF1232955F3DE008C9526 /* TodoListRow.swift */; };
2121
7956405E2954ADE00088A06F /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7956405D2954ADE00088A06F /* Secrets.swift */; };
22-
795640602954AE140088A06F /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7956405F2954AE140088A06F /* AuthView.swift */; };
22+
795640602954AE140088A06F /* AuthController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7956405F2954AE140088A06F /* AuthController.swift */; };
2323
795640622955AD2B0088A06F /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795640612955AD2B0088A06F /* HomeView.swift */; };
2424
795640662955AE9C0088A06F /* TodoListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795640652955AE9C0088A06F /* TodoListView.swift */; };
2525
795640682955AEB30088A06F /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795640672955AEB30088A06F /* Models.swift */; };
@@ -29,6 +29,10 @@
2929
796298992AEBBA77000AA957 /* MFAFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796298982AEBBA77000AA957 /* MFAFlow.swift */; };
3030
7962989D2AEBC6F9000AA957 /* SVGView in Frameworks */ = {isa = PBXBuildFile; productRef = 7962989C2AEBC6F9000AA957 /* SVGView */; };
3131
79719ECE2ADF26C400737804 /* Supabase in Frameworks */ = {isa = PBXBuildFile; productRef = 79719ECD2ADF26C400737804 /* Supabase */; };
32+
79AF047F2B2CE207008761AD /* AuthWithEmailAndPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF047E2B2CE207008761AD /* AuthWithEmailAndPassword.swift */; };
33+
79AF04812B2CE261008761AD /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF04802B2CE261008761AD /* AuthView.swift */; };
34+
79AF04842B2CE408008761AD /* AuthWithMagicLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF04832B2CE408008761AD /* AuthWithMagicLink.swift */; };
35+
79AF04862B2CE586008761AD /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AF04852B2CE586008761AD /* Debug.swift */; };
3236
79FEFFAF2B07873600D36347 /* UserManagementApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FEFFAE2B07873600D36347 /* UserManagementApp.swift */; };
3337
79FEFFB12B07873600D36347 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79FEFFB02B07873600D36347 /* AppView.swift */; };
3438
79FEFFB32B07873700D36347 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 79FEFFB22B07873700D36347 /* Assets.xcassets */; };
@@ -58,13 +62,17 @@
5862
794EF1212955F26A008C9526 /* AddTodoListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTodoListView.swift; sourceTree = "<group>"; };
5963
794EF1232955F3DE008C9526 /* TodoListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoListRow.swift; sourceTree = "<group>"; };
6064
7956405D2954ADE00088A06F /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
61-
7956405F2954AE140088A06F /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
65+
7956405F2954AE140088A06F /* AuthController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthController.swift; sourceTree = "<group>"; };
6266
795640612955AD2B0088A06F /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
6367
795640652955AE9C0088A06F /* TodoListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoListView.swift; sourceTree = "<group>"; };
6468
795640672955AEB30088A06F /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = "<group>"; };
6569
795640692955AFBD0088A06F /* ErrorText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorText.swift; sourceTree = "<group>"; };
6670
796298982AEBBA77000AA957 /* MFAFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MFAFlow.swift; sourceTree = "<group>"; };
6771
7962989A2AEBBD9F000AA957 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
72+
79AF047E2B2CE207008761AD /* AuthWithEmailAndPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthWithEmailAndPassword.swift; sourceTree = "<group>"; };
73+
79AF04802B2CE261008761AD /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
74+
79AF04832B2CE408008761AD /* AuthWithMagicLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthWithMagicLink.swift; sourceTree = "<group>"; };
75+
79AF04852B2CE586008761AD /* Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debug.swift; sourceTree = "<group>"; };
6876
79FEFFAC2B07873600D36347 /* UserManagement.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UserManagement.app; sourceTree = BUILT_PRODUCTS_DIR; };
6977
79FEFFAE2B07873600D36347 /* UserManagementApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserManagementApp.swift; sourceTree = "<group>"; };
7078
79FEFFB02B07873600D36347 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
@@ -155,9 +163,9 @@
155163
793895C82954ABFF0044F2B8 /* Examples */ = {
156164
isa = PBXGroup;
157165
children = (
166+
79AF04822B2CE3BD008761AD /* Auth */,
158167
7962989A2AEBBD9F000AA957 /* Info.plist */,
159168
793895CD2954AC000044F2B8 /* Assets.xcassets */,
160-
7956405F2954AE140088A06F /* AuthView.swift */,
161169
793895CF2954AC000044F2B8 /* Examples.entitlements */,
162170
793895C92954ABFF0044F2B8 /* ExamplesApp.swift */,
163171
793895D02954AC000044F2B8 /* Preview Content */,
@@ -170,6 +178,7 @@
170178
794EF1212955F26A008C9526 /* AddTodoListView.swift */,
171179
794EF1232955F3DE008C9526 /* TodoListRow.swift */,
172180
796298982AEBBA77000AA957 /* MFAFlow.swift */,
181+
79AF04852B2CE586008761AD /* Debug.swift */,
173182
);
174183
path = Examples;
175184
sourceTree = "<group>";
@@ -189,6 +198,17 @@
189198
name = Frameworks;
190199
sourceTree = "<group>";
191200
};
201+
79AF04822B2CE3BD008761AD /* Auth */ = {
202+
isa = PBXGroup;
203+
children = (
204+
7956405F2954AE140088A06F /* AuthController.swift */,
205+
79AF04802B2CE261008761AD /* AuthView.swift */,
206+
79AF047E2B2CE207008761AD /* AuthWithEmailAndPassword.swift */,
207+
79AF04832B2CE408008761AD /* AuthWithMagicLink.swift */,
208+
);
209+
path = Auth;
210+
sourceTree = "<group>";
211+
};
192212
79FEFFAD2B07873600D36347 /* UserManagement */ = {
193213
isa = PBXGroup;
194214
children = (
@@ -373,14 +393,18 @@
373393
buildActionMask = 2147483647;
374394
files = (
375395
796298992AEBBA77000AA957 /* MFAFlow.swift in Sources */,
396+
79AF04862B2CE586008761AD /* Debug.swift in Sources */,
397+
79AF04842B2CE408008761AD /* AuthWithMagicLink.swift in Sources */,
376398
793895CC2954ABFF0044F2B8 /* RootView.swift in Sources */,
377399
7956406A2955AFBD0088A06F /* ErrorText.swift in Sources */,
400+
79AF04812B2CE261008761AD /* AuthView.swift in Sources */,
378401
794EF1242955F3DE008C9526 /* TodoListRow.swift in Sources */,
379402
794EF1222955F26A008C9526 /* AddTodoListView.swift in Sources */,
380403
7956405E2954ADE00088A06F /* Secrets.swift in Sources */,
381404
795640682955AEB30088A06F /* Models.swift in Sources */,
382405
795640662955AE9C0088A06F /* TodoListView.swift in Sources */,
383-
795640602954AE140088A06F /* AuthView.swift in Sources */,
406+
795640602954AE140088A06F /* AuthController.swift in Sources */,
407+
79AF047F2B2CE207008761AD /* AuthWithEmailAndPassword.swift in Sources */,
384408
795640622955AD2B0088A06F /* HomeView.swift in Sources */,
385409
793895CA2954ABFF0044F2B8 /* ExamplesApp.swift in Sources */,
386410
);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// AuthController.swift
3+
// Examples
4+
//
5+
// Created by Guilherme Souza on 22/12/22.
6+
//
7+
8+
import Auth
9+
import SwiftUI
10+
11+
@Observable
12+
@MainActor
13+
final class AuthController {
14+
var session: Session?
15+
16+
var currentUserID: UUID {
17+
guard let id = session?.user.id else {
18+
preconditionFailure("Required session.")
19+
}
20+
21+
return id
22+
}
23+
24+
@ObservationIgnored
25+
private var observeAuthStateChangesTask: Task<Void, Never>?
26+
27+
init() {
28+
observeAuthStateChangesTask = Task {
29+
for await (event, session) in await supabase.auth.authStateChanges {
30+
guard [.initialSession, .signedIn, .signedOut].contains(event) else { return }
31+
32+
self.session = session
33+
}
34+
}
35+
}
36+
37+
deinit {
38+
observeAuthStateChangesTask?.cancel()
39+
}
40+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// AuthView.swift
3+
// Examples
4+
//
5+
// Created by Guilherme Souza on 15/12/23.
6+
//
7+
8+
import SwiftUI
9+
10+
struct AuthView: View {
11+
enum Option: CaseIterable {
12+
case emailAndPassword
13+
case magicLink
14+
15+
var title: String {
16+
switch self {
17+
case .emailAndPassword: "Auth with Email & Password"
18+
case .magicLink: "Auth with Magic Link"
19+
}
20+
}
21+
}
22+
23+
var body: some View {
24+
List {
25+
ForEach(Option.allCases, id: \.self) { option in
26+
NavigationLink(option.title, value: option)
27+
}
28+
}
29+
.navigationDestination(for: Option.self) { options in
30+
options
31+
.navigationTitle(options.title)
32+
}
33+
.navigationBarTitleDisplayMode(.inline)
34+
}
35+
}
36+
37+
extension AuthView.Option: View {
38+
var body: some View {
39+
switch self {
40+
case .emailAndPassword: AuthWithEmailAndPassword()
41+
case .magicLink: AuthWithMagicLink()
42+
}
43+
}
44+
}
45+
46+
#Preview {
47+
AuthView()
48+
}
Lines changed: 29 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,29 @@
11
//
2-
// AuthView.swift
2+
// AuthWithEmailAndPassword.swift
33
// Examples
44
//
5-
// Created by Guilherme Souza on 22/12/22.
5+
// Created by Guilherme Souza on 15/12/23.
66
//
77

8-
import Auth
98
import SwiftUI
109

11-
@Observable
12-
@MainActor
13-
final class AuthController {
14-
var session: Session?
15-
16-
var currentUserID: UUID {
17-
guard let id = session?.user.id else {
18-
preconditionFailure("Required session.")
19-
}
20-
21-
return id
22-
}
23-
24-
@ObservationIgnored
25-
private var observeAuthStateChangesTask: Task<Void, Never>?
26-
27-
init() {
28-
observeAuthStateChangesTask = Task {
29-
for await (event, session) in await supabase.auth.authStateChanges {
30-
guard event == .initialSession || event == .signedIn || event == .signedOut else {
31-
return
32-
}
33-
34-
self.session = session
35-
}
36-
}
37-
}
38-
39-
deinit {
40-
observeAuthStateChangesTask?.cancel()
41-
}
42-
}
43-
44-
struct AuthView: View {
10+
struct AuthWithEmailAndPassword: View {
4511
enum Mode {
4612
case signIn, signUp
4713
}
4814

15+
enum Result {
16+
case failure(Error)
17+
case needsEmailConfirmation
18+
}
19+
4920
@Environment(AuthController.self) var auth
5021

5122
@State var email = ""
5223
@State var password = ""
5324
@State var mode: Mode = .signIn
5425
@State var result: Result?
5526

56-
enum Result {
57-
case failure(Error)
58-
case needsEmailConfirmation
59-
}
60-
6127
var body: some View {
6228
Form {
6329
Section {
@@ -76,12 +42,20 @@ struct AuthView: View {
7642
}
7743
}
7844

79-
if case .failure(let error) = result {
45+
if case let .failure(error) = result {
8046
ErrorText(error)
8147
}
48+
}
8249

83-
if case .needsEmailConfirmation = result {
50+
if mode == .signUp, case .needsEmailConfirmation = result {
51+
Section {
8452
Text("Check you inbox.")
53+
54+
Button("Resend confirmation") {
55+
Task {
56+
await resendConfirmationButtonTapped()
57+
}
58+
}
8559
}
8660
}
8761

@@ -96,8 +70,6 @@ struct AuthView: View {
9670
}
9771
}
9872
}
99-
.navigationTitle("Auth with Email & Password")
100-
.navigationBarTitleDisplayMode(.inline)
10173
}
10274

10375
func primaryActionButtonTapped() async {
@@ -108,7 +80,7 @@ struct AuthView: View {
10880
try await supabase.auth.signIn(email: email, password: password)
10981
case .signUp:
11082
let response = try await supabase.auth.signUp(
111-
email: email, password: password, redirectTo: URL(string: "com.supabase.Examples://")!
83+
email: email, password: password, redirectTo: URL(string: "com.supabase.Examples://")
11284
)
11385

11486
if case .user = response {
@@ -121,10 +93,16 @@ struct AuthView: View {
12193
}
12294
}
12395
}
124-
}
12596

126-
struct AuthView_Previews: PreviewProvider {
127-
static var previews: some View {
128-
AuthView()
97+
private func resendConfirmationButtonTapped() async {
98+
do {
99+
try await supabase.auth.resend(email: email, type: .signup)
100+
} catch {
101+
debug("Fail to resend email confirmation: \(error)")
102+
}
129103
}
130104
}
105+
106+
#Preview {
107+
AuthWithEmailAndPassword()
108+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// AuthWithMagicLink.swift
3+
// Examples
4+
//
5+
// Created by Guilherme Souza on 15/12/23.
6+
//
7+
8+
import SwiftUI
9+
10+
struct AuthWithMagicLink: View {
11+
@State var email = ""
12+
@State var error: Error?
13+
14+
var body: some View {
15+
Form {
16+
Section {
17+
TextField("Email", text: $email)
18+
}
19+
20+
Section {
21+
Button("Sign in with magic link") {
22+
Task {
23+
await signInWithMagicLinkTapped()
24+
}
25+
}
26+
}
27+
}
28+
.onOpenURL { url in
29+
Task { await onOpenURL(url) }
30+
}
31+
}
32+
33+
private func signInWithMagicLinkTapped() async {
34+
do {
35+
try await supabase.auth.signInWithOTP(
36+
email: email,
37+
redirectTo: URL(string: "com.supabase.Examples://")
38+
)
39+
} catch {
40+
self.error = error
41+
}
42+
}
43+
44+
private func onOpenURL(_ url: URL) async {
45+
debug("onOpenURL: \(url)")
46+
47+
do {
48+
try await supabase.auth.session(from: url)
49+
} catch {
50+
self.error = error
51+
}
52+
}
53+
}
54+
55+
#Preview {
56+
AuthWithMagicLink()
57+
}

0 commit comments

Comments
 (0)