Skip to content

Commit 37970aa

Browse files
Merge branch 'main' into facebook-reauthentication
2 parents 323eef6 + ff1c0bd commit 37970aa

File tree

4 files changed

+126
-25
lines changed

4 files changed

+126
-25
lines changed
Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,50 @@
11

2+
import FirebaseAuth
23
import SwiftUI
34

5+
public struct AccountMergeConflictContext: LocalizedError {
6+
public let credential: AuthCredential
7+
public let underlyingError: Error
8+
public let message: String
9+
// TODO: - should make this User type once fixed upstream in firebase-ios-sdk. See: https://github.com/firebase/FirebaseUI-iOS/pull/1247#discussion_r2085455355
10+
public let uid: String?
11+
12+
public var errorDescription: String? {
13+
return message
14+
}
15+
}
16+
417
public enum AuthServiceError: LocalizedError {
5-
case invalidEmailLink
18+
case noCurrentUser
19+
case invalidEmailLink(String)
620
case notConfiguredProvider(String)
721
case clientIdNotFound(String)
8-
case notConfiguredActionCodeSettings
22+
case notConfiguredActionCodeSettings(String)
923
case reauthenticationRequired(String)
1024
case invalidCredentials(String)
1125
case signInFailed(underlying: Error)
26+
case accountMergeConflict(context: AccountMergeConflictContext)
1227

1328
public var errorDescription: String? {
1429
switch self {
15-
case .invalidEmailLink:
16-
return "Invalid sign in link. Most likely, the link you used has expired. Try signing in again."
30+
case .noCurrentUser:
31+
return "No user is currently signed in."
32+
case let .invalidEmailLink(description):
33+
return description
1734
case let .notConfiguredProvider(description):
1835
return description
1936
case let .clientIdNotFound(description):
2037
return description
21-
case .notConfiguredActionCodeSettings:
22-
return "ActionCodeSettings has not been configured for `AuthConfiguration.emailLinkSignInActionCodeSettings`"
38+
case let .notConfiguredActionCodeSettings(description):
39+
return description
2340
case let .reauthenticationRequired(description):
2441
return description
2542
case let .invalidCredentials(description):
2643
return description
2744
case let .signInFailed(underlying: error):
2845
return "Failed to sign in: \(error.localizedDescription)"
46+
case let .accountMergeConflict(context):
47+
return context.errorDescription
2948
}
3049
}
3150
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,9 @@ public final class AuthService {
152152
guard let actionCodeSettings = configuration
153153
.emailLinkSignInActionCodeSettings else {
154154
throw AuthServiceError
155-
.notConfiguredActionCodeSettings
155+
.notConfiguredActionCodeSettings(
156+
"ActionCodeSettings has not been configured for `AuthConfiguration.emailLinkSignInActionCodeSettings`"
157+
)
156158
}
157159
return actionCodeSettings
158160
}
@@ -169,6 +171,10 @@ public final class AuthService {
169171
errorMessage = ""
170172
}
171173

174+
public var shouldHandleAnonymousUpgrade: Bool {
175+
currentUser?.isAnonymous == true && configuration.shouldAutoUpgradeAnonymousUsers
176+
}
177+
172178
public func signOut() async throws {
173179
do {
174180
try await auth.signOut()
@@ -195,22 +201,42 @@ public final class AuthService {
195201
}
196202
}
197203

198-
public func signIn(credentials credentials: AuthCredential) async throws {
204+
public func handleAutoUpgradeAnonymousUser(credentials: AuthCredential) async throws {
205+
if currentUser == nil {
206+
throw AuthServiceError.noCurrentUser
207+
}
208+
do {
209+
try await currentUser?.link(with: credentials)
210+
} catch let error as NSError {
211+
if error.code == AuthErrorCode.emailAlreadyInUse.rawValue {
212+
let context = AccountMergeConflictContext(
213+
credential: credentials,
214+
underlyingError: error,
215+
message: "Unable to merge accounts. Use the credential in the context to resolve the conflict.",
216+
uid: currentUser?.uid
217+
)
218+
throw AuthServiceError.accountMergeConflict(context: context)
219+
}
220+
throw error
221+
}
222+
}
223+
224+
public func signIn(credentials: AuthCredential) async throws {
199225
authenticationState = .authenticating
200-
if currentUser?.isAnonymous == true, configuration.shouldAutoUpgradeAnonymousUsers {
201-
try await linkAccounts(credentials: credentials)
202-
} else {
203-
do {
226+
do {
227+
if shouldHandleAnonymousUpgrade {
228+
try await handleAutoUpgradeAnonymousUser(credentials: credentials)
229+
} else {
204230
let result = try await auth.signIn(with: credentials)
205231
signedInCredential = result.credential
206-
updateAuthenticationState()
207-
} catch {
208-
authenticationState = .unauthenticated
209-
errorMessage = string.localizedErrorMessage(
210-
for: error
211-
)
212-
throw error
213232
}
233+
updateAuthenticationState()
234+
} catch {
235+
authenticationState = .unauthenticated
236+
errorMessage = string.localizedErrorMessage(
237+
for: error
238+
)
239+
throw error
214240
}
215241
}
216242

@@ -282,8 +308,13 @@ public extension AuthService {
282308
authenticationState = .authenticating
283309

284310
do {
285-
let result = try await auth.createUser(withEmail: email, password: password)
286-
signedInCredential = result.credential
311+
if shouldHandleAnonymousUpgrade {
312+
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
313+
try await handleAutoUpgradeAnonymousUser(credentials: credential)
314+
} else {
315+
let result = try await auth.createUser(withEmail: email, password: password)
316+
signedInCredential = result.credential
317+
}
287318
updateAuthenticationState()
288319
} catch {
289320
authenticationState = .unauthenticated
@@ -311,7 +342,7 @@ public extension AuthService {
311342
public extension AuthService {
312343
func sendEmailSignInLink(to email: String) async throws {
313344
do {
314-
let actionCodeSettings = try safeActionCodeSettings()
345+
let actionCodeSettings = try updateActionCodeSettings()
315346
try await auth.sendSignInLink(
316347
toEmail: email,
317348
actionCodeSettings: actionCodeSettings
@@ -327,11 +358,27 @@ public extension AuthService {
327358
func handleSignInLink(url url: URL) async throws {
328359
do {
329360
guard let email = emailLink else {
330-
throw AuthServiceError.invalidEmailLink
361+
throw AuthServiceError
362+
.invalidEmailLink("email address is missing from app storage. Is this the same device?")
331363
}
332364
let link = url.absoluteString
365+
guard let continueUrl = CommonUtils.getQueryParamValue(from: link, paramName: "continueUrl")
366+
else {
367+
throw AuthServiceError
368+
.invalidEmailLink("`continueUrl` parameter is missing from the email link URL")
369+
}
370+
333371
if auth.isSignIn(withEmailLink: link) {
334-
let result = try await auth.signIn(withEmail: email, link: link)
372+
let anonymousUserID = CommonUtils.getQueryParamValue(
373+
from: continueUrl,
374+
paramName: "ui_auid"
375+
)
376+
if shouldHandleAnonymousUpgrade, anonymousUserID == currentUser?.uid {
377+
let credential = EmailAuthProvider.credential(withEmail: email, link: link)
378+
try await handleAutoUpgradeAnonymousUser(credentials: credential)
379+
} else {
380+
let result = try await auth.signIn(withEmail: email, link: link)
381+
}
335382
updateAuthenticationState()
336383
emailLink = nil
337384
}
@@ -342,6 +389,33 @@ public extension AuthService {
342389
throw error
343390
}
344391
}
392+
393+
private func updateActionCodeSettings() throws -> ActionCodeSettings {
394+
let actionCodeSettings = try safeActionCodeSettings()
395+
guard var urlComponents = URLComponents(string: actionCodeSettings.url!.absoluteString) else {
396+
throw AuthServiceError
397+
.notConfiguredActionCodeSettings(
398+
"ActionCodeSettings.url has not been configured for `AuthConfiguration.emailLinkSignInActionCodeSettings`"
399+
)
400+
}
401+
402+
var queryItems: [URLQueryItem] = []
403+
404+
if shouldHandleAnonymousUpgrade {
405+
if let currentUser = currentUser {
406+
let anonymousUID = currentUser.uid
407+
let auidItem = URLQueryItem(name: "ui_auid", value: anonymousUID)
408+
queryItems.append(auidItem)
409+
}
410+
}
411+
412+
urlComponents.queryItems = queryItems
413+
if let finalURL = urlComponents.url {
414+
actionCodeSettings.url = finalURL
415+
}
416+
417+
return actionCodeSettings
418+
}
345419
}
346420

347421
// MARK: - Google Sign In

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Utils/CommonUtils.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ public class CommonUtils {
4747
}
4848
return hash.map { String(format: "%02x", $0) }.joined()
4949
}
50+
51+
public static func getQueryParamValue(from urlString: String, paramName: String) -> String? {
52+
guard let urlComponents = URLComponents(string: urlString) else {
53+
return nil
54+
}
55+
56+
return urlComponents.queryItems?.first(where: { $0.name == paramName })?.value
57+
}
5058
}
5159

5260
public extension FirebaseOptions {

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ extension EmailAuthView: View {
127127
.frame(maxWidth: .infinity)
128128
.buttonStyle(.borderedProminent)
129129
Button(action: {
130-
authService.authView = .passwordRecovery
130+
authService.authView = .emailLink
131131
}) {
132132
Text(authService.string.signUpWithEmailLinkButtonLabel)
133133
}

0 commit comments

Comments
 (0)