Skip to content

Conversation

@russellwheatley
Copy link
Member

@russellwheatley russellwheatley commented Nov 4, 2025

Account Conflict Handling: Typed Errors & Auto-Recovery for Anonymous Users

Summary

This PR introduces a comprehensive, type-safe system for handling account credential conflicts during sign-in, with special support for automatic anonymous user upgrade recovery. The implementation separates concerns between the service layer (AuthService) and view layer (AuthPickerView), allowing both opinionated default behaviour and flexible custom implementations if the consumer decides to implement their own custom Views.

What Changed

1. Typed Account Conflict System (AuthServiceError.swift)

Added structured conflict types to replace generic error handling:

public enum AccountConflictType: Equatable {
  case accountExistsWithDifferentCredential  // User signed up with different provider
  case credentialAlreadyInUse                 // Credential linked to another account
  case emailAlreadyInUse                      // Email already registered
  case anonymousUpgradeConflict               // Anonymous user conflicts with existing account
}

public struct AccountConflictContext: LocalizedError, Identifiable, Equatable {
  public let conflictType: AccountConflictType
  public let credential: AuthCredential       // Available for backend updates
  public let underlyingError: Error
  public let message: String
  public let email: String?
  public var conflictDescription: String      // Human-readable description
}

Renamed: AuthServiceError.accountMergeConflictAuthServiceError.accountConflict

2. Dual-Purpose Error Properties (AuthService.swift)

Added observable property for programmatic conflict handling while maintaining existing error alert system:

public private(set) var currentError: AlertError?            // For displaying alerts to users
public private(set) var currentAccountConflict: AccountConflictContext?  // For programmatic handling**How it works:**
  • When a conflict occurs during sign-in, AuthService detects the error type
  • Sets currentAccountConflict with full context (always)
  • Sets currentError for user-facing alerts (except for auto-handled conflicts)
  • Throws AuthServiceError.accountConflict for immediate error handling

3. Opinionated View Layer: Auto-Recovery (AuthPickerView.swift)

AuthPickerView now automatically handles anonymous user upgrade conflicts:

.onChange(of: authService.currentAccountConflict) { _, conflict in
  if conflict.conflictType == .anonymousUpgradeConflict {
    // Automatically sign out anonymous user and sign in with new credential
    try await authService.signOut()
    _ = try await authService.signIn(credentials: conflict.credential)
  } else {
    // Store credential for linking after manual sign-in
    pendingCredentialForLinking = conflict.credential
  }
}

Behavior:

  • Anonymous upgrade conflicts: Silently handled (no error alert shown)
  • Other conflicts: Error alert shown to user, credential stored for potential linking

4. Smart Alert Filtering (ErrorAlertView.swift)

Updated ErrorAlertModifier to prevent showing alerts for auto-handled conflicts:

private func shouldShowAlert(for error: AlertError?) -> Bool {
  // Don't show alert for anonymous upgrade conflicts (auto-handled)
  if let authError = error.underlyingError as? AuthServiceError,
     case .accountConflict(let context) = authError,
     context.conflictType == .anonymousUpgradeConflict {
    return false
  }
  return true
}

Using the API in Custom Views

Consumers can observe currentAccountConflict to implement custom behaviour or update their backend:

@Environment(AuthService.self) private var authService

var body: some View {
  MyCustomAuthView()
    .onChange(of: authService.currentAccountConflict) { _, conflict in
      guard let conflict = conflict else { return }
      
      // Type-safe pattern matching
      switch conflict.conflictType {
      case .accountExistsWithDifferentCredential:
        // Update backend: user needs to sign in with existing provider
        await notifyBackend(
          email: conflict.email,
          attemptedCredential: conflict.credential
        )
        
      case .credentialAlreadyInUse:
        await logConflictToBackend(credential: conflict.credential)
        
      case .emailAlreadyInUse:
        await updateBackendEmailStatus(email: conflict.email)
        
      case .anonymousUpgradeConflict:
        // Implement custom recovery logic or use default
        break
      }
    }
}

Or catch the error directly:

do {
  let outcome = try await authService.signIn(myProvider)
} catch let error as AuthServiceError {
  if case .accountConflict(let context) = error {
    // Access conflict details
    print("Conflict type: \(context.conflictType)")
    await updateBackend(with: context.credential)
  }
}

@russellwheatley russellwheatley changed the title feat: attempt to link accounts if get account merge conflict refactor: error handling, merge conflicts and auto anonymous upgrade Nov 4, 2025
@russellwheatley russellwheatley marked this pull request as ready for review November 5, 2025 12:00
@russellwheatley russellwheatley merged commit d9e7b79 into development Nov 5, 2025
2 of 4 checks passed
@russellwheatley russellwheatley deleted the account-exists-diff-credential branch November 5, 2025 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants