diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift index badbc98519..fbb5570f45 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift @@ -36,6 +36,8 @@ 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? { switch self { @@ -55,6 +57,10 @@ public enum AuthServiceError: LocalizedError { return "Failed to sign in: \(error.localizedDescription)" case let .accountMergeConflict(context): return context.errorDescription + case let .providerNotFound(description): + return description + case let .invalidPhoneAuthenticationArguments(description): + return description } } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index b124e69b29..82e326164f 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 } @@ -41,7 +40,7 @@ public enum AuthenticationState { } public enum AuthenticationFlow { - case login + case signIn case signUp } @@ -52,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? @@ -95,7 +98,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() @@ -137,30 +140,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 +160,10 @@ 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 signIn(_ provider: AuthProviderSwift) async throws -> SignInOutcome { + let credential = try await provider.createAuthCredential() + let result = try await signIn(credentials: credential) + return result } // MARK: - End Provider APIs @@ -256,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( @@ -276,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( @@ -295,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 @@ -327,13 +296,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 @@ -369,23 +341,24 @@ public extension AuthService { return self } - func signIn(withEmail email: String, password: String) async throws { + func signIn(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(email 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( @@ -395,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 { @@ -410,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( @@ -488,49 +461,60 @@ public extension AuthService { } } -// MARK: - Google Sign In + +// MARK: - Phone Auth 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." - ) + func verifyPhoneNumber(phoneNumber: String) async throws -> String { + 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!) + } } - 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) + func signInWithPhoneNumber(verificationID: String, verificationCode: String) async throws { + let credential = PhoneAuthProvider.provider() + .credential(withVerificationID: verificationID, verificationCode: verificationCode) try await signIn(credentials: credential) } } -// MARK: - Phone Auth Sign In +// MARK: - User Profile Management public extension AuthService { - func verifyPhoneNumber(phoneNumber: String) async throws -> String { + func updateUserPhotoURL(url: URL) async throws { + guard let user = currentUser else { + throw AuthServiceError.noCurrentUser + } + do { - return try await phoneAuthProvider.verifyPhoneNumber(phoneNumber: phoneNumber) + let changeRequest = user.createProfileChangeRequest() + changeRequest.photoURL = url + try await changeRequest.commitChanges() } catch { - errorMessage = string.localizedErrorMessage( - for: error - ) + errorMessage = string.localizedErrorMessage(for: error) throw error } } - - func signInWithPhoneNumber(verificationID: String, verificationCode: String) async throws { - let credential = PhoneAuthProvider.provider() - .credential(withVerificationID: verificationID, verificationCode: verificationCode) - try await signIn(credentials: credential) + + 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 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..c79541a3ab 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 @@ -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 {} } } @@ -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) 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)) 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/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..fb459c3f6d 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,20 +41,22 @@ 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) + @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 { @@ -66,9 +68,9 @@ public class FacebookProviderAuthUI: FacebookProviderAuthUIProtocol { try await operation(on: user) } - @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 guard let configuration: LoginConfiguration = { if loginType == .limited { diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift index 6ddfc1d681..eaa7606c27 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 facebookProvider = FacebookProviderAuthUI(isLimitedLogin: limitedLogin) + try await authService.signIn(facebookProvider) } catch { switch error { case FacebookProviderError.signInCancelled: 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/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..34a2b37199 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift @@ -29,19 +29,21 @@ 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 } @MainActor public func authButton() -> AnyView { - // Moved to SignInWithGoogleButton so we could sign in via AuthService AnyView(SignInWithGoogleButton()) } @@ -50,7 +52,7 @@ public class GoogleProviderAuthUI: @preconcurrency GoogleProviderAuthUIProtocol try await operation(on: user) } - @MainActor public func signInWithGoogle(clientID: String) async throws -> AuthCredential { + @MainActor public func createAuthCredential() async throws -> AuthCredential { guard let presentingViewController = await (UIApplication.shared.connectedScenes .first as? UIWindowScene)?.windows.first?.rootViewController else { throw GoogleProviderError @@ -59,7 +61,7 @@ public class GoogleProviderAuthUI: @preconcurrency GoogleProviderAuthUIProtocol ) } - 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 634f33cbdc..7cc5a9131f 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,7 +39,7 @@ extension SignInWithGoogleButton: View { public var body: some View { GoogleSignInButton(viewModel: customViewModel) { Task { - try await authService.signInWithGoogle() + try await authService.signIn(googleProvider) } } } 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..22899e0c8c 100644 --- a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift @@ -18,8 +18,14 @@ import SwiftUI public typealias VerificationID = String -public class PhoneAuthProviderAuthUI: @preconcurrency PhoneAuthProviderAuthUIProtocol { +public class PhoneAuthProviderAuthUI: 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()) @@ -37,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..769b488b47 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.signIn(phoneProvider) } catch { errorMessage = authService.string.localizedErrorMessage(for: error) } 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" ),