@@ -130,11 +130,27 @@ public final class AuthService {
130130 public var currentUser : User ?
131131 public var authenticationState : AuthenticationState = . unauthenticated
132132 public var authenticationFlow : AuthenticationFlow = . signIn
133- public var currentError : AlertError ?
133+ private var _currentError : AlertError ?
134+
135+ /// A binding that allows SwiftUI views to observe and clear errors
136+ public var currentError : Binding < AlertError ? > {
137+ Binding (
138+ get: { self . _currentError } ,
139+ set: { newValue in
140+ if newValue == nil {
141+ self . _currentError = nil
142+ }
143+ }
144+ )
145+ }
146+
134147 public let passwordPrompt : PasswordPromptCoordinator = . init( )
135148 public var currentMFARequired : MFARequired ?
136149 private var currentMFAResolver : MultiFactorResolver ?
137150
151+ /// Current account conflict context - observe this to handle conflicts and update backend
152+ public private( set) var currentAccountConflict : AccountConflictContext ?
153+
138154 // MARK: - Provider APIs
139155
140156 private var listenerManager : AuthListenerManager ?
@@ -204,11 +220,12 @@ public final class AuthService {
204220 }
205221
206222 func reset( ) {
207- currentError = nil
223+ _currentError = nil
224+ currentAccountConflict = nil
208225 }
209226
210227 func updateError( title: String = " Error " , message: String , underlyingError: Error ? = nil ) {
211- currentError = AlertError ( title: title, message: message, underlyingError: underlyingError)
228+ _currentError = AlertError ( title: title, message: message, underlyingError: underlyingError)
212229 }
213230
214231 public var shouldHandleAnonymousUpgrade : Bool {
@@ -239,9 +256,12 @@ public final class AuthService {
239256 }
240257 updateAuthenticationState ( )
241258 } catch {
259+ // Possible conflicts from user.link():
260+ // - credentialAlreadyInUse: credential is already linked to another account
261+ // - emailAlreadyInUse: email from credential is already used by another account
262+ // - accountExistsWithDifferentCredential: account exists with different sign-in method
242263 authenticationState = . unauthenticated
243- updateError ( message: string. localizedErrorMessage ( for: error) , underlyingError: error)
244- throw error
264+ try handleErrorWithConflictCheck ( error: error, credential: credentials)
245265 }
246266 }
247267
@@ -254,32 +274,7 @@ public final class AuthService {
254274 let result = try await currentUser? . link ( with: credentials)
255275 updateAuthenticationState ( )
256276 return . signedIn( result)
257- } catch let error as NSError {
258- // Handle credentialAlreadyInUse error
259- if error. code == AuthErrorCode . credentialAlreadyInUse. rawValue {
260- // Extract the updated credential from the error
261- let updatedCredential = error. userInfo [ " FIRAuthUpdatedCredentialKey " ] as? AuthCredential
262- ?? credentials
263-
264- let context = AccountMergeConflictContext (
265- credential: updatedCredential,
266- underlyingError: error,
267- message: " Unable to merge accounts. The credential is already associated with a different account. " ,
268- uid: currentUser? . uid
269- )
270- throw AuthServiceError . accountMergeConflict ( context: context)
271- }
272-
273- // Handle emailAlreadyInUse error
274- if error. code == AuthErrorCode . emailAlreadyInUse. rawValue {
275- let context = AccountMergeConflictContext (
276- credential: credentials,
277- underlyingError: error,
278- message: " Unable to merge accounts. This email is already associated with a different account. " ,
279- uid: currentUser? . uid
280- )
281- throw AuthServiceError . accountMergeConflict ( context: context)
282- }
277+ } catch {
283278 throw error
284279 }
285280 }
@@ -296,18 +291,19 @@ public final class AuthService {
296291 }
297292 } catch let error as NSError {
298293 authenticationState = . unauthenticated
294+
299295 // Check if this is an MFA required error
300296 if error. code == AuthErrorCode . secondFactorRequired. rawValue {
301297 if let resolver = error
302298 . userInfo [ AuthErrorUserInfoMultiFactorResolverKey] as? MultiFactorResolver {
303299 return handleMFARequiredError ( resolver: resolver)
304300 }
305- } else {
306- // Don't want error modal on MFA error so we only update here
307- updateError ( message: string. localizedErrorMessage ( for: error) , underlyingError: error)
308301 }
309302
310- throw error
303+ // Possible conflicts from auth.signIn(with:):
304+ // - accountExistsWithDifferentCredential: account exists with different provider
305+ // - credentialAlreadyInUse: credential is already linked to another account
306+ try handleErrorWithConflictCheck ( error: error, credential: credentials)
311307 }
312308 }
313309
@@ -381,20 +377,21 @@ public extension AuthService {
381377
382378 func createUser( email email: String , password: String ) async throws -> SignInOutcome {
383379 authenticationState = . authenticating
380+ let credential = EmailAuthProvider . credential ( withEmail: email, password: password)
384381
385382 do {
386383 if shouldHandleAnonymousUpgrade {
387- let credential = EmailAuthProvider . credential ( withEmail: email, password: password)
388384 return try await handleAutoUpgradeAnonymousUser ( credentials: credential)
389385 } else {
390386 let result = try await auth. createUser ( withEmail: email, password: password)
391387 updateAuthenticationState ( )
392388 return . signedIn( result)
393389 }
394390 } catch {
391+ // Possible conflicts from auth.createUser():
392+ // - emailAlreadyInUse: email is already registered with another account
395393 authenticationState = . unauthenticated
396- updateError ( message: string. localizedErrorMessage ( for: error) , underlyingError: error)
397- throw error
394+ try handleErrorWithConflictCheck ( error: error, credential: credential)
398395 }
399396 }
400397
@@ -452,8 +449,18 @@ public extension AuthService {
452449 emailLink = nil
453450 }
454451 } catch {
455- updateError ( message: string. localizedErrorMessage ( for: error) , underlyingError: error)
456- throw error
452+ // Reconstruct credential for conflict handling
453+ let link = url. absoluteString
454+ guard let email = emailLink else {
455+ throw AuthServiceError
456+ . invalidEmailLink ( " email address is missing from app storage. Is this the same device? " )
457+ }
458+ let credential = EmailAuthProvider . credential ( withEmail: email, link: link)
459+
460+ // Possible conflicts from auth.signIn(withEmail:link:):
461+ // - accountExistsWithDifferentCredential: account exists with different provider
462+ // - credentialAlreadyInUse: credential is already linked to another account
463+ try handleErrorWithConflictCheck ( error: error, credential: credential)
457464 }
458465 }
459466
@@ -853,6 +860,71 @@ public extension AuthService {
853860 }
854861 }
855862
863+ // MARK: - Account Conflict Helper Methods
864+
865+ private func determineConflictType( from error: NSError ) -> AccountConflictType ? {
866+ switch error. code {
867+ case AuthErrorCode . accountExistsWithDifferentCredential. rawValue:
868+ return shouldHandleAnonymousUpgrade ? . anonymousUpgradeConflict :
869+ . accountExistsWithDifferentCredential
870+ case AuthErrorCode . credentialAlreadyInUse. rawValue:
871+ return shouldHandleAnonymousUpgrade ? . anonymousUpgradeConflict : . credentialAlreadyInUse
872+ case AuthErrorCode . emailAlreadyInUse. rawValue:
873+ return shouldHandleAnonymousUpgrade ? . anonymousUpgradeConflict : . emailAlreadyInUse
874+ default :
875+ return nil
876+ }
877+ }
878+
879+ private func createConflictContext( from error: NSError ,
880+ conflictType: AccountConflictType ,
881+ credential: AuthCredential ) -> AccountConflictContext {
882+ let updatedCredential = error
883+ . userInfo [ AuthErrorUserInfoUpdatedCredentialKey] as? AuthCredential ?? credential
884+ let email = error. userInfo [ AuthErrorUserInfoEmailKey] as? String
885+
886+ return AccountConflictContext (
887+ conflictType: conflictType,
888+ credential: updatedCredential,
889+ underlyingError: error,
890+ message: string. localizedErrorMessage ( for: error) ,
891+ email: email
892+ )
893+ }
894+
895+ /// Handles account conflict errors by creating context, storing it, and throwing structured error
896+ /// - Parameters:
897+ /// - error: The error to check and handle
898+ /// - credential: The credential that caused the conflict
899+ /// - Throws: AuthServiceError.accountConflict if it's a conflict error, otherwise rethrows the
900+ /// original error
901+ private func handleErrorWithConflictCheck( error: Error ,
902+ credential: AuthCredential ) throws -> Never {
903+ // Check for account conflict errors
904+ if let error = error as NSError ? ,
905+ let conflictType = determineConflictType ( from: error) {
906+ let context = createConflictContext (
907+ from: error,
908+ conflictType: conflictType,
909+ credential: credential
910+ )
911+
912+ // Store it for consumers to observe
913+ currentAccountConflict = context
914+
915+ // Only set error alert if we're NOT auto-handling it
916+ if conflictType != . anonymousUpgradeConflict {
917+ updateError ( message: context. message, underlyingError: error)
918+ }
919+
920+ // Throw the specific error with context
921+ throw AuthServiceError . accountConflict ( context)
922+ } else {
923+ updateError ( message: string. localizedErrorMessage ( for: error) , underlyingError: error)
924+ throw error
925+ }
926+ }
927+
856928 // MARK: - MFA Helper Methods
857929
858930 private func extractMFAHints( from resolver: MultiFactorResolver ) -> [ MFAHint ] {
0 commit comments