Skip to content

Commit c8b8acb

Browse files
feat: email link sign-in
1 parent c7d5ca9 commit c8b8acb

4 files changed

Lines changed: 75 additions & 26 deletions

File tree

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthConfiguration.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import FirebaseAuth
12
import Foundation
23

34
public final class AuthConfiguration {
@@ -7,18 +8,24 @@ public final class AuthConfiguration {
78
let customStringsBundle: Bundle?
89
let tosUrl: URL
910
let privacyPolicyUrl: URL
11+
let emailLinkSignInActionCodeSettings: ActionCodeSettings?
12+
let verifyEmailActionCodeSettings: ActionCodeSettings?
1013

1114
public init(shouldHideCancelButton: Bool = false,
1215
interactiveDismissEnabled: Bool = true,
1316
shouldAutoUpgradeAnonymousUsers: Bool = false,
1417
customStringsBundle: Bundle? = nil,
1518
tosUrl: URL = URL(string: "https://example.com/tos")!,
16-
privacyPolicyUrl: URL = URL(string: "https://example.com/privacy")!) {
19+
privacyPolicyUrl: URL = URL(string: "https://example.com/privacy")!,
20+
emailLinkSignInActionCodeSettings: ActionCodeSettings? = nil,
21+
verifyEmailActionCodeSettings: ActionCodeSettings? = nil) {
1722
self.shouldHideCancelButton = shouldHideCancelButton
1823
self.interactiveDismissEnabled = interactiveDismissEnabled
1924
self.shouldAutoUpgradeAnonymousUsers = shouldAutoUpgradeAnonymousUsers
2025
self.customStringsBundle = customStringsBundle
2126
self.tosUrl = tosUrl
2227
self.privacyPolicyUrl = privacyPolicyUrl
28+
self.emailLinkSignInActionCodeSettings = emailLinkSignInActionCodeSettings
29+
self.verifyEmailActionCodeSettings = verifyEmailActionCodeSettings
2330
}
2431
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ public enum AuthenticationFlow {
3030
case signUp
3131
}
3232

33+
public enum AuthServiceError: Error {
34+
case invalidEmailLink(String)
35+
case notConfiguredProvider(String)
36+
case clientIdNotFound(String)
37+
case notConfiguredActionCodeSettings(String)
38+
}
39+
3340
@MainActor
3441
final class AuthListenerManager {
3542
private var authStateHandle: AuthStateDidChangeListenerHandle?
@@ -59,6 +66,7 @@ final class AuthListenerManager {
5966
@MainActor
6067
@Observable
6168
public final class AuthService {
69+
@ObservationIgnored @AppStorage("email-link") public var emailLink: String?
6270
public let configuration: AuthConfiguration
6371
public let auth: Auth
6472
private var listenerManager: AuthListenerManager?
@@ -84,13 +92,8 @@ public final class AuthService {
8492
private var safeGoogleProvider: GoogleProviderProtocol {
8593
get throws {
8694
guard let provider = googleProvider else {
87-
throw NSError(
88-
domain: "AuthEnvironmentErrorDomain",
89-
code: 1,
90-
userInfo: [
91-
NSLocalizedDescriptionKey: "`GoogleProviderSwift` has not been configured",
92-
]
93-
)
95+
throw AuthServiceError
96+
.notConfiguredProvider("`GoogleProviderSwift` has not been configured")
9497
}
9598
return provider
9699
}
@@ -99,18 +102,25 @@ public final class AuthService {
99102
private var safeFacebookProvider: FacebookProviderProtocol {
100103
get throws {
101104
guard let provider = facebookProvider else {
102-
throw NSError(
103-
domain: "AuthEnvironmentErrorDomain",
104-
code: 1,
105-
userInfo: [
106-
NSLocalizedDescriptionKey: "`FacebookProviderSwift` has not been configured",
107-
]
108-
)
105+
throw AuthServiceError
106+
.notConfiguredProvider("`FacebookProviderSwift` has not been configured")
109107
}
110108
return provider
111109
}
112110
}
113111

112+
private func safeActionCodeSettings(emailLinkSignIn: Bool = true) throws -> ActionCodeSettings {
113+
guard let actionCodeSettings = emailLinkSignIn ? configuration
114+
.emailLinkSignInActionCodeSettings : configuration.verifyEmailActionCodeSettings else {
115+
let errorMessage = emailLinkSignIn ?
116+
"ActionCodeSettings has not been configured for `AuthConfiguration.emailLinkSignInActionCodeSettings`" :
117+
"ActionCodeSettings has not been configured for `AuthConfiguration.verifyEmailActionCodeSettings`"
118+
throw AuthServiceError
119+
.notConfiguredActionCodeSettings(errorMessage)
120+
}
121+
return actionCodeSettings
122+
}
123+
114124
func updateAuthenticationState() {
115125
authenticationState =
116126
(currentUser == nil || currentUser?.isAnonymous == true)
@@ -126,13 +136,12 @@ public final class AuthService {
126136
public func signInWithGoogle() async throws {
127137
authenticationState = .authenticating
128138
do {
129-
guard let clientID = auth.app?.options.clientID else { throw NSError(
130-
domain: "AuthServiceErrorDomain",
131-
code: 2,
132-
userInfo: [
133-
NSLocalizedDescriptionKey: "OAuth client ID not found. Please make sure Google Sign-In is enabled in the Firebase console. You may have to download a new GoogleService-Info.plist file after enabling Google Sign-In.",
134-
]
135-
) }
139+
guard let clientID = auth.app?.options.clientID else {
140+
throw AuthServiceError
141+
.clientIdNotFound(
142+
"OAuth client ID not found. Please make sure Google Sign-In is enabled in the Firebase console. You may have to download a new GoogleService-Info.plist file after enabling Google Sign-In."
143+
)
144+
}
136145
let credential = try await safeGoogleProvider.signInWithGoogle(clientID: clientID)
137146

138147
try await signIn(with: credential)
@@ -191,9 +200,7 @@ public final class AuthService {
191200

192201
func sendEmailSignInLink(to email: String) async throws {
193202
do {
194-
// TODO: - how does user set action code settings? Needs configuring
195-
let actionCodeSettings = ActionCodeSettings()
196-
actionCodeSettings.handleCodeInApp = true
203+
let actionCodeSettings = try safeActionCodeSettings()
197204
try await auth.sendSignInLink(
198205
toEmail: email,
199206
actionCodeSettings: actionCodeSettings
@@ -202,4 +209,22 @@ public final class AuthService {
202209
throw error
203210
}
204211
}
212+
213+
func handleSignInLink(url url: URL) async throws {
214+
do {
215+
guard let email = emailLink else {
216+
throw AuthServiceError.invalidEmailLink(
217+
"Invalid email address. Most likely, the link you used has expired. Try signing in again."
218+
)
219+
}
220+
let link = url.absoluteString
221+
if auth.isSignIn(withEmailLink: link) {
222+
let result = try await auth.signIn(withEmail: email, link: link)
223+
updateAuthenticationState()
224+
emailLink = nil
225+
}
226+
} catch {
227+
throw error
228+
}
229+
}
205230
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import FirebaseAuth
12
import SwiftUI
23

34
public struct EmailLinkView {
@@ -35,6 +36,7 @@ extension EmailLinkView: View {
3536
Button(action: {
3637
Task {
3738
await sendEmailLink()
39+
authService.emailLink = email
3840
}
3941
}) {
4042
Text("Send email sign-in link")
@@ -58,6 +60,14 @@ extension EmailLinkView: View {
5860
.padding()
5961
}
6062
.padding()
63+
}.onOpenURL { url in
64+
Task {
65+
do {
66+
try await authService.handleSignInLink(url: url)
67+
} catch {
68+
errorMessage = authService.string.localizedErrorMessage(for: error)
69+
}
70+
}
6171
}
6272
}
6373
}

samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExampleApp.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,14 @@ struct ContentView: View {
6363
let authService: AuthService
6464

6565
init() {
66-
authService = AuthService(googleProvider: googleProvider)
66+
let actionCodeSettings = ActionCodeSettings()
67+
actionCodeSettings.handleCodeInApp = true
68+
actionCodeSettings
69+
.url = URL(string: "https://flutterfire-e2e-tests.firebaseapp.com")
70+
actionCodeSettings.linkDomain = "flutterfire-e2e-tests.firebaseapp.com"
71+
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
72+
let configuration = AuthConfiguration(emailLinkSignInActionCodeSettings: actionCodeSettings)
73+
authService = AuthService(configuration: configuration, googleProvider: googleProvider)
6774
}
6875

6976
var body: some View {

0 commit comments

Comments
 (0)