Skip to content

Commit 9e58282

Browse files
Merge pull request #1272 from firebase/refactor-api
2 parents a863e58 + c7c3e4d commit 9e58282

18 files changed

+155
-142
lines changed

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public enum AuthServiceError: LocalizedError {
3636
case invalidCredentials(String)
3737
case signInFailed(underlying: Error)
3838
case accountMergeConflict(context: AccountMergeConflictContext)
39+
case invalidPhoneAuthenticationArguments(String)
40+
case providerNotFound(String)
3941

4042
public var errorDescription: String? {
4143
switch self {
@@ -55,6 +57,10 @@ public enum AuthServiceError: LocalizedError {
5557
return "Failed to sign in: \(error.localizedDescription)"
5658
case let .accountMergeConflict(context):
5759
return context.errorDescription
60+
case let .providerNotFound(description):
61+
return description
62+
case let .invalidPhoneAuthenticationArguments(description):
63+
return description
5864
}
5965
}
6066
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 86 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,21 @@
1515
@preconcurrency import FirebaseAuth
1616
import 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

4342
public 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
5659
private 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

412385
public 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

493467
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+
}
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

519490
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+
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+
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public struct AuthPickerView {
2323

2424
private func switchFlow() {
2525
authService.authenticationFlow = authService
26-
.authenticationFlow == .login ? .signUp : .login
26+
.authenticationFlow == .signIn ? .signUp : .signIn
2727
}
2828

2929
private var isAuthModalPresented: Binding<Bool> {
@@ -59,7 +59,7 @@ extension AuthPickerView: View {
5959
EmailLinkView()
6060
case .authPicker:
6161
if authService.emailSignInEnabled {
62-
Text(authService.authenticationFlow == .login ? authService.string
62+
Text(authService.authenticationFlow == .signIn ? authService.string
6363
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
6464
Divider()
6565
EmailAuthView()
@@ -71,7 +71,7 @@ extension AuthPickerView: View {
7171
Divider()
7272
HStack {
7373
Text(authService
74-
.authenticationFlow == .login ? authService.string.dontHaveAnAccountYetLabel :
74+
.authenticationFlow == .signIn ? authService.string.dontHaveAnAccountYetLabel :
7575
authService.string.alreadyHaveAnAccountLabel)
7676
Button(action: {
7777
withAnimation {

0 commit comments

Comments
 (0)