1515@preconcurrency import FirebaseAuth
1616import SwiftUI
1717
18- public protocol ExternalAuthProvider {
19- var id : String { get }
20- @MainActor func authButton( ) -> AnyView
18+ public protocol AuthProviderSwift {
19+ @MainActor func createAuthCredential( ) async throws -> AuthCredential
2120}
2221
23- public protocol GoogleProviderAuthUIProtocol : ExternalAuthProvider {
24- @MainActor func signInWithGoogle( clientID: String ) async throws -> AuthCredential
25- @MainActor func deleteUser( user: User ) async throws
22+ public protocol AuthProviderUI {
23+ var id : String { get }
24+ @MainActor func authButton( ) -> AnyView
25+ var provider : AuthProviderSwift { get }
2626}
2727
28- public protocol FacebookProviderAuthUIProtocol : ExternalAuthProvider {
29- @MainActor func signInWithFacebook( isLimitedLogin: Bool ) async throws -> AuthCredential
28+ public protocol DeleteUserSwift {
3029 @MainActor func deleteUser( user: User ) async throws
3130}
3231
33- public protocol PhoneAuthProviderAuthUIProtocol : ExternalAuthProvider {
32+ public protocol PhoneAuthProviderAuthUIProtocol : AuthProviderSwift {
3433 @MainActor func verifyPhoneNumber( phoneNumber: String ) async throws -> String
3534}
3635
@@ -41,7 +40,7 @@ public enum AuthenticationState {
4140}
4241
4342public enum AuthenticationFlow {
44- case login
43+ case signIn
4544 case signUp
4645}
4746
@@ -52,6 +51,10 @@ public enum AuthView {
5251 case updatePassword
5352}
5453
54+ public enum SignInOutcome : @unchecked Sendable {
55+ case signedIn( AuthDataResult ? )
56+ }
57+
5558@MainActor
5659private final class AuthListenerManager {
5760 private var authStateHandle : AuthStateDidChangeListenerHandle ?
@@ -95,7 +98,7 @@ public final class AuthService {
9598 public let string : StringUtils
9699 public var currentUser : User ?
97100 public var authenticationState : AuthenticationState = . unauthenticated
98- public var authenticationFlow : AuthenticationFlow = . login
101+ public var authenticationFlow : AuthenticationFlow = . signIn
99102 public var errorMessage = " "
100103 public let passwordPrompt : PasswordPromptCoordinator = . init( )
101104
@@ -137,30 +140,14 @@ public final class AuthService {
137140
138141 // MARK: - Provider APIs
139142
140- private var unsafeGoogleProvider : ( any GoogleProviderAuthUIProtocol ) ?
141- private var unsafeFacebookProvider : ( any FacebookProviderAuthUIProtocol ) ?
142- private var unsafePhoneAuthProvider : ( any PhoneAuthProviderAuthUIProtocol ) ?
143-
144143 private var listenerManager : AuthListenerManager ?
145144 public var signedInCredential : AuthCredential ?
146145
147146 var emailSignInEnabled = false
148147
149- private var providers : [ ExternalAuthProvider ] = [ ]
150- public func register( provider: ExternalAuthProvider ) {
151- switch provider {
152- case let google as GoogleProviderAuthUIProtocol :
153- unsafeGoogleProvider = google
154- providers. append ( provider)
155- case let facebook as FacebookProviderAuthUIProtocol :
156- unsafeFacebookProvider = facebook
157- providers. append ( provider)
158- case let phone as PhoneAuthProviderAuthUIProtocol :
159- unsafePhoneAuthProvider = phone
160- providers. append ( provider)
161- default :
162- break
163- }
148+ private var providers : [ AuthProviderUI ] = [ ]
149+ public func registerProvider( provider: AuthProviderUI ) {
150+ providers. append ( provider)
164151 }
165152
166153 public func renderButtons( spacing: CGFloat = 16 ) -> AnyView {
@@ -173,31 +160,10 @@ public final class AuthService {
173160 )
174161 }
175162
176- private var googleProvider : any GoogleProviderAuthUIProtocol {
177- get throws {
178- guard let provider = unsafeGoogleProvider else {
179- fatalError ( " `GoogleProviderAuthUI` has not been configured " )
180- }
181- return provider
182- }
183- }
184-
185- private var facebookProvider : any FacebookProviderAuthUIProtocol {
186- get throws {
187- guard let provider = unsafeFacebookProvider else {
188- fatalError ( " `FacebookProviderAuthUI` has not been configured " )
189- }
190- return provider
191- }
192- }
193-
194- private var phoneAuthProvider : any PhoneAuthProviderAuthUIProtocol {
195- get throws {
196- guard let provider = unsafePhoneAuthProvider else {
197- fatalError ( " `PhoneAuthProviderAuthUI` has not been configured " )
198- }
199- return provider
200- }
163+ public func signIn( _ provider: AuthProviderSwift ) async throws -> SignInOutcome {
164+ let credential = try await provider. createAuthCredential ( )
165+ let result = try await signIn ( credentials: credential)
166+ return result
201167 }
202168
203169 // MARK: - End Provider APIs
@@ -256,12 +222,14 @@ public final class AuthService {
256222 }
257223 }
258224
259- public func handleAutoUpgradeAnonymousUser( credentials: AuthCredential ) async throws {
225+ public func handleAutoUpgradeAnonymousUser( credentials: AuthCredential ) async throws -> SignInOutcome {
260226 if currentUser == nil {
261227 throw AuthServiceError . noCurrentUser
262228 }
263229 do {
264- try await currentUser? . link ( with: credentials)
230+ let result = try await currentUser? . link ( with: credentials)
231+ updateAuthenticationState ( )
232+ return . signedIn( result)
265233 } catch let error as NSError {
266234 if error. code == AuthErrorCode . emailAlreadyInUse. rawValue {
267235 let context = AccountMergeConflictContext (
@@ -276,16 +244,17 @@ public final class AuthService {
276244 }
277245 }
278246
279- public func signIn( credentials: AuthCredential ) async throws {
247+ public func signIn( credentials: AuthCredential ) async throws -> SignInOutcome {
280248 authenticationState = . authenticating
281249 do {
282250 if shouldHandleAnonymousUpgrade {
283- try await handleAutoUpgradeAnonymousUser ( credentials: credentials)
251+ return try await handleAutoUpgradeAnonymousUser ( credentials: credentials)
284252 } else {
285253 let result = try await auth. signIn ( with: credentials)
286254 signedInCredential = result. credential ?? credentials
255+ updateAuthenticationState ( )
256+ return . signedIn( result)
287257 }
288- updateAuthenticationState ( )
289258 } catch {
290259 authenticationState = . unauthenticated
291260 errorMessage = string. localizedErrorMessage (
@@ -295,7 +264,7 @@ public final class AuthService {
295264 }
296265 }
297266
298- func sendEmailVerification( ) async throws {
267+ public func sendEmailVerification( ) async throws {
299268 do {
300269 if let user = currentUser {
301270 // Requires running on MainActor as passing to sendEmailVerification() which is non-isolated
@@ -327,13 +296,16 @@ public extension AuthService {
327296 if providerId == EmailAuthProviderID {
328297 let operation = EmailPasswordDeleteUserOperation ( passwordPrompt: passwordPrompt)
329298 try await operation ( on: user)
330- } else if providerId == FacebookAuthProviderID {
331- try await facebookProvider. deleteUser ( user: user)
332- } else if providerId == GoogleAuthProviderID {
333- try await googleProvider. deleteUser ( user: user)
299+ } else {
300+ // Find provider by matching ID and ensure it can delete users
301+ guard let matchingProvider = providers. first ( where: { $0. id == providerId } ) ,
302+ let provider = matchingProvider. provider as? DeleteUserSwift else {
303+ throw AuthServiceError . providerNotFound ( " No provider found for \( providerId) " )
304+ }
305+
306+ try await provider. deleteUser ( user: user)
334307 }
335308 }
336-
337309 } catch {
338310 errorMessage = string. localizedErrorMessage (
339311 for: error
@@ -369,23 +341,24 @@ public extension AuthService {
369341 return self
370342 }
371343
372- func signIn( withEmail email: String , password: String ) async throws {
344+ func signIn( email: String , password: String ) async throws -> SignInOutcome {
373345 let credential = EmailAuthProvider . credential ( withEmail: email, password: password)
374- try await signIn ( credentials: credential)
346+ return try await signIn ( credentials: credential)
375347 }
376348
377- func createUser( withEmail email: String , password: String ) async throws {
349+ func createUser( email email: String , password: String ) async throws -> SignInOutcome {
378350 authenticationState = . authenticating
379351
380352 do {
381353 if shouldHandleAnonymousUpgrade {
382354 let credential = EmailAuthProvider . credential ( withEmail: email, password: password)
383- try await handleAutoUpgradeAnonymousUser ( credentials: credential)
355+ return try await handleAutoUpgradeAnonymousUser ( credentials: credential)
384356 } else {
385357 let result = try await auth. createUser ( withEmail: email, password: password)
386358 signedInCredential = result. credential
359+ updateAuthenticationState ( )
360+ return . signedIn( result)
387361 }
388- updateAuthenticationState ( )
389362 } catch {
390363 authenticationState = . unauthenticated
391364 errorMessage = string. localizedErrorMessage (
@@ -395,7 +368,7 @@ public extension AuthService {
395368 }
396369 }
397370
398- func sendPasswordRecoveryEmail( to email: String ) async throws {
371+ func sendPasswordRecoveryEmail( email: String ) async throws {
399372 do {
400373 try await auth. sendPasswordReset ( withEmail: email)
401374 } catch {
@@ -410,7 +383,7 @@ public extension AuthService {
410383// MARK: - Email Link Sign In
411384
412385public extension AuthService {
413- func sendEmailSignInLink( to email: String ) async throws {
386+ func sendEmailSignInLink( email: String ) async throws {
414387 do {
415388 let actionCodeSettings = try updateActionCodeSettings ( )
416389 try await auth. sendSignInLink (
@@ -488,49 +461,60 @@ public extension AuthService {
488461 }
489462}
490463
491- // MARK: - Google Sign In
464+
465+ // MARK: - Phone Auth Sign In
492466
493467public extension AuthService {
494- func signInWithGoogle( ) async throws {
495- guard let clientID = auth. app? . options. clientID else {
496- throw AuthServiceError
497- . clientIdNotFound (
498- " OAuth client ID not found. Please make sure Google Sign-In is enabled in the Firebase console. You may have to download a new GoogleService-Info.plist file after enabling Google Sign-In. "
499- )
468+ func verifyPhoneNumber( phoneNumber: String ) async throws -> String {
469+ return try await withCheckedThrowingContinuation { continuation in
470+ PhoneAuthProvider . provider ( )
471+ . verifyPhoneNumber ( phoneNumber, uiDelegate: nil ) { verificationID, error in
472+ if let error = error {
473+ continuation. resume ( throwing: error)
474+ return
475+ }
476+ continuation. resume ( returning: verificationID!)
477+ }
500478 }
501- let credential = try await googleProvider. signInWithGoogle ( clientID: clientID)
502-
503- try await signIn ( credentials: credential)
504479 }
505- }
506480
507- // MARK: - Facebook Sign In
508-
509- public extension AuthService {
510- func signInWithFacebook( limitedLogin: Bool = true ) async throws {
511- let credential = try await facebookProvider
512- . signInWithFacebook ( isLimitedLogin: limitedLogin)
481+ func signInWithPhoneNumber( verificationID: String , verificationCode: String ) async throws {
482+ let credential = PhoneAuthProvider . provider ( )
483+ . credential ( withVerificationID: verificationID, verificationCode: verificationCode)
513484 try await signIn ( credentials: credential)
514485 }
515486}
516487
517- // MARK: - Phone Auth Sign In
488+ // MARK: - User Profile Management
518489
519490public extension AuthService {
520- func verifyPhoneNumber( phoneNumber: String ) async throws -> String {
491+ func updateUserPhotoURL( url: URL ) async throws {
492+ guard let user = currentUser else {
493+ throw AuthServiceError . noCurrentUser
494+ }
495+
521496 do {
522- return try await phoneAuthProvider. verifyPhoneNumber ( phoneNumber: phoneNumber)
497+ let changeRequest = user. createProfileChangeRequest ( )
498+ changeRequest. photoURL = url
499+ try await changeRequest. commitChanges ( )
523500 } catch {
524- errorMessage = string. localizedErrorMessage (
525- for: error
526- )
501+ errorMessage = string. localizedErrorMessage ( for: error)
527502 throw error
528503 }
529504 }
530-
531- func signInWithPhoneNumber( verificationID: String , verificationCode: String ) async throws {
532- let credential = PhoneAuthProvider . provider ( )
533- . credential ( withVerificationID: verificationID, verificationCode: verificationCode)
534- try await signIn ( credentials: credential)
505+
506+ func updateUserDisplayName( name: String ) async throws {
507+ guard let user = currentUser else {
508+ throw AuthServiceError . noCurrentUser
509+ }
510+
511+ do {
512+ let changeRequest = user. createProfileChangeRequest ( )
513+ changeRequest. displayName = name
514+ try await changeRequest. commitChanges ( )
515+ } catch {
516+ errorMessage = string. localizedErrorMessage ( for: error)
517+ throw error
518+ }
535519 }
536- }
520+ }
0 commit comments