Skip to content

Commit 265830c

Browse files
refactor: handle auto upgrade anonymous improvement
1 parent ff8ff47 commit 265830c

10 files changed

Lines changed: 142 additions & 11 deletions

File tree

FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Views/SignInWithAppleButton.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import SwiftUI
1919
@MainActor
2020
public struct SignInWithAppleButton {
2121
@Environment(AuthService.self) private var authService
22+
@Environment(\.signInWithMergeConflictHandler) private var signInHandler
2223
let provider: AuthProviderSwift
2324
public init(provider: AuthProviderSwift) {
2425
self.provider = provider
@@ -29,7 +30,13 @@ extension SignInWithAppleButton: View {
2930
public var body: some View {
3031
Button(action: {
3132
Task {
32-
try? await authService.signIn(provider)
33+
if let handler = signInHandler {
34+
try? await handler(authService) {
35+
try await authService.signIn(provider)
36+
}
37+
} else {
38+
try? await authService.signIn(provider)
39+
}
3340
}
3441
}) {
3542
HStack {

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ public final class AuthService {
177177
public func signOut() async throws {
178178
do {
179179
try await auth.signOut()
180+
// Cannot wait for auth listener to change, feedback needs to be immediate
181+
currentUser = nil
180182
updateAuthenticationState()
181183
} catch {
182184
updateError(message: string.localizedErrorMessage(for: error))
@@ -202,7 +204,7 @@ public final class AuthService {
202204
}
203205
}
204206

205-
public func handleAutoUpgradeAnonymousUser(credentials: AuthCredential) async throws
207+
private func handleAutoUpgradeAnonymousUser(credentials: AuthCredential) async throws
206208
-> SignInOutcome {
207209
if currentUser == nil {
208210
throw AuthServiceError.noCurrentUser
@@ -213,11 +215,27 @@ public final class AuthService {
213215
updateAuthenticationState()
214216
return .signedIn(result)
215217
} catch let error as NSError {
218+
// Handle credentialAlreadyInUse error
219+
if error.code == AuthErrorCode.credentialAlreadyInUse.rawValue {
220+
// Extract the updated credential from the error
221+
let updatedCredential = error.userInfo["FIRAuthUpdatedCredentialKey"] as? AuthCredential
222+
?? credentials
223+
224+
let context = AccountMergeConflictContext(
225+
credential: updatedCredential,
226+
underlyingError: error,
227+
message: "Unable to merge accounts. The credential is already associated with a different account.",
228+
uid: currentUser?.uid
229+
)
230+
throw AuthServiceError.accountMergeConflict(context: context)
231+
}
232+
233+
// Handle emailAlreadyInUse error
216234
if error.code == AuthErrorCode.emailAlreadyInUse.rawValue {
217235
let context = AccountMergeConflictContext(
218236
credential: credentials,
219237
underlyingError: error,
220-
message: "Unable to merge accounts. Use the credential in the context to resolve the conflict.",
238+
message: "Unable to merge accounts. This email is already associated with a different account.",
221239
uid: currentUser?.uid
222240
)
223241
throw AuthServiceError.accountMergeConflict(context: context)

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,59 @@
1515
import FirebaseCore
1616
import SwiftUI
1717

18+
// MARK: - Merge Conflict Handling
19+
20+
/// Helper function to handle sign-in with automatic merge conflict resolution.
21+
///
22+
/// This function attempts to sign in with the provided action. If a merge conflict occurs
23+
/// (when an anonymous user is being upgraded and the credential is already associated with
24+
/// an existing account), it automatically signs out the anonymous user and signs in with
25+
/// the existing account's credential.
26+
///
27+
/// - Parameters:
28+
/// - authService: The AuthService instance to use for sign-in operations
29+
/// - signInAction: An async closure that performs the sign-in operation
30+
/// - Returns: The SignInOutcome from the successful sign-in
31+
/// - Throws: Re-throws any errors except accountMergeConflict (which is handled internally)
32+
@MainActor
33+
public func signInWithMergeConflictHandling(authService: AuthService,
34+
signInAction: () async throws
35+
-> SignInOutcome) async throws -> SignInOutcome {
36+
do {
37+
return try await signInAction()
38+
} catch let error as AuthServiceError {
39+
if case let .accountMergeConflict(context) = error {
40+
// The anonymous account conflicts with an existing account
41+
// Sign out the anonymous user
42+
try await authService.signOut()
43+
44+
// Sign in with the existing account's credential
45+
// This works because shouldHandleAnonymousUpgrade is now false after sign out
46+
return try await authService.signIn(credentials: context.credential)
47+
}
48+
throw error
49+
}
50+
}
51+
52+
// MARK: - Environment Key for Sign-In Handler
53+
54+
/// Environment key for a sign-in handler that includes merge conflict resolution
55+
private struct SignInHandlerKey: EnvironmentKey {
56+
static let defaultValue: (@MainActor (AuthService, () async throws -> SignInOutcome) async throws
57+
-> SignInOutcome)? = nil
58+
}
59+
60+
public extension EnvironmentValues {
61+
/// A sign-in handler that automatically handles merge conflicts for anonymous user upgrades.
62+
/// When set in the environment, views should use this handler to wrap their sign-in calls.
63+
var signInWithMergeConflictHandler: (@MainActor (AuthService,
64+
() async throws -> SignInOutcome) async throws
65+
-> SignInOutcome)? {
66+
get { self[SignInHandlerKey.self] }
67+
set { self[SignInHandlerKey.self] = newValue }
68+
}
69+
}
70+
1871
@MainActor
1972
public struct AuthPickerView {
2073
@Environment(AuthService.self) private var authService
@@ -67,10 +120,13 @@ extension AuthPickerView: View {
67120
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
68121
Divider()
69122
EmailAuthView()
123+
.environment(\.signInWithMergeConflictHandler, signInWithMergeConflictHandling)
70124
}
71125
VStack {
72126
authService.renderButtons()
73-
}.padding(.horizontal)
127+
}
128+
.padding(.horizontal)
129+
.environment(\.signInWithMergeConflictHandler, signInWithMergeConflictHandling)
74130
if authService.emailSignInEnabled {
75131
Divider()
76132
HStack {

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ private enum FocusableField: Hashable {
3131
@MainActor
3232
public struct EmailAuthView {
3333
@Environment(AuthService.self) private var authService
34+
@Environment(\.signInWithMergeConflictHandler) private var signInHandler
3435

3536
@State private var email = ""
3637
@State private var password = ""
@@ -49,11 +50,23 @@ public struct EmailAuthView {
4950
}
5051

5152
private func signInWithEmailPassword() async {
52-
try? await authService.signIn(email: email, password: password)
53+
if let handler = signInHandler {
54+
try? await handler(authService) {
55+
try await authService.signIn(email: email, password: password)
56+
}
57+
} else {
58+
try? await authService.signIn(email: email, password: password)
59+
}
5360
}
5461

5562
private func createUserWithEmailPassword() async {
56-
try? await authService.createUser(email: email, password: password)
63+
if let handler = signInHandler {
64+
try? await handler(authService) {
65+
try await authService.createUser(email: email, password: password)
66+
}
67+
} else {
68+
try? await authService.createUser(email: email, password: password)
69+
}
5770
}
5871
}
5972

FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import SwiftUI
2323
@MainActor
2424
public struct SignInWithFacebookButton {
2525
@Environment(AuthService.self) private var authService
26+
@Environment(\.signInWithMergeConflictHandler) private var signInHandler
2627
let facebookProvider: FacebookProviderSwift
2728
@State private var showCanceledAlert = false
2829
@State private var limitedLogin = true
@@ -67,7 +68,13 @@ extension SignInWithFacebookButton: View {
6768
Button(action: {
6869
Task {
6970
facebookProvider.isLimitedLogin = limitedLogin
70-
try? await authService.signIn(facebookProvider)
71+
if let handler = signInHandler {
72+
try? await handler(authService) {
73+
try await authService.signIn(facebookProvider)
74+
}
75+
} else {
76+
try? await authService.signIn(facebookProvider)
77+
}
7178
}
7279
}) {
7380
HStack {

FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import SwiftUI
2626
@MainActor
2727
public struct SignInWithGoogleButton {
2828
@Environment(AuthService.self) private var authService
29+
@Environment(\.signInWithMergeConflictHandler) private var signInHandler
2930
let googleProvider: AuthProviderSwift
3031

3132
public init(googleProvider: AuthProviderSwift) {
@@ -43,7 +44,13 @@ extension SignInWithGoogleButton: View {
4344
public var body: some View {
4445
GoogleSignInButton(viewModel: customViewModel) {
4546
Task {
46-
try? await authService.signIn(googleProvider)
47+
if let handler = signInHandler {
48+
try? await handler(authService) {
49+
try await authService.signIn(googleProvider)
50+
}
51+
} else {
52+
try? await authService.signIn(googleProvider)
53+
}
4754
}
4855
}
4956
.accessibilityIdentifier("sign-in-with-google-button")

FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Views/GenericOAuthButton.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import SwiftUI
1919
@MainActor
2020
public struct GenericOAuthButton {
2121
@Environment(AuthService.self) private var authService
22+
@Environment(\.signInWithMergeConflictHandler) private var signInHandler
2223
let provider: AuthProviderSwift
2324
public init(provider: AuthProviderSwift) {
2425
self.provider = provider
@@ -36,7 +37,13 @@ extension GenericOAuthButton: View {
3637
return AnyView(
3738
Button(action: {
3839
Task {
39-
try await authService.signIn(provider)
40+
if let handler = signInHandler {
41+
try? await handler(authService) {
42+
try await authService.signIn(provider)
43+
}
44+
} else {
45+
try? await authService.signIn(provider)
46+
}
4047
}
4148
}) {
4249
HStack {

FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthButtonView.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import SwiftUI
1919
@MainActor
2020
public struct PhoneAuthButtonView {
2121
@Environment(AuthService.self) private var authService
22+
@Environment(\.signInWithMergeConflictHandler) private var signInHandler
2223
let phoneProvider: PhoneAuthProviderSwift
2324

2425
public init(phoneProvider: PhoneAuthProviderSwift) {
@@ -30,7 +31,13 @@ extension PhoneAuthButtonView: View {
3031
public var body: some View {
3132
Button(action: {
3233
Task {
33-
try await authService.signIn(phoneProvider)
34+
if let handler = signInHandler {
35+
try? await handler(authService) {
36+
try await authService.signIn(phoneProvider)
37+
}
38+
} else {
39+
try? await authService.signIn(phoneProvider)
40+
}
3441
}
3542
}) {
3643
Label("Sign in with Phone", systemImage: "phone.fill")

FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Views/SignInWithTwitterButton.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import SwiftUI
1919
@MainActor
2020
public struct SignInWithTwitterButton {
2121
@Environment(AuthService.self) private var authService
22+
@Environment(\.signInWithMergeConflictHandler) private var signInHandler
2223
let provider: AuthProviderSwift
2324
public init(provider: AuthProviderSwift) {
2425
self.provider = provider
@@ -29,7 +30,13 @@ extension SignInWithTwitterButton: View {
2930
public var body: some View {
3031
Button(action: {
3132
Task {
32-
try? await authService.signIn(provider)
33+
if let handler = signInHandler {
34+
try? await handler(authService) {
35+
try await authService.signIn(provider)
36+
}
37+
} else {
38+
try? await authService.signIn(provider)
39+
}
3340
}
3441
}) {
3542
HStack {

samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@ struct ContentView: View {
3333
let authService: AuthService
3434

3535
init() {
36+
Auth.auth().signInAnonymously()
3637
let actionCodeSettings = ActionCodeSettings()
3738
actionCodeSettings.handleCodeInApp = true
3839
actionCodeSettings
3940
.url = URL(string: "https://flutterfire-e2e-tests.firebaseapp.com")
4041
actionCodeSettings.linkDomain = "flutterfire-e2e-tests.firebaseapp.com"
4142
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
4243
let configuration = AuthConfiguration(
44+
shouldAutoUpgradeAnonymousUsers: true,
4345
tosUrl: URL(string: "https://example.com/tos"),
4446
privacyPolicyUrl: URL(string: "https://example.com/privacy"),
4547
emailLinkSignInActionCodeSettings: actionCodeSettings,

0 commit comments

Comments
 (0)