-
Notifications
You must be signed in to change notification settings - Fork 485
feat: anonymous account upgrade with error handling #1247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
699c134
52b92fb
5614990
89db7b0
7d75ca2
89e3b6f
32d82c2
3f796c6
fbd0d4f
086cb4e
b26351c
a014c7a
6721314
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,45 @@ | ||
|
||
import FirebaseAuth | ||
import SwiftUI | ||
|
||
public struct AccountMergeConflictContext: LocalizedError { | ||
public let credential: AuthCredential | ||
public let underlyingError: Error | ||
public let message: String | ||
|
||
public var errorDescription: String? { | ||
return message | ||
} | ||
} | ||
|
||
public enum AuthServiceError: LocalizedError { | ||
case invalidEmailLink | ||
case invalidEmailLink(String) | ||
case notConfiguredProvider(String) | ||
case clientIdNotFound(String) | ||
case notConfiguredActionCodeSettings | ||
case notConfiguredActionCodeSettings(String) | ||
case reauthenticationRequired(String) | ||
case invalidCredentials(String) | ||
case signInFailed(underlying: Error) | ||
case accountMergeConflict(context: AccountMergeConflictContext) | ||
|
||
public var errorDescription: String? { | ||
switch self { | ||
case .invalidEmailLink: | ||
return "Invalid sign in link. Most likely, the link you used has expired. Try signing in again." | ||
case let .invalidEmailLink(description): | ||
return description | ||
case let .notConfiguredProvider(description): | ||
return description | ||
case let .clientIdNotFound(description): | ||
return description | ||
case .notConfiguredActionCodeSettings: | ||
return "ActionCodeSettings has not been configured for `AuthConfiguration.emailLinkSignInActionCodeSettings`" | ||
case let .notConfiguredActionCodeSettings(description): | ||
return description | ||
case let .reauthenticationRequired(description): | ||
return description | ||
case let .invalidCredentials(description): | ||
return description | ||
case let .signInFailed(underlying: error): | ||
return "Failed to sign in: \(error.localizedDescription)" | ||
case let .accountMergeConflict(context): | ||
return context.errorDescription | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -124,7 +124,9 @@ public final class AuthService { | |
guard let actionCodeSettings = configuration | ||
.emailLinkSignInActionCodeSettings else { | ||
throw AuthServiceError | ||
.notConfiguredActionCodeSettings | ||
.notConfiguredActionCodeSettings( | ||
"ActionCodeSettings has not been configured for `AuthConfiguration.emailLinkSignInActionCodeSettings`" | ||
) | ||
} | ||
return actionCodeSettings | ||
} | ||
|
@@ -141,6 +143,10 @@ public final class AuthService { | |
errorMessage = "" | ||
} | ||
|
||
public var shouldHandleAnonymousUpgrade: Bool { | ||
currentUser?.isAnonymous == true && configuration.shouldAutoUpgradeAnonymousUsers | ||
} | ||
|
||
public func signOut() async throws { | ||
do { | ||
try await auth.signOut() | ||
|
@@ -167,21 +173,37 @@ public final class AuthService { | |
} | ||
} | ||
|
||
public func handleAutoUpgradeAnonymousUser(credentials credentials: AuthCredential) async throws { | ||
do { | ||
try await currentUser?.link(with: credentials) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should handle the In theory There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @morganchen12 - updated. I didn't add User to the exception as it produced an Xcode compiler error: Stored property 'user' of 'Sendable'-conforming struct 'AccountMergeConflictContext' has non-sendable type 'User' It seems User is non-sendable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A fix for that is in the pipeline in upstream Firebase, so maybe leave a TODO and just send the user's UID (String) for now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, see |
||
} catch let error as NSError { | ||
if error.code == AuthErrorCode.emailAlreadyInUse.rawValue { | ||
let context = AccountMergeConflictContext( | ||
credential: credentials, | ||
underlyingError: error, | ||
message: "Unable to merge accounts. Use the credential in the context to resolve the conflict." | ||
) | ||
throw AuthServiceError.accountMergeConflict(context: context) | ||
} | ||
throw error | ||
} | ||
} | ||
|
||
public func signIn(credentials credentials: AuthCredential) async throws { | ||
russellwheatley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
authenticationState = .authenticating | ||
if currentUser?.isAnonymous == true, configuration.shouldAutoUpgradeAnonymousUsers { | ||
try await linkAccounts(credentials: credentials) | ||
} else { | ||
do { | ||
do { | ||
if shouldHandleAnonymousUpgrade { | ||
try await handleAutoUpgradeAnonymousUser(credentials: credentials) | ||
} else { | ||
try await auth.signIn(with: credentials) | ||
updateAuthenticationState() | ||
} catch { | ||
authenticationState = .unauthenticated | ||
errorMessage = string.localizedErrorMessage( | ||
for: error | ||
) | ||
throw error | ||
} | ||
updateAuthenticationState() | ||
} catch { | ||
authenticationState = .unauthenticated | ||
errorMessage = string.localizedErrorMessage( | ||
for: error | ||
) | ||
throw error | ||
} | ||
} | ||
|
||
|
@@ -231,7 +253,12 @@ public extension AuthService { | |
authenticationState = .authenticating | ||
|
||
do { | ||
try await auth.createUser(withEmail: email, password: password) | ||
if shouldHandleAnonymousUpgrade { | ||
let credential = EmailAuthProvider.credential(withEmail: email, password: password) | ||
try await handleAutoUpgradeAnonymousUser(credentials: credential) | ||
} else { | ||
try await auth.createUser(withEmail: email, password: password) | ||
} | ||
updateAuthenticationState() | ||
} catch { | ||
authenticationState = .unauthenticated | ||
|
@@ -259,7 +286,7 @@ public extension AuthService { | |
public extension AuthService { | ||
func sendEmailSignInLink(to email: String) async throws { | ||
do { | ||
let actionCodeSettings = try safeActionCodeSettings() | ||
let actionCodeSettings = try updateActionCodeSettings() | ||
try await auth.sendSignInLink( | ||
toEmail: email, | ||
actionCodeSettings: actionCodeSettings | ||
|
@@ -275,11 +302,27 @@ public extension AuthService { | |
func handleSignInLink(url url: URL) async throws { | ||
do { | ||
guard let email = emailLink else { | ||
throw AuthServiceError.invalidEmailLink | ||
throw AuthServiceError | ||
.invalidEmailLink("email address is missing from app storage. Is this the same device?") | ||
} | ||
let link = url.absoluteString | ||
guard let continueUrl = CommonUtils.getQueryParamValue(from: link, paramName: "continueUrl") | ||
else { | ||
throw AuthServiceError | ||
.invalidEmailLink("`continueUrl` parameter is missing from the email link URL") | ||
} | ||
|
||
if auth.isSignIn(withEmailLink: link) { | ||
let result = try await auth.signIn(withEmail: email, link: link) | ||
let anonymousUserID = CommonUtils.getQueryParamValue( | ||
from: continueUrl, | ||
paramName: "ui_auid" | ||
) | ||
if shouldHandleAnonymousUpgrade, anonymousUserID == currentUser?.uid { | ||
let credential = EmailAuthProvider.credential(withEmail: email, link: link) | ||
try await handleAutoUpgradeAnonymousUser(credentials: credential) | ||
} else { | ||
let result = try await auth.signIn(withEmail: email, link: link) | ||
} | ||
updateAuthenticationState() | ||
emailLink = nil | ||
} | ||
|
@@ -290,6 +333,33 @@ public extension AuthService { | |
throw error | ||
} | ||
} | ||
|
||
private func updateActionCodeSettings() throws -> ActionCodeSettings { | ||
let actionCodeSettings = try safeActionCodeSettings() | ||
guard var urlComponents = URLComponents(string: actionCodeSettings.url!.absoluteString) else { | ||
throw AuthServiceError | ||
.notConfiguredActionCodeSettings( | ||
"ActionCodeSettings.url has not been configured for `AuthConfiguration.emailLinkSignInActionCodeSettings`" | ||
) | ||
} | ||
|
||
var queryItems: [URLQueryItem] = [] | ||
|
||
if shouldHandleAnonymousUpgrade { | ||
if let currentUser = currentUser { | ||
let anonymousUID = currentUser.uid | ||
let auidItem = URLQueryItem(name: "ui_auid", value: anonymousUID) | ||
queryItems.append(auidItem) | ||
} | ||
} | ||
|
||
urlComponents.queryItems = queryItems | ||
if let finalURL = urlComponents.url { | ||
actionCodeSettings.url = finalURL | ||
} | ||
|
||
return actionCodeSettings | ||
} | ||
} | ||
|
||
// MARK: - Google Sign In | ||
|
Uh oh!
There was an error while loading. Please reload this page.