From 855b96d9fdb1c60078825c56dd159aca92fa122f Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 2 Oct 2025 16:46:55 +0100 Subject: [PATCH 01/13] refactor: implementations as described in API doc --- .../Sources/AuthServiceError.swift | 3 + .../Sources/Services/AuthService.swift | 121 +++++------------- .../Services/AuthService+Facebook.swift | 2 +- .../Services/FacebookProviderAuthUI.swift | 8 +- .../Views/SignInWithFacebookButton.swift | 3 +- .../Sources/Services/AuthService+Google.swift | 2 +- .../Services/GoogleProviderAuthUI.swift | 9 +- .../Views/SignInWithGoogleButton.swift | 6 +- .../Sources/Services/AuthService+Phone.swift | 2 +- .../Services/PhoneAuthProviderAuthUI.swift | 19 ++- 10 files changed, 77 insertions(+), 98 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift index badbc98519..b30f8d3912 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift @@ -36,6 +36,7 @@ public enum AuthServiceError: LocalizedError { case invalidCredentials(String) case signInFailed(underlying: Error) case accountMergeConflict(context: AccountMergeConflictContext) + case providerNotFound(String) public var errorDescription: String? { switch self { @@ -55,6 +56,8 @@ public enum AuthServiceError: LocalizedError { return "Failed to sign in: \(error.localizedDescription)" case let .accountMergeConflict(context): return context.errorDescription + case let .providerNotFound(description): + return description } } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index b124e69b29..e1551a2c28 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -15,22 +15,21 @@ @preconcurrency import FirebaseAuth import SwiftUI -public protocol ExternalAuthProvider { - var id: String { get } - @MainActor func authButton() -> AnyView +public protocol AuthProviderSwift { + @MainActor func createAuthCredential() async throws -> AuthCredential } -public protocol GoogleProviderAuthUIProtocol: ExternalAuthProvider { - @MainActor func signInWithGoogle(clientID: String) async throws -> AuthCredential - @MainActor func deleteUser(user: User) async throws +public protocol AuthProviderUI { + var id: String { get } + @MainActor func authButton() -> AnyView + var provider: AuthProviderSwift { get } } -public protocol FacebookProviderAuthUIProtocol: ExternalAuthProvider { - @MainActor func signInWithFacebook(isLimitedLogin: Bool) async throws -> AuthCredential +public protocol DeleteUserSwift { @MainActor func deleteUser(user: User) async throws } -public protocol PhoneAuthProviderAuthUIProtocol: ExternalAuthProvider { +public protocol PhoneAuthProviderAuthUIProtocol: AuthProviderSwift { @MainActor func verifyPhoneNumber(phoneNumber: String) async throws -> String } @@ -137,30 +136,14 @@ public final class AuthService { // MARK: - Provider APIs - private var unsafeGoogleProvider: (any GoogleProviderAuthUIProtocol)? - private var unsafeFacebookProvider: (any FacebookProviderAuthUIProtocol)? - private var unsafePhoneAuthProvider: (any PhoneAuthProviderAuthUIProtocol)? - private var listenerManager: AuthListenerManager? public var signedInCredential: AuthCredential? var emailSignInEnabled = false - private var providers: [ExternalAuthProvider] = [] - public func register(provider: ExternalAuthProvider) { - switch provider { - case let google as GoogleProviderAuthUIProtocol: - unsafeGoogleProvider = google - providers.append(provider) - case let facebook as FacebookProviderAuthUIProtocol: - unsafeFacebookProvider = facebook - providers.append(provider) - case let phone as PhoneAuthProviderAuthUIProtocol: - unsafePhoneAuthProvider = phone - providers.append(provider) - default: - break - } + private var providers: [AuthProviderUI] = [] + public func registerProvider(provider: AuthProviderUI) { + providers.append(provider) } public func renderButtons(spacing: CGFloat = 16) -> AnyView { @@ -173,31 +156,9 @@ public final class AuthService { ) } - private var googleProvider: any GoogleProviderAuthUIProtocol { - get throws { - guard let provider = unsafeGoogleProvider else { - fatalError("`GoogleProviderAuthUI` has not been configured") - } - return provider - } - } - - private var facebookProvider: any FacebookProviderAuthUIProtocol { - get throws { - guard let provider = unsafeFacebookProvider else { - fatalError("`FacebookProviderAuthUI` has not been configured") - } - return provider - } - } - - private var phoneAuthProvider: any PhoneAuthProviderAuthUIProtocol { - get throws { - guard let provider = unsafePhoneAuthProvider else { - fatalError("`PhoneAuthProviderAuthUI` has not been configured") - } - return provider - } + public func signInWithProvider(_ provider: AuthProviderSwift) async throws { + let credential = try await provider.createAuthCredential() + try await signIn(credentials: credential) } // MARK: - End Provider APIs @@ -327,13 +288,16 @@ public extension AuthService { if providerId == EmailAuthProviderID { let operation = EmailPasswordDeleteUserOperation(passwordPrompt: passwordPrompt) try await operation(on: user) - } else if providerId == FacebookAuthProviderID { - try await facebookProvider.deleteUser(user: user) - } else if providerId == GoogleAuthProviderID { - try await googleProvider.deleteUser(user: user) + } else { + // Find provider by matching ID and ensure it can delete users + guard let matchingProvider = providers.first(where: { $0.id == providerId }), + let provider = matchingProvider.provider as? DeleteUserSwift else { + throw AuthServiceError.providerNotFound("No provider found for \(providerId)") + } + + try await provider.deleteUser(user: user) } } - } catch { errorMessage = string.localizedErrorMessage( for: error @@ -488,43 +452,20 @@ public extension AuthService { } } -// MARK: - Google Sign In - -public extension AuthService { - func signInWithGoogle() async throws { - guard let clientID = auth.app?.options.clientID else { - throw AuthServiceError - .clientIdNotFound( - "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." - ) - } - let credential = try await googleProvider.signInWithGoogle(clientID: clientID) - - try await signIn(credentials: credential) - } -} - -// MARK: - Facebook Sign In - -public extension AuthService { - func signInWithFacebook(limitedLogin: Bool = true) async throws { - let credential = try await facebookProvider - .signInWithFacebook(isLimitedLogin: limitedLogin) - try await signIn(credentials: credential) - } -} // MARK: - Phone Auth Sign In public extension AuthService { func verifyPhoneNumber(phoneNumber: String) async throws -> String { - do { - return try await phoneAuthProvider.verifyPhoneNumber(phoneNumber: phoneNumber) - } catch { - errorMessage = string.localizedErrorMessage( - for: error - ) - throw error + return try await withCheckedThrowingContinuation { continuation in + PhoneAuthProvider.provider() + .verifyPhoneNumber(phoneNumber, uiDelegate: nil) { verificationID, error in + if let error = error { + continuation.resume(throwing: error) + return + } + continuation.resume(returning: verificationID!) + } } } diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AuthService+Facebook.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AuthService+Facebook.swift index 5aa8481340..a37f1bdfed 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AuthService+Facebook.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AuthService+Facebook.swift @@ -25,7 +25,7 @@ public extension AuthService { @discardableResult func withFacebookSignIn(scopes scopes: [String]? = nil) -> AuthService { FacebookProviderAuthUI.configureProvider(scopes: scopes) - register(provider: FacebookProviderAuthUI.shared) + registerProvider(provider: FacebookProviderAuthUI.shared) return self } } diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift index 2501e50610..6e8aab55dd 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift @@ -31,7 +31,7 @@ public enum FacebookProviderError: Error { case authenticationToken(String) } -public class FacebookProviderAuthUI: FacebookProviderAuthUIProtocol { +public class FacebookProviderAuthUI: AuthProviderSwift, AuthProviderUI, DeleteUserSwift { public let id: String = "facebook" let scopes: [String] let shortName = "Facebook" @@ -41,6 +41,8 @@ public class FacebookProviderAuthUI: FacebookProviderAuthUIProtocol { private var shaNonce: String? // Needed for reauthentication var isLimitedLogin: Bool = true + + public var provider: AuthProviderSwift { self } @MainActor private static var _shared: FacebookProviderAuthUI = .init(scopes: kDefaultFacebookScopes) @@ -66,6 +68,10 @@ public class FacebookProviderAuthUI: FacebookProviderAuthUIProtocol { try await operation(on: user) } + @MainActor public func createAuthCredential() async throws -> AuthCredential { + return try await signInWithFacebook(isLimitedLogin: true) + } + @MainActor public func signInWithFacebook(isLimitedLogin: Bool) async throws -> AuthCredential { let loginType: LoginTracking = isLimitedLogin ? .limited : .enabled self.isLimitedLogin = isLimitedLogin diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift index 6ddfc1d681..24a9c3ad28 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift @@ -65,7 +65,8 @@ extension SignInWithFacebookButton: View { Button(action: { Task { do { - try await authService.signInWithFacebook(limitedLogin: limitedLogin) + let credential = try await FacebookProviderAuthUI.shared.signInWithFacebook(isLimitedLogin: limitedLogin) + try await authService.signIn(credentials: credential) } catch { switch error { case FacebookProviderError.signInCancelled: diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AuthService+Google.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AuthService+Google.swift index 2e51b84785..ddcc1a2223 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AuthService+Google.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AuthService+Google.swift @@ -25,7 +25,7 @@ public extension AuthService { @discardableResult func withGoogleSignIn(scopes scopes: [String]? = nil) -> AuthService { let clientID = auth.app?.options.clientID ?? "" - register(provider: GoogleProviderAuthUI(scopes: scopes, clientID: clientID)) + registerProvider(provider: GoogleProviderAuthUI(scopes: scopes, clientID: clientID)) return self } } diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift index 236e66289f..4bf15ae721 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift @@ -29,12 +29,15 @@ public enum GoogleProviderError: Error { case user(String) } -public class GoogleProviderAuthUI: @preconcurrency GoogleProviderAuthUIProtocol { +public class GoogleProviderAuthUI: AuthProviderSwift, AuthProviderUI, DeleteUserSwift { public let id: String = "google" let scopes: [String] let shortName = "Google" let providerId = "google.com" public let clientID: String + + public var provider: AuthProviderSwift { self } + public init(scopes: [String]? = nil, clientID: String = FirebaseApp.app()!.options.clientID!) { self.scopes = scopes ?? kDefaultScopes self.clientID = clientID @@ -50,6 +53,10 @@ public class GoogleProviderAuthUI: @preconcurrency GoogleProviderAuthUIProtocol try await operation(on: user) } + @MainActor public func createAuthCredential() async throws -> AuthCredential { + return try await signInWithGoogle(clientID: clientID) + } + @MainActor public func signInWithGoogle(clientID: String) async throws -> AuthCredential { guard let presentingViewController = await (UIApplication.shared.connectedScenes .first as? UIWindowScene)?.windows.first?.rootViewController else { diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift index 634f33cbdc..b8debc47b4 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift @@ -38,7 +38,11 @@ extension SignInWithGoogleButton: View { public var body: some View { GoogleSignInButton(viewModel: customViewModel) { Task { - try await authService.signInWithGoogle() + guard let clientID = authService.auth.app?.options.clientID else { + return + } + let provider = GoogleProviderAuthUI(clientID: clientID) + try await authService.signInWithProvider(provider) } } } diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/AuthService+Phone.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/AuthService+Phone.swift index 462eb9a96f..079381d7f6 100644 --- a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/AuthService+Phone.swift +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/AuthService+Phone.swift @@ -24,7 +24,7 @@ import FirebaseAuthSwiftUI public extension AuthService { @discardableResult func withPhoneSignIn() -> AuthService { - register(provider: PhoneAuthProviderAuthUI()) + registerProvider(provider: PhoneAuthProviderAuthUI()) return self } } diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift index 13bf1c7956..13a277694b 100644 --- a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift @@ -18,12 +18,29 @@ import SwiftUI public typealias VerificationID = String -public class PhoneAuthProviderAuthUI: @preconcurrency PhoneAuthProviderAuthUIProtocol { +public class PhoneAuthProviderAuthUI: @preconcurrency PhoneAuthProviderAuthUIProtocol, AuthProviderUI { public let id: String = "phone" + + public var provider: AuthProviderSwift { self } + + // Store verification details for the signIn method + private var storedVerificationID: String? + private var storedVerificationCode: String? @MainActor public func authButton() -> AnyView { AnyView(PhoneAuthButtonView()) } + + @MainActor public func createAuthCredential() async throws -> AuthCredential { + guard let verificationID = storedVerificationID, + let verificationCode = storedVerificationCode else { + fatalError("Phone authentication requires verifyPhoneNumber to be called first") + } + return PhoneAuthProvider.provider().credential( + withVerificationID: verificationID, + verificationCode: verificationCode + ) + } @MainActor public func verifyPhoneNumber(phoneNumber: String) async throws -> VerificationID { return try await withCheckedThrowingContinuation { continuation in From 8c15371e13e7c95b92ff58238df0ca76602ae617 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 2 Oct 2025 17:00:03 +0100 Subject: [PATCH 02/13] refactor: Facebook provider --- .../Services/AccountService+Facebook.swift | 3 +-- .../Sources/Services/FacebookProviderAuthUI.swift | 15 ++++++--------- .../Sources/Views/SignInWithFacebookButton.swift | 4 ++-- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AccountService+Facebook.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AccountService+Facebook.swift index 54f655dff2..45493b3e69 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AccountService+Facebook.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AccountService+Facebook.swift @@ -34,8 +34,7 @@ extension FacebookOperationReauthentication { } do { - let credential = try await facebookProvider - .signInWithFacebook(isLimitedLogin: facebookProvider.isLimitedLogin) + let credential = try await facebookProvider.createAuthCredential() try await user.reauthenticate(with: credential) return .firebase("") diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift index 6e8aab55dd..76c8e2133e 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift @@ -44,19 +44,19 @@ public class FacebookProviderAuthUI: AuthProviderSwift, AuthProviderUI, DeleteUs public var provider: AuthProviderSwift { self } - @MainActor private static var _shared: FacebookProviderAuthUI = - .init(scopes: kDefaultFacebookScopes) + @MainActor private static var _shared: FacebookProviderAuthUI = FacebookProviderAuthUI(scopes: kDefaultFacebookScopes) @MainActor public static var shared: FacebookProviderAuthUI { return _shared } - @MainActor public static func configureProvider(scopes: [String]? = nil) { - _shared = FacebookProviderAuthUI(scopes: scopes) + @MainActor public static func configureProvider(scopes: [String]? = nil, isLimitedLogin: Bool = true) { + _shared = FacebookProviderAuthUI(scopes: scopes, isLimitedLogin: isLimitedLogin) } - private init(scopes: [String]? = nil) { + public init(scopes: [String]? = nil, isLimitedLogin: Bool = true) { self.scopes = scopes ?? kDefaultFacebookScopes + self.isLimitedLogin = isLimitedLogin } @MainActor public func authButton() -> AnyView { @@ -68,11 +68,8 @@ public class FacebookProviderAuthUI: AuthProviderSwift, AuthProviderUI, DeleteUs try await operation(on: user) } - @MainActor public func createAuthCredential() async throws -> AuthCredential { - return try await signInWithFacebook(isLimitedLogin: true) - } - @MainActor public func signInWithFacebook(isLimitedLogin: Bool) async throws -> AuthCredential { + @MainActor public func createAuthCredential() async throws -> AuthCredential { let loginType: LoginTracking = isLimitedLogin ? .limited : .enabled self.isLimitedLogin = isLimitedLogin diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift index 24a9c3ad28..88e81cb6f4 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift @@ -65,8 +65,8 @@ extension SignInWithFacebookButton: View { Button(action: { Task { do { - let credential = try await FacebookProviderAuthUI.shared.signInWithFacebook(isLimitedLogin: limitedLogin) - try await authService.signIn(credentials: credential) + let facebookProvider = FacebookProviderAuthUI(isLimitedLogin: limitedLogin) + try await authService.signInWithProvider(facebookProvider) } catch { switch error { case FacebookProviderError.signInCancelled: From bbce389ef63969913842f2703ead9f426a6ef6fa Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 2 Oct 2025 17:04:51 +0100 Subject: [PATCH 03/13] refactor: google provider --- .../Sources/Services/AccountService+Google.swift | 3 +-- .../Sources/Services/GoogleProviderAuthUI.swift | 7 +------ .../Sources/Views/SignInWithGoogleButton.swift | 7 ++----- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AccountService+Google.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AccountService+Google.swift index f51d1501c4..176dd36d37 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AccountService+Google.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AccountService+Google.swift @@ -41,8 +41,7 @@ extension GoogleOperationReauthentication { } do { - let credential = try await googleProvider - .signInWithGoogle(clientID: googleProvider.clientID) + let credential = try await googleProvider.createAuthCredential() try await user.reauthenticate(with: credential) return .firebase("") diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift index 4bf15ae721..34a2b37199 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift @@ -44,7 +44,6 @@ public class GoogleProviderAuthUI: AuthProviderSwift, AuthProviderUI, DeleteUser } @MainActor public func authButton() -> AnyView { - // Moved to SignInWithGoogleButton so we could sign in via AuthService AnyView(SignInWithGoogleButton()) } @@ -54,10 +53,6 @@ public class GoogleProviderAuthUI: AuthProviderSwift, AuthProviderUI, DeleteUser } @MainActor public func createAuthCredential() async throws -> AuthCredential { - return try await signInWithGoogle(clientID: clientID) - } - - @MainActor public func signInWithGoogle(clientID: String) async throws -> AuthCredential { guard let presentingViewController = await (UIApplication.shared.connectedScenes .first as? UIWindowScene)?.windows.first?.rootViewController else { throw GoogleProviderError @@ -66,7 +61,7 @@ public class GoogleProviderAuthUI: AuthProviderSwift, AuthProviderUI, DeleteUser ) } - let config = GIDConfiguration(clientID: clientID) + let config = GIDConfiguration(clientID: self.clientID) GIDSignIn.sharedInstance.configuration = config return try await withCheckedThrowingContinuation { continuation in diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift index b8debc47b4..17610d0787 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift @@ -26,6 +26,7 @@ import SwiftUI @MainActor public struct SignInWithGoogleButton { @Environment(AuthService.self) private var authService + private let googleProvider = GoogleProviderAuthUI() let customViewModel = GoogleSignInButtonViewModel( scheme: .light, @@ -38,11 +39,7 @@ extension SignInWithGoogleButton: View { public var body: some View { GoogleSignInButton(viewModel: customViewModel) { Task { - guard let clientID = authService.auth.app?.options.clientID else { - return - } - let provider = GoogleProviderAuthUI(clientID: clientID) - try await authService.signInWithProvider(provider) + try await authService.signInWithProvider(googleProvider) } } } From f2609f062f13db5c8bc63cf8082d8cd9b4a2554f Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 2 Oct 2025 17:10:51 +0100 Subject: [PATCH 04/13] refactor: phone auth provider --- .../Sources/AuthServiceError.swift | 3 +++ .../Services/PhoneAuthProviderAuthUI.swift | 27 +++++++++++-------- .../Sources/Views/PhoneAuthView.swift | 6 +++-- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift index b30f8d3912..fbb5570f45 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift @@ -36,6 +36,7 @@ public enum AuthServiceError: LocalizedError { case invalidCredentials(String) case signInFailed(underlying: Error) case accountMergeConflict(context: AccountMergeConflictContext) + case invalidPhoneAuthenticationArguments(String) case providerNotFound(String) public var errorDescription: String? { @@ -58,6 +59,8 @@ public enum AuthServiceError: LocalizedError { return context.errorDescription case let .providerNotFound(description): return description + case let .invalidPhoneAuthenticationArguments(description): + return description } } } diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift index 13a277694b..97077db1ab 100644 --- a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift @@ -30,17 +30,6 @@ public class PhoneAuthProviderAuthUI: @preconcurrency PhoneAuthProviderAuthUIPro @MainActor public func authButton() -> AnyView { AnyView(PhoneAuthButtonView()) } - - @MainActor public func createAuthCredential() async throws -> AuthCredential { - guard let verificationID = storedVerificationID, - let verificationCode = storedVerificationCode else { - fatalError("Phone authentication requires verifyPhoneNumber to be called first") - } - return PhoneAuthProvider.provider().credential( - withVerificationID: verificationID, - verificationCode: verificationCode - ) - } @MainActor public func verifyPhoneNumber(phoneNumber: String) async throws -> VerificationID { return try await withCheckedThrowingContinuation { continuation in @@ -54,4 +43,20 @@ public class PhoneAuthProviderAuthUI: @preconcurrency PhoneAuthProviderAuthUIPro } } } + + // Set verification details before calling signIn + public func setVerificationDetails(verificationID: String, verificationCode: String) { + self.storedVerificationID = verificationID + self.storedVerificationCode = verificationCode + } + + @MainActor public func createAuthCredential() async throws -> AuthCredential { + guard let verificationID = storedVerificationID, + let verificationCode = storedVerificationCode else { + throw AuthServiceError.invalidPhoneAuthenticationArguments("please call setVerificationDetails() before creating Phone Auth credential") + } + + return PhoneAuthProvider.provider() + .credential(withVerificationID: verificationID, verificationCode: verificationCode) + } } diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthView.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthView.swift index 6483c3e0bb..856a8367d4 100644 --- a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthView.swift +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthView.swift @@ -31,6 +31,7 @@ public struct PhoneAuthView { @State private var showVerificationCodeInput = false @State private var verificationCode = "" @State private var verificationID = "" + private let phoneProvider = PhoneAuthProviderAuthUI() public init() {} } @@ -52,7 +53,7 @@ extension PhoneAuthView: View { Button(action: { Task { do { - let id = try await authService.verifyPhoneNumber(phoneNumber: phoneNumber) + let id = try await phoneProvider.verifyPhoneNumber(phoneNumber: phoneNumber) verificationID = id showVerificationCodeInput = true } catch { @@ -82,10 +83,11 @@ extension PhoneAuthView: View { Button(action: { Task { do { - try await authService.signInWithPhoneNumber( + phoneProvider.setVerificationDetails( verificationID: verificationID, verificationCode: verificationCode ) + try await authService.signInWithProvider(phoneProvider) } catch { errorMessage = authService.string.localizedErrorMessage(for: error) } From 6fe345a784f0141a74a3d350cd1fe1529482baeb Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 2 Oct 2025 17:19:28 +0100 Subject: [PATCH 05/13] chore: rename according to API doc --- .../FirebaseAuthSwiftUI/Sources/Services/AuthService.swift | 2 +- .../Sources/Views/SignInWithFacebookButton.swift | 2 +- .../Sources/Views/SignInWithGoogleButton.swift | 2 +- .../FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthView.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index e1551a2c28..57d07618a9 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -156,7 +156,7 @@ public final class AuthService { ) } - public func signInWithProvider(_ provider: AuthProviderSwift) async throws { + public func signIn(_ provider: AuthProviderSwift) async throws { let credential = try await provider.createAuthCredential() try await signIn(credentials: credential) } diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift index 88e81cb6f4..eaa7606c27 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift @@ -66,7 +66,7 @@ extension SignInWithFacebookButton: View { Task { do { let facebookProvider = FacebookProviderAuthUI(isLimitedLogin: limitedLogin) - try await authService.signInWithProvider(facebookProvider) + try await authService.signIn(facebookProvider) } catch { switch error { case FacebookProviderError.signInCancelled: diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift index 17610d0787..7cc5a9131f 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift @@ -39,7 +39,7 @@ extension SignInWithGoogleButton: View { public var body: some View { GoogleSignInButton(viewModel: customViewModel) { Task { - try await authService.signInWithProvider(googleProvider) + try await authService.signIn(googleProvider) } } } diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthView.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthView.swift index 856a8367d4..769b488b47 100644 --- a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthView.swift +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthView.swift @@ -87,7 +87,7 @@ extension PhoneAuthView: View { verificationID: verificationID, verificationCode: verificationCode ) - try await authService.signInWithProvider(phoneProvider) + try await authService.signIn(phoneProvider) } catch { errorMessage = authService.string.localizedErrorMessage(for: error) } From 0802dfaa16d0204377ae6f6e660ef7fcbcde2ff7 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 2 Oct 2025 17:20:56 +0100 Subject: [PATCH 06/13] refactor: rename login to signUp --- .../Sources/Services/AuthService.swift | 4 ++-- .../Sources/Views/AuthPickerView.swift | 6 +++--- .../FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index 57d07618a9..bfb030b0c6 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -40,7 +40,7 @@ public enum AuthenticationState { } public enum AuthenticationFlow { - case login + case signIn case signUp } @@ -94,7 +94,7 @@ public final class AuthService { public let string: StringUtils public var currentUser: User? public var authenticationState: AuthenticationState = .unauthenticated - public var authenticationFlow: AuthenticationFlow = .login + public var authenticationFlow: AuthenticationFlow = .signIn public var errorMessage = "" public let passwordPrompt: PasswordPromptCoordinator = .init() diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift index e48b8ddc32..805fe7bc6e 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift @@ -23,7 +23,7 @@ public struct AuthPickerView { private func switchFlow() { authService.authenticationFlow = authService - .authenticationFlow == .login ? .signUp : .login + .authenticationFlow == .signIn ? .signUp : .signIn } private var isAuthModalPresented: Binding { @@ -59,7 +59,7 @@ extension AuthPickerView: View { EmailLinkView() case .authPicker: if authService.emailSignInEnabled { - Text(authService.authenticationFlow == .login ? authService.string + Text(authService.authenticationFlow == .signIn ? authService.string .emailLoginFlowLabel : authService.string.emailSignUpFlowLabel) Divider() EmailAuthView() @@ -71,7 +71,7 @@ extension AuthPickerView: View { Divider() HStack { Text(authService - .authenticationFlow == .login ? authService.string.dontHaveAnAccountYetLabel : + .authenticationFlow == .signIn ? authService.string.dontHaveAnAccountYetLabel : authService.string.alreadyHaveAnAccountLabel) Button(action: { withAnimation { diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift index 739bbebe11..00fa3792b6 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift @@ -41,7 +41,7 @@ public struct EmailAuthView { public init() {} private var isValid: Bool { - return if authService.authenticationFlow == .login { + return if authService.authenticationFlow == .signIn { !email.isEmpty && !password.isEmpty } else { !email.isEmpty && !password.isEmpty && password == confirmPassword @@ -98,7 +98,7 @@ extension EmailAuthView: View { .padding(.bottom, 8) .accessibilityIdentifier("password-field") - if authService.authenticationFlow == .login { + if authService.authenticationFlow == .signIn { Button(action: { authService.authView = .passwordRecovery }) { @@ -127,12 +127,12 @@ extension EmailAuthView: View { Button(action: { Task { - if authService.authenticationFlow == .login { await signInWithEmailPassword() } + if authService.authenticationFlow == .signIn { await signInWithEmailPassword() } else { await createUserWithEmailPassword() } } }) { if authService.authenticationState != .authenticating { - Text(authService.authenticationFlow == .login ? authService.string + Text(authService.authenticationFlow == .signIn ? authService.string .signInWithEmailButtonLabel : authService.string.signUpWithEmailButtonLabel) .padding(.vertical, 8) .frame(maxWidth: .infinity) From 3103a2ddfd9fa7d57521365e1bb9c997b58fb8da Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 2 Oct 2025 17:30:16 +0100 Subject: [PATCH 07/13] chore: rm unneeded assignment --- .../Sources/Services/FacebookProviderAuthUI.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift index 76c8e2133e..fb459c3f6d 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift @@ -71,7 +71,6 @@ public class FacebookProviderAuthUI: AuthProviderSwift, AuthProviderUI, DeleteUs @MainActor public func createAuthCredential() async throws -> AuthCredential { let loginType: LoginTracking = isLimitedLogin ? .limited : .enabled - self.isLimitedLogin = isLimitedLogin guard let configuration: LoginConfiguration = { if loginType == .limited { From efa29b48171784172d69ecdd3836bfa5fca9b50c Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 2 Oct 2025 17:36:01 +0100 Subject: [PATCH 08/13] refactor: return `SignInOutcome` for authentication methods --- .../Sources/Services/AuthService.swift | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index bfb030b0c6..5876d2b5df 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -51,6 +51,10 @@ public enum AuthView { case updatePassword } +public enum SignInOutcome: @unchecked Sendable { + case signedIn(AuthDataResult?) +} + @MainActor private final class AuthListenerManager { private var authStateHandle: AuthStateDidChangeListenerHandle? @@ -156,9 +160,10 @@ public final class AuthService { ) } - public func signIn(_ provider: AuthProviderSwift) async throws { + public func signIn(_ provider: AuthProviderSwift) async throws -> SignInOutcome { let credential = try await provider.createAuthCredential() - try await signIn(credentials: credential) + let result = try await signIn(credentials: credential) + return result } // MARK: - End Provider APIs @@ -217,12 +222,14 @@ public final class AuthService { } } - public func handleAutoUpgradeAnonymousUser(credentials: AuthCredential) async throws { + public func handleAutoUpgradeAnonymousUser(credentials: AuthCredential) async throws -> SignInOutcome { if currentUser == nil { throw AuthServiceError.noCurrentUser } do { - try await currentUser?.link(with: credentials) + let result = try await currentUser?.link(with: credentials) + updateAuthenticationState() + return .signedIn(result) } catch let error as NSError { if error.code == AuthErrorCode.emailAlreadyInUse.rawValue { let context = AccountMergeConflictContext( @@ -237,16 +244,17 @@ public final class AuthService { } } - public func signIn(credentials: AuthCredential) async throws { + public func signIn(credentials: AuthCredential) async throws -> SignInOutcome { authenticationState = .authenticating do { if shouldHandleAnonymousUpgrade { - try await handleAutoUpgradeAnonymousUser(credentials: credentials) + return try await handleAutoUpgradeAnonymousUser(credentials: credentials) } else { let result = try await auth.signIn(with: credentials) signedInCredential = result.credential ?? credentials + updateAuthenticationState() + return .signedIn(result) } - updateAuthenticationState() } catch { authenticationState = .unauthenticated errorMessage = string.localizedErrorMessage( @@ -333,23 +341,24 @@ public extension AuthService { return self } - func signIn(withEmail email: String, password: String) async throws { + func signIn(withEmail email: String, password: String) async throws -> SignInOutcome { let credential = EmailAuthProvider.credential(withEmail: email, password: password) - try await signIn(credentials: credential) + return try await signIn(credentials: credential) } - func createUser(withEmail email: String, password: String) async throws { + func createUser(withEmail email: String, password: String) async throws -> SignInOutcome { authenticationState = .authenticating do { if shouldHandleAnonymousUpgrade { let credential = EmailAuthProvider.credential(withEmail: email, password: password) - try await handleAutoUpgradeAnonymousUser(credentials: credential) + return try await handleAutoUpgradeAnonymousUser(credentials: credential) } else { let result = try await auth.createUser(withEmail: email, password: password) signedInCredential = result.credential + updateAuthenticationState() + return .signedIn(result) } - updateAuthenticationState() } catch { authenticationState = .unauthenticated errorMessage = string.localizedErrorMessage( From 5c84e32cd98a5cb1a41a4bf3b245d69f3da243fa Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 2 Oct 2025 17:36:28 +0100 Subject: [PATCH 09/13] fix: make sendEmailVerification public --- .../FirebaseAuthSwiftUI/Sources/Services/AuthService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index 5876d2b5df..ab7a3a0dbf 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -264,7 +264,7 @@ public final class AuthService { } } - func sendEmailVerification() async throws { + public func sendEmailVerification() async throws { do { if let user = currentUser { // Requires running on MainActor as passing to sendEmailVerification() which is non-isolated From b3fd259831c2715414b97200821fc1c19fb71f38 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 2 Oct 2025 17:44:52 +0100 Subject: [PATCH 10/13] refactor: rename API labels --- .../Sources/Services/AuthService.swift | 8 ++++---- .../FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift | 4 ++-- .../FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift | 2 +- .../Sources/Views/PasswordRecoveryView.swift | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index ab7a3a0dbf..350a9fa522 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -341,12 +341,12 @@ public extension AuthService { return self } - func signIn(withEmail email: String, password: String) async throws -> SignInOutcome { + func signIn(email: String, password: String) async throws -> SignInOutcome { let credential = EmailAuthProvider.credential(withEmail: email, password: password) return try await signIn(credentials: credential) } - func createUser(withEmail email: String, password: String) async throws -> SignInOutcome { + func createUser(email email: String, password: String) async throws -> SignInOutcome { authenticationState = .authenticating do { @@ -368,7 +368,7 @@ public extension AuthService { } } - func sendPasswordRecoveryEmail(to email: String) async throws { + func sendPasswordRecoveryEmail(email: String) async throws { do { try await auth.sendPasswordReset(withEmail: email) } catch { @@ -383,7 +383,7 @@ public extension AuthService { // MARK: - Email Link Sign In public extension AuthService { - func sendEmailSignInLink(to email: String) async throws { + func sendEmailSignInLink(email: String) async throws { do { let actionCodeSettings = try updateActionCodeSettings() try await auth.sendSignInLink( diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift index 00fa3792b6..c79541a3ab 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift @@ -50,13 +50,13 @@ public struct EmailAuthView { private func signInWithEmailPassword() async { do { - try await authService.signIn(withEmail: email, password: password) + try await authService.signIn(email: email, password: password) } catch {} } private func createUserWithEmailPassword() async { do { - try await authService.createUser(withEmail: email, password: password) + try await authService.createUser(email: email, password: password) } catch {} } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift index 5e242f8d62..62c6e923aa 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift @@ -25,7 +25,7 @@ public struct EmailLinkView { private func sendEmailLink() async { do { - try await authService.sendEmailSignInLink(to: email) + try await authService.sendEmailSignInLink(email: email) showModal = true } catch {} } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift index d58f2be1f7..ee2a77a5ed 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift @@ -31,7 +31,7 @@ public struct PasswordRecoveryView { let recoveryResult: Result do { - try await authService.sendPasswordRecoveryEmail(to: email) + try await authService.sendPasswordRecoveryEmail(email: email) resultWrapper = ResultWrapper(value: .success(())) } catch { resultWrapper = ResultWrapper(value: .failure(error)) From 04c3dce125332a09d53dea6f1151c5ecde00e617 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 2 Oct 2025 17:49:33 +0100 Subject: [PATCH 11/13] feat: user profile update methods --- .../Sources/Services/AuthService.swift | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index 350a9fa522..82e326164f 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -484,3 +484,37 @@ public extension AuthService { try await signIn(credentials: credential) } } + +// MARK: - User Profile Management + +public extension AuthService { + func updateUserPhotoURL(url: URL) async throws { + guard let user = currentUser else { + throw AuthServiceError.noCurrentUser + } + + do { + let changeRequest = user.createProfileChangeRequest() + changeRequest.photoURL = url + try await changeRequest.commitChanges() + } catch { + errorMessage = string.localizedErrorMessage(for: error) + throw error + } + } + + func updateUserDisplayName(name: String) async throws { + guard let user = currentUser else { + throw AuthServiceError.noCurrentUser + } + + do { + let changeRequest = user.createProfileChangeRequest() + changeRequest.displayName = name + try await changeRequest.commitChanges() + } catch { + errorMessage = string.localizedErrorMessage(for: error) + throw error + } + } +} \ No newline at end of file From 2b02383876c9d0fa7dc5106d0b59d16ebe501174 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 7 Oct 2025 12:03:40 +0100 Subject: [PATCH 12/13] chore: remove deprecated "name" arg from packages --- Package.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Package.swift b/Package.swift index d3cba1ce9d..ef7348373e 100644 --- a/Package.swift +++ b/Package.swift @@ -81,27 +81,22 @@ let package = Package( ], dependencies: [ .package( - name: "Facebook", url: "https://github.com/facebook/facebook-ios-sdk.git", "17.0.0" ..< "18.0.0" ), .package( - name: "Firebase", url: "https://github.com/firebase/firebase-ios-sdk.git", "8.0.0" ..< "13.0.0" ), .package( - name: "GoogleSignIn", url: "https://github.com/google/GoogleSignIn-iOS", from: "7.0.0" ), .package( - name: "GoogleUtilities", url: "https://github.com/google/GoogleUtilities.git", "7.4.1" ..< "9.0.0" ), .package( - name: "SDWebImage", url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.0.0" ), From c7c3e4db1ae87d222378282dfb030693a736a3ab Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 7 Oct 2025 13:28:55 +0100 Subject: [PATCH 13/13] chore: remove preconcurrency from phone auth --- .../Sources/Services/PhoneAuthProviderAuthUI.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift index 97077db1ab..22899e0c8c 100644 --- a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift @@ -18,7 +18,7 @@ import SwiftUI public typealias VerificationID = String -public class PhoneAuthProviderAuthUI: @preconcurrency PhoneAuthProviderAuthUIProtocol, AuthProviderUI { +public class PhoneAuthProviderAuthUI: PhoneAuthProviderAuthUIProtocol, AuthProviderUI { public let id: String = "phone" public var provider: AuthProviderSwift { self }