diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index 360daa74d6..2ba0ed5a6e 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -29,7 +29,7 @@ public protocol DeleteUserSwift { @MainActor func deleteUser(user: User) async throws } -public protocol PhoneAuthProviderAuthUIProtocol: AuthProviderSwift { +public protocol PhoneAuthProviderSwift: AuthProviderSwift { @MainActor func verifyPhoneNumber(phoneNumber: String) async throws -> String } @@ -108,42 +108,6 @@ public final class AuthService { public var currentMFARequired: MFARequired? private var currentMFAResolver: MultiFactorResolver? - // MARK: - AuthPickerView Modal APIs - - public var isShowingAuthModal = false - - public enum AuthModalContentType { - case phoneAuth - } - - public var currentModal: AuthModalContentType? - - public var authModalViewBuilderRegistry: [AuthModalContentType: () -> AnyView] = [:] - - public func registerModalView(for type: AuthModalContentType, - @ViewBuilder builder: @escaping () -> AnyView) { - authModalViewBuilderRegistry[type] = builder - } - - public func viewForCurrentModal() -> AnyView? { - guard let type = currentModal, - let builder = authModalViewBuilderRegistry[type] else { - return nil - } - return builder() - } - - public func presentModal(for type: AuthModalContentType) { - currentModal = type - isShowingAuthModal = true - } - - public func dismissModal() { - isShowingAuthModal = false - } - - // MARK: - End AuthPickerView Modal APIs - // MARK: - Provider APIs private var listenerManager: AuthListenerManager? diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift index 57c98f227c..1ba4f62146 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift @@ -26,13 +26,6 @@ public struct AuthPickerView { .authenticationFlow == .signIn ? .signUp : .signIn } - private var isAuthModalPresented: Binding { - Binding( - get: { authService.isShowingAuthModal }, - set: { authService.isShowingAuthModal = $0 } - ) - } - @ViewBuilder private var authPickerTitleView: some View { if authService.authView == .authPicker { @@ -103,33 +96,6 @@ extension AuthPickerView: View { EmptyView() } } - }.sheet(isPresented: isAuthModalPresented) { - VStack(spacing: 0) { - HStack { - Button(action: { - authService.dismissModal() - }) { - HStack(spacing: 4) { - Image(systemName: "chevron.left") - .font(.system(size: 17, weight: .medium)) - Text(authService.string.backButtonLabel) - .font(.system(size: 17)) - } - .foregroundColor(.blue) - } - Spacer() - } - .padding() - .background(Color(.systemBackground)) - - Divider() - - if let view = authService.viewForCurrentModal() { - view - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding() - } - } } } } diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AccountService+Facebook.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AccountService+Facebook.swift index 45493b3e69..5fd744bee8 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AccountService+Facebook.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AccountService+Facebook.swift @@ -24,7 +24,7 @@ import FirebaseAuthSwiftUI import Observation protocol FacebookOperationReauthentication { - var facebookProvider: FacebookProviderAuthUI { get } + var facebookProvider: FacebookProviderSwift { get } } extension FacebookOperationReauthentication { @@ -47,8 +47,8 @@ extension FacebookOperationReauthentication { @MainActor class FacebookDeleteUserOperation: AuthenticatedOperation, @preconcurrency FacebookOperationReauthentication { - let facebookProvider: FacebookProviderAuthUI - init(facebookProvider: FacebookProviderAuthUI) { + let facebookProvider: FacebookProviderSwift + init(facebookProvider: FacebookProviderSwift) { self.facebookProvider = facebookProvider } diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AuthService+Facebook.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AuthService+Facebook.swift index 9122e5b544..68c201f2cd 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AuthService+Facebook.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/AuthService+Facebook.swift @@ -23,9 +23,9 @@ import FirebaseAuthSwiftUI public extension AuthService { @discardableResult - func withFacebookSignIn(scopes scopes: [String]? = nil) -> AuthService { - FacebookProviderAuthUI.configureProvider(scopes: scopes) - registerProvider(providerWithButton: FacebookProviderAuthUI.shared) + func withFacebookSignIn(_ provider: FacebookProviderSwift? = nil) -> AuthService { + registerProvider(providerWithButton: FacebookProviderAuthUI(provider: provider ?? + FacebookProviderSwift())) return self } } diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift index fb459c3f6d..fa76d8ed3b 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift @@ -19,10 +19,6 @@ import FirebaseAuth import FirebaseAuthSwiftUI import SwiftUI -let kFacebookEmailScope = "email" -let kFacebookProfileScope = "public_profile" -let kDefaultFacebookScopes = [kFacebookEmailScope, kFacebookProfileScope] - public enum FacebookProviderError: Error { case signInCancelled(String) case configurationInvalid(String) @@ -31,44 +27,20 @@ public enum FacebookProviderError: Error { case authenticationToken(String) } -public class FacebookProviderAuthUI: AuthProviderSwift, AuthProviderUI, DeleteUserSwift { - public let id: String = "facebook" +public class FacebookProviderSwift: AuthProviderSwift, DeleteUserSwift { let scopes: [String] - let shortName = "Facebook" let providerId = "facebook.com" private let loginManager = LoginManager() private var rawNonce: String? private var shaNonce: String? // Needed for reauthentication var isLimitedLogin: Bool = true - - public var provider: AuthProviderSwift { self } - - @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, isLimitedLogin: Bool = true) { - _shared = FacebookProviderAuthUI(scopes: scopes, isLimitedLogin: isLimitedLogin) - } - - public init(scopes: [String]? = nil, isLimitedLogin: Bool = true) { - self.scopes = scopes ?? kDefaultFacebookScopes + public init(scopes: [String] = ["email", "public_profile"], isLimitedLogin: Bool = true) { + self.scopes = scopes self.isLimitedLogin = isLimitedLogin } - @MainActor public func authButton() -> AnyView { - AnyView(SignInWithFacebookButton()) - } - - public func deleteUser(user: User) async throws { - let operation = FacebookDeleteUserOperation(facebookProvider: self) - try await operation(on: user) - } - - @MainActor public func createAuthCredential() async throws -> AuthCredential { let loginType: LoginTracking = isLimitedLogin ? .limited : .enabled @@ -149,4 +121,22 @@ public class FacebookProviderAuthUI: AuthProviderSwift, AuthProviderUI, DeleteUs ) } } + + public func deleteUser(user: User) async throws { + let operation = FacebookDeleteUserOperation(facebookProvider: self) + try await operation(on: user) + } +} + +public class FacebookProviderAuthUI: AuthProviderUI { + public var provider: AuthProviderSwift + public let id: String = "facebook.com" + + public init(provider: AuthProviderSwift) { + self.provider = provider + } + + @MainActor public func authButton() -> AnyView { + AnyView(SignInWithFacebookButton(facebookProvider: provider)) + } } diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift index c41bca579d..e2f79f18f2 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift @@ -23,6 +23,7 @@ import SwiftUI @MainActor public struct SignInWithFacebookButton { @Environment(AuthService.self) private var authService + let facebookProvider: AuthProviderSwift @State private var errorMessage = "" @State private var showCanceledAlert = false @State private var limitedLogin = true @@ -30,7 +31,8 @@ public struct SignInWithFacebookButton { @State private var trackingAuthorizationStatus: ATTrackingManager .AuthorizationStatus = .notDetermined - public init() { + public init(facebookProvider: AuthProviderSwift) { + self.facebookProvider = facebookProvider _trackingAuthorizationStatus = State(initialValue: ATTrackingManager .trackingAuthorizationStatus) } @@ -65,7 +67,6 @@ extension SignInWithFacebookButton: View { Button(action: { Task { do { - let facebookProvider = FacebookProviderAuthUI(isLimitedLogin: limitedLogin) try await authService.signIn(facebookProvider) } catch { switch error { @@ -128,6 +129,7 @@ extension SignInWithFacebookButton: View { #Preview { FirebaseOptions.dummyConfigurationForPreview() - return SignInWithFacebookButton() + let facebookProvider = FacebookProviderSwift() + return SignInWithFacebookButton(facebookProvider: facebookProvider) .environment(AuthService()) } diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AccountService+Google.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AccountService+Google.swift index 176dd36d37..758005163e 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AccountService+Google.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AccountService+Google.swift @@ -31,7 +31,7 @@ import FirebaseAuthSwiftUI import Observation protocol GoogleOperationReauthentication { - var googleProvider: GoogleProviderAuthUI { get } + var googleProvider: GoogleProviderSwift { get } } extension GoogleOperationReauthentication { @@ -54,8 +54,8 @@ extension GoogleOperationReauthentication { @MainActor class GoogleDeleteUserOperation: AuthenticatedOperation, @preconcurrency GoogleOperationReauthentication { - let googleProvider: GoogleProviderAuthUI - init(googleProvider: GoogleProviderAuthUI) { + let googleProvider: GoogleProviderSwift + init(googleProvider: GoogleProviderSwift) { self.googleProvider = googleProvider } diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AuthService+Google.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AuthService+Google.swift index e1c2d2930a..d968e83b2f 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AuthService+Google.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AuthService+Google.swift @@ -23,9 +23,9 @@ import FirebaseAuthSwiftUI public extension AuthService { @discardableResult - func withGoogleSignIn(scopes scopes: [String]? = nil) -> AuthService { - let clientID = auth.app?.options.clientID ?? "" - registerProvider(providerWithButton: GoogleProviderAuthUI(scopes: scopes, clientID: clientID)) + func withGoogleSignIn(_ provider: GoogleProviderSwift? = nil) -> AuthService { + registerProvider(providerWithButton: GoogleProviderAuthUI(provider: provider ?? + GoogleProviderSwift(clientID: auth.app?.options.clientID ?? ""))) return self } } diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift index 34a2b37199..d785ce5f07 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift @@ -19,37 +19,24 @@ import GoogleSignIn import GoogleSignInSwift import SwiftUI -let kGoogleUserInfoEmailScope = "https://www.googleapis.com/auth/userinfo.email" -let kGoogleUserInfoProfileScope = "https://www.googleapis.com/auth/userinfo.profile" -let kDefaultScopes = [kGoogleUserInfoEmailScope, kGoogleUserInfoProfileScope] - public enum GoogleProviderError: Error { case rootViewControllerNotFound(String) case authenticationToken(String) case user(String) } -public class GoogleProviderAuthUI: AuthProviderSwift, AuthProviderUI, DeleteUserSwift { - public let id: String = "google" +public class GoogleProviderSwift: AuthProviderSwift, DeleteUserSwift { let scopes: [String] - let shortName = "Google" + let clientID: String 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 { - AnyView(SignInWithGoogleButton()) - } - public func deleteUser(user: User) async throws { - let operation = GoogleDeleteUserOperation(googleProvider: self) - try await operation(on: user) + public init(scopes: [String] = [ + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", + ], + clientID: String) { + self.clientID = clientID + self.scopes = scopes } @MainActor public func createAuthCredential() async throws -> AuthCredential { @@ -61,7 +48,7 @@ public class GoogleProviderAuthUI: AuthProviderSwift, AuthProviderUI, DeleteUser ) } - let config = GIDConfiguration(clientID: self.clientID) + let config = GIDConfiguration(clientID: clientID) GIDSignIn.sharedInstance.configuration = config return try await withCheckedThrowingContinuation { continuation in @@ -86,4 +73,22 @@ public class GoogleProviderAuthUI: AuthProviderSwift, AuthProviderUI, DeleteUser } } } + + public func deleteUser(user: User) async throws { + let operation = GoogleDeleteUserOperation(googleProvider: self) + try await operation(on: user) + } +} + +public class GoogleProviderAuthUI: AuthProviderUI { + public var provider: AuthProviderSwift + public let id: String = "google.com" + + public init(provider: AuthProviderSwift) { + self.provider = provider + } + + @MainActor public func authButton() -> AnyView { + AnyView(SignInWithGoogleButton(googleProvider: provider)) + } } diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift index 02c270f732..3a61a6f70d 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift @@ -26,7 +26,11 @@ import SwiftUI @MainActor public struct SignInWithGoogleButton { @Environment(AuthService.self) private var authService - private let googleProvider = GoogleProviderAuthUI() + let googleProvider: AuthProviderSwift + + public init(googleProvider: AuthProviderSwift) { + self.googleProvider = googleProvider + } let customViewModel = GoogleSignInButtonViewModel( scheme: .light, @@ -48,6 +52,7 @@ extension SignInWithGoogleButton: View { #Preview { FirebaseOptions.dummyConfigurationForPreview() - return SignInWithGoogleButton() + let googleProvider = GoogleProviderSwift(clientID: "") + return SignInWithGoogleButton(googleProvider: googleProvider) .environment(AuthService()) } diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift index 22899e0c8c..be555bdc92 100644 --- a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift @@ -18,18 +18,8 @@ import SwiftUI public typealias VerificationID = String -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()) - } +public class PhoneProviderSwift: PhoneAuthProviderSwift { + public init() {} @MainActor public func verifyPhoneNumber(phoneNumber: String) async throws -> VerificationID { return try await withCheckedThrowingContinuation { continuation in @@ -43,20 +33,47 @@ public class PhoneAuthProviderAuthUI: PhoneAuthProviderAuthUIProtocol, AuthProvi } } } - - // Set verification details before calling signIn - public func setVerificationDetails(verificationID: String, verificationCode: String) { - self.storedVerificationID = verificationID - self.storedVerificationCode = verificationCode - } - + + // Present phone auth UI and wait for user to complete the flow @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") + guard let presentingViewController = await (UIApplication.shared.connectedScenes + .first as? UIWindowScene)?.windows.first?.rootViewController else { + throw AuthServiceError + .invalidPhoneAuthenticationArguments( + "Root View controller is not available to present Phone auth View." + ) + } + + return try await withCheckedThrowingContinuation { continuation in + let phoneAuthView = PhoneAuthView(phoneProvider: self) { result in + switch result { + case let .success(verificationID, verificationCode): + // Create the credential here + let credential = PhoneAuthProvider.provider() + .credential(withVerificationID: verificationID, verificationCode: verificationCode) + continuation.resume(returning: credential) + case let .failure(error): + continuation.resume(throwing: error) + } + } + + let hostingController = UIHostingController(rootView: phoneAuthView) + hostingController.modalPresentationStyle = .formSheet + + presentingViewController.present(hostingController, animated: true) } - - return PhoneAuthProvider.provider() - .credential(withVerificationID: verificationID, verificationCode: verificationCode) + } +} + +public class PhoneAuthProviderAuthUI: AuthProviderUI { + public var provider: AuthProviderSwift + public let id: String = "phone.com" + + public init(provider: PhoneAuthProviderSwift? = nil) { + self.provider = provider ?? PhoneProviderSwift() + } + + @MainActor public func authButton() -> AnyView { + AnyView(PhoneAuthButtonView(phoneProvider: provider as! PhoneAuthProviderSwift)) } } diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthButtonView.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthButtonView.swift index 9ca1a5b41b..de045e736e 100644 --- a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthButtonView.swift +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthButtonView.swift @@ -19,17 +19,19 @@ import SwiftUI @MainActor public struct PhoneAuthButtonView { @Environment(AuthService.self) private var authService + let phoneProvider: PhoneAuthProviderSwift - public init() {} + public init(phoneProvider: PhoneAuthProviderSwift) { + self.phoneProvider = phoneProvider + } } extension PhoneAuthButtonView: View { public var body: some View { Button(action: { - authService.registerModalView(for: .phoneAuth) { - AnyView(PhoneAuthView().environment(authService)) + Task { + try await authService.signIn(phoneProvider) } - authService.presentModal(for: .phoneAuth) }) { Label("Sign in with Phone", systemImage: "phone.fill") .foregroundColor(.white) @@ -44,6 +46,7 @@ extension PhoneAuthButtonView: View { #Preview { FirebaseOptions.dummyConfigurationForPreview() - return PhoneAuthButtonView() + let phoneProvider = PhoneProviderSwift() + return PhoneAuthButtonView(phoneProvider: phoneProvider) .environment(AuthService()) } diff --git a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthView.swift b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthView.swift index 769b488b47..0ec37e0b93 100644 --- a/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthView.swift +++ b/FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Views/PhoneAuthView.swift @@ -19,104 +19,183 @@ // Created by Russell Wheatley on 14/05/2025. // +import FirebaseAuth import FirebaseAuthSwiftUI import FirebaseCore import SwiftUI @MainActor public struct PhoneAuthView { - @Environment(AuthService.self) private var authService + @Environment(\.dismiss) private var dismiss @State private var errorMessage = "" @State private var phoneNumber = "" @State private var showVerificationCodeInput = false @State private var verificationCode = "" @State private var verificationID = "" - private let phoneProvider = PhoneAuthProviderAuthUI() + @State private var isProcessing = false + let phoneProvider: PhoneAuthProviderSwift + let completion: (Result<(String, String), Error>) -> Void - public init() {} + public init(phoneProvider: PhoneAuthProviderSwift, + completion: @escaping (Result<(String, String), Error>) -> Void) { + self.phoneProvider = phoneProvider + self.completion = completion + } } extension PhoneAuthView: View { public var body: some View { - if authService.authenticationState != .authenticating { - VStack { - LabeledContent { - TextField(authService.string.enterPhoneNumberLabel, text: $phoneNumber) - .textInputAutocapitalization(.never) - .disableAutocorrection(true) - .submitLabel(.next) - } label: { - Image(systemName: "at") - }.padding(.vertical, 6) + ZStack { + VStack(spacing: 16) { + // Header with cancel button + HStack { + Spacer() + Button(action: { + completion(.failure(NSError( + domain: "PhoneAuthError", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "User cancelled"] + ))) + dismiss() + }) { + Image(systemName: "xmark.circle.fill") + .font(.title2) + .foregroundColor(.gray) + } + } + .padding(.horizontal) + .padding(.top, 8) + + if !isProcessing { + Text("Sign in with Phone") + .font(.title2) + .bold() + + LabeledContent { + TextField("Enter phone number", text: $phoneNumber) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .submitLabel(.next) + .keyboardType(.phonePad) + } label: { + Image(systemName: "phone.fill") + } + .padding(.vertical, 6) .background(Divider(), alignment: .bottom) .padding(.bottom, 4) - Button(action: { - Task { - do { - let id = try await phoneProvider.verifyPhoneNumber(phoneNumber: phoneNumber) - verificationID = id - showVerificationCodeInput = true - } catch { - errorMessage = authService.string.localizedErrorMessage( - for: error - ) + .padding(.horizontal) + + if !errorMessage.isEmpty { + Text(errorMessage) + .foregroundColor(.red) + .font(.caption) + .padding(.horizontal) + } + + Button(action: { + Task { + isProcessing = true + do { + let id = try await phoneProvider.verifyPhoneNumber(phoneNumber: phoneNumber) + verificationID = id + showVerificationCodeInput = true + errorMessage = "" + } catch { + errorMessage = error.localizedDescription + } + isProcessing = false } + }) { + Text("Send Code") + .padding(.vertical, 8) + .frame(maxWidth: .infinity) } - }) { - Text(authService.string.smsCodeSendButtonLabel) - .padding(.vertical, 8) - .frame(maxWidth: .infinity) - } - .disabled(!PhoneUtils.isValidPhoneNumber(phoneNumber)) - .padding([.top, .bottom], 8) - .frame(maxWidth: .infinity) - .buttonStyle(.borderedProminent) - Text(errorMessage).foregroundColor(.red) - }.sheet(isPresented: $showVerificationCodeInput) { - TextField(authService.string.phoneNumberVerificationCodeLabel, text: $verificationCode) - .keyboardType(.numberPad) - .padding() - .background(Color(.systemGray6)) - .cornerRadius(8) + .disabled(!PhoneUtils.isValidPhoneNumber(phoneNumber) || isProcessing) + .padding([.top, .bottom], 8) .padding(.horizontal) + .buttonStyle(.borderedProminent) - Button(action: { - Task { - do { - phoneProvider.setVerificationDetails( - verificationID: verificationID, - verificationCode: verificationCode - ) - try await authService.signIn(phoneProvider) - } catch { - errorMessage = authService.string.localizedErrorMessage(for: error) + Spacer() + } else { + Spacer() + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .padding() + Text("Processing...") + .foregroundColor(.secondary) + Spacer() + } + } + .sheet(isPresented: $showVerificationCodeInput) { + VStack(spacing: 16) { + // Header with cancel button + HStack { + Spacer() + Button(action: { + showVerificationCodeInput = false + }) { + Image(systemName: "xmark.circle.fill") + .font(.title2) + .foregroundColor(.gray) } - showVerificationCodeInput = false - authService.dismissModal() } - }) { - Text(authService.string.verifyPhoneNumberAndSignInLabel) - .foregroundColor(.white) + .padding(.horizontal) + .padding(.top, 8) + + Text("Enter Verification Code") + .font(.title2) + .bold() + + TextField("Verification Code", text: $verificationCode) + .keyboardType(.numberPad) .padding() - .frame(maxWidth: .infinity) - .background(Color.green) + .background(Color(.systemGray6)) .cornerRadius(8) .padding(.horizontal) + + if !errorMessage.isEmpty { + Text(errorMessage) + .foregroundColor(.red) + .font(.caption) + .padding(.horizontal) + } + + Button(action: { + Task { + isProcessing = true + // Return the verification details to createAuthCredential + completion(.success((verificationID, verificationCode))) + showVerificationCodeInput = false + dismiss() + } + }) { + Text("Verify and Sign In") + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(Color.green) + .cornerRadius(8) + .padding(.horizontal) + } + .disabled(verificationCode.isEmpty || isProcessing) + + Spacer() } - }.onOpenURL { url in - authService.auth.canHandle(url) + .padding(.vertical) } - } else { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .padding(.vertical, 8) - .frame(maxWidth: .infinity) } } } #Preview { FirebaseOptions.dummyConfigurationForPreview() - return PhoneAuthView() - .environment(AuthService()) + let phoneProvider = PhoneProviderSwift() + return PhoneAuthView(phoneProvider: phoneProvider) { result in + switch result { + case let .success(verificationID, verificationCode): + print("Preview: Got verification - ID: \(verificationID), Code: \(verificationCode)") + case let .failure(error): + print("Preview: Phone auth failed with error: \(error)") + } + } }