15
15
@preconcurrency import FirebaseAuth
16
16
import SwiftUI
17
17
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
21
20
}
22
21
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 }
26
26
}
27
27
28
- public protocol FacebookProviderAuthUIProtocol : ExternalAuthProvider {
29
- @MainActor func signInWithFacebook( isLimitedLogin: Bool ) async throws -> AuthCredential
28
+ public protocol DeleteUserSwift {
30
29
@MainActor func deleteUser( user: User ) async throws
31
30
}
32
31
33
- public protocol PhoneAuthProviderAuthUIProtocol : ExternalAuthProvider {
32
+ public protocol PhoneAuthProviderAuthUIProtocol : AuthProviderSwift {
34
33
@MainActor func verifyPhoneNumber( phoneNumber: String ) async throws -> String
35
34
}
36
35
@@ -41,7 +40,7 @@ public enum AuthenticationState {
41
40
}
42
41
43
42
public enum AuthenticationFlow {
44
- case login
43
+ case signIn
45
44
case signUp
46
45
}
47
46
@@ -52,6 +51,10 @@ public enum AuthView {
52
51
case updatePassword
53
52
}
54
53
54
+ public enum SignInOutcome : @unchecked Sendable {
55
+ case signedIn( AuthDataResult ? )
56
+ }
57
+
55
58
@MainActor
56
59
private final class AuthListenerManager {
57
60
private var authStateHandle : AuthStateDidChangeListenerHandle ?
@@ -95,7 +98,7 @@ public final class AuthService {
95
98
public let string : StringUtils
96
99
public var currentUser : User ?
97
100
public var authenticationState : AuthenticationState = . unauthenticated
98
- public var authenticationFlow : AuthenticationFlow = . login
101
+ public var authenticationFlow : AuthenticationFlow = . signIn
99
102
public var errorMessage = " "
100
103
public let passwordPrompt : PasswordPromptCoordinator = . init( )
101
104
@@ -137,30 +140,14 @@ public final class AuthService {
137
140
138
141
// MARK: - Provider APIs
139
142
140
- private var unsafeGoogleProvider : ( any GoogleProviderAuthUIProtocol ) ?
141
- private var unsafeFacebookProvider : ( any FacebookProviderAuthUIProtocol ) ?
142
- private var unsafePhoneAuthProvider : ( any PhoneAuthProviderAuthUIProtocol ) ?
143
-
144
143
private var listenerManager : AuthListenerManager ?
145
144
public var signedInCredential : AuthCredential ?
146
145
147
146
var emailSignInEnabled = false
148
147
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)
164
151
}
165
152
166
153
public func renderButtons( spacing: CGFloat = 16 ) -> AnyView {
@@ -173,31 +160,10 @@ public final class AuthService {
173
160
)
174
161
}
175
162
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
201
167
}
202
168
203
169
// MARK: - End Provider APIs
@@ -256,12 +222,14 @@ public final class AuthService {
256
222
}
257
223
}
258
224
259
- public func handleAutoUpgradeAnonymousUser( credentials: AuthCredential ) async throws {
225
+ public func handleAutoUpgradeAnonymousUser( credentials: AuthCredential ) async throws -> SignInOutcome {
260
226
if currentUser == nil {
261
227
throw AuthServiceError . noCurrentUser
262
228
}
263
229
do {
264
- try await currentUser? . link ( with: credentials)
230
+ let result = try await currentUser? . link ( with: credentials)
231
+ updateAuthenticationState ( )
232
+ return . signedIn( result)
265
233
} catch let error as NSError {
266
234
if error. code == AuthErrorCode . emailAlreadyInUse. rawValue {
267
235
let context = AccountMergeConflictContext (
@@ -276,16 +244,17 @@ public final class AuthService {
276
244
}
277
245
}
278
246
279
- public func signIn( credentials: AuthCredential ) async throws {
247
+ public func signIn( credentials: AuthCredential ) async throws -> SignInOutcome {
280
248
authenticationState = . authenticating
281
249
do {
282
250
if shouldHandleAnonymousUpgrade {
283
- try await handleAutoUpgradeAnonymousUser ( credentials: credentials)
251
+ return try await handleAutoUpgradeAnonymousUser ( credentials: credentials)
284
252
} else {
285
253
let result = try await auth. signIn ( with: credentials)
286
254
signedInCredential = result. credential ?? credentials
255
+ updateAuthenticationState ( )
256
+ return . signedIn( result)
287
257
}
288
- updateAuthenticationState ( )
289
258
} catch {
290
259
authenticationState = . unauthenticated
291
260
errorMessage = string. localizedErrorMessage (
@@ -295,7 +264,7 @@ public final class AuthService {
295
264
}
296
265
}
297
266
298
- func sendEmailVerification( ) async throws {
267
+ public func sendEmailVerification( ) async throws {
299
268
do {
300
269
if let user = currentUser {
301
270
// Requires running on MainActor as passing to sendEmailVerification() which is non-isolated
@@ -327,13 +296,16 @@ public extension AuthService {
327
296
if providerId == EmailAuthProviderID {
328
297
let operation = EmailPasswordDeleteUserOperation ( passwordPrompt: passwordPrompt)
329
298
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)
334
307
}
335
308
}
336
-
337
309
} catch {
338
310
errorMessage = string. localizedErrorMessage (
339
311
for: error
@@ -369,23 +341,24 @@ public extension AuthService {
369
341
return self
370
342
}
371
343
372
- func signIn( withEmail email: String , password: String ) async throws {
344
+ func signIn( email: String , password: String ) async throws -> SignInOutcome {
373
345
let credential = EmailAuthProvider . credential ( withEmail: email, password: password)
374
- try await signIn ( credentials: credential)
346
+ return try await signIn ( credentials: credential)
375
347
}
376
348
377
- func createUser( withEmail email: String , password: String ) async throws {
349
+ func createUser( email email: String , password: String ) async throws -> SignInOutcome {
378
350
authenticationState = . authenticating
379
351
380
352
do {
381
353
if shouldHandleAnonymousUpgrade {
382
354
let credential = EmailAuthProvider . credential ( withEmail: email, password: password)
383
- try await handleAutoUpgradeAnonymousUser ( credentials: credential)
355
+ return try await handleAutoUpgradeAnonymousUser ( credentials: credential)
384
356
} else {
385
357
let result = try await auth. createUser ( withEmail: email, password: password)
386
358
signedInCredential = result. credential
359
+ updateAuthenticationState ( )
360
+ return . signedIn( result)
387
361
}
388
- updateAuthenticationState ( )
389
362
} catch {
390
363
authenticationState = . unauthenticated
391
364
errorMessage = string. localizedErrorMessage (
@@ -395,7 +368,7 @@ public extension AuthService {
395
368
}
396
369
}
397
370
398
- func sendPasswordRecoveryEmail( to email: String ) async throws {
371
+ func sendPasswordRecoveryEmail( email: String ) async throws {
399
372
do {
400
373
try await auth. sendPasswordReset ( withEmail: email)
401
374
} catch {
@@ -410,7 +383,7 @@ public extension AuthService {
410
383
// MARK: - Email Link Sign In
411
384
412
385
public extension AuthService {
413
- func sendEmailSignInLink( to email: String ) async throws {
386
+ func sendEmailSignInLink( email: String ) async throws {
414
387
do {
415
388
let actionCodeSettings = try updateActionCodeSettings ( )
416
389
try await auth. sendSignInLink (
@@ -488,49 +461,60 @@ public extension AuthService {
488
461
}
489
462
}
490
463
491
- // MARK: - Google Sign In
464
+
465
+ // MARK: - Phone Auth Sign In
492
466
493
467
public 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
+ }
500
478
}
501
- let credential = try await googleProvider. signInWithGoogle ( clientID: clientID)
502
-
503
- try await signIn ( credentials: credential)
504
479
}
505
- }
506
480
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)
513
484
try await signIn ( credentials: credential)
514
485
}
515
486
}
516
487
517
- // MARK: - Phone Auth Sign In
488
+ // MARK: - User Profile Management
518
489
519
490
public 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
+
521
496
do {
522
- return try await phoneAuthProvider. verifyPhoneNumber ( phoneNumber: phoneNumber)
497
+ let changeRequest = user. createProfileChangeRequest ( )
498
+ changeRequest. photoURL = url
499
+ try await changeRequest. commitChanges ( )
523
500
} catch {
524
- errorMessage = string. localizedErrorMessage (
525
- for: error
526
- )
501
+ errorMessage = string. localizedErrorMessage ( for: error)
527
502
throw error
528
503
}
529
504
}
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
+ }
535
519
}
536
- }
520
+ }
0 commit comments