From 65e23659134a43b08a1ff2561816f76984c01726 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 14 Nov 2025 11:11:52 +0000 Subject: [PATCH 1/6] fix: make shouldHandleAnonymousUpgrade private --- .../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 0640ef415f..0d781fbc0d 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -199,7 +199,7 @@ public final class AuthService { : .authenticated } - public var shouldHandleAnonymousUpgrade: Bool { + private var shouldHandleAnonymousUpgrade: Bool { currentUser?.isAnonymous == true && configuration.shouldAutoUpgradeAnonymousUsers } From 087b7219a808a8ebb25576d3c251ca414216deeb Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 14 Nov 2025 11:52:15 +0000 Subject: [PATCH 2/6] feat: allow consumer to update password prompt --- .../Sources/Services/AccountService+Email.swift | 10 ++++++---- .../Sources/Services/AuthService.swift | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift index 6f77ef6d67..2bb0b053eb 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift @@ -18,22 +18,24 @@ import Observation @MainActor @Observable public final class PasswordPromptCoordinator { - var isPromptingPassword = false + public var isPromptingPassword = false private var continuation: CheckedContinuation? - func confirmPassword() async throws -> String { + public init() {} + + public func confirmPassword() async throws -> String { return try await withCheckedThrowingContinuation { continuation in self.continuation = continuation self.isPromptingPassword = true } } - func submit(password: String) { + public func submit(password: String) { continuation?.resume(returning: password) cleanup() } - func cancel() { + public func cancel() { continuation? .resume(throwing: AuthServiceError .signInCancelled("Password entry cancelled for Email provider")) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index 0d781fbc0d..db9e2ad4dc 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -132,7 +132,7 @@ public final class AuthService { public var authenticationState: AuthenticationState = .unauthenticated public var authenticationFlow: AuthenticationFlow = .signIn - public let passwordPrompt: PasswordPromptCoordinator = .init() + public var passwordPrompt: PasswordPromptCoordinator = .init() public var currentMFARequired: MFARequired? private var currentMFAResolver: MultiFactorResolver? From 77d6ed9a1c13e3e7dc98c164e82d4329b66d916d Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 14 Nov 2025 13:08:50 +0000 Subject: [PATCH 3/6] refactor: remove currentMFARequired and make private --- .../Sources/Auth/MultiFactor.swift | 4 +-- .../Sources/Services/AuthService.swift | 5 +-- .../Sources/Views/AuthPickerView.swift | 4 +-- .../Sources/Views/MFAHandlerModifier.swift | 4 +-- .../Sources/Views/MFAResolutionView.swift | 36 +++++++++---------- .../Application/ContentView.swift | 2 +- 6 files changed, 26 insertions(+), 29 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Auth/MultiFactor.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Auth/MultiFactor.swift index e2be260f56..0e07bebacd 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Auth/MultiFactor.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Auth/MultiFactor.swift @@ -100,12 +100,12 @@ public struct EnrollmentSession { } } -public enum MFAHint { +public enum MFAHint: Hashable { case phone(displayName: String?, uid: String, phoneNumber: String?) case totp(displayName: String?, uid: String) } -public struct MFARequired { +public struct MFARequired: Hashable { public let hints: [MFAHint] public init(hints: [MFAHint]) { diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index db9e2ad4dc..fcc985b05b 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -49,7 +49,7 @@ public enum AuthView: Hashable { case updatePassword case mfaEnrollment case mfaManagement - case mfaResolution + case mfaResolution(MFARequired) case enterPhoneNumber case enterVerificationCode(verificationID: String, fullPhoneNumber: String) } @@ -133,7 +133,6 @@ public final class AuthService { public var authenticationFlow: AuthenticationFlow = .signIn public var passwordPrompt: PasswordPromptCoordinator = .init() - public var currentMFARequired: MFARequired? private var currentMFAResolver: MultiFactorResolver? // MARK: - Provider APIs @@ -879,7 +878,6 @@ public extension AuthService { private func handleMFARequiredError(resolver: MultiFactorResolver) -> SignInOutcome { let hints = extractMFAHints(from: resolver) - currentMFARequired = MFARequired(hints: hints) currentMFAResolver = resolver return .mfaRequired(MFARequired(hints: hints)) } @@ -957,7 +955,6 @@ public extension AuthService { updateAuthenticationState() // Clear MFA resolution state - currentMFARequired = nil currentMFAResolver = nil } catch { diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift index fbdc965238..42529968da 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift @@ -57,8 +57,8 @@ extension AuthPickerView: View { MFAEnrolmentView() case AuthView.mfaManagement: MFAManagementView() - case AuthView.mfaResolution: - MFAResolutionView() + case let .mfaResolution(mfaRequired): + MFAResolutionView(mfaRequired: mfaRequired) case AuthView.enterPhoneNumber: EnterPhoneNumberView() case let .enterVerificationCode(verificationID, fullPhoneNumber): diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAHandlerModifier.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAHandlerModifier.swift index 9c29ab8056..d593ee7d44 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAHandlerModifier.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAHandlerModifier.swift @@ -39,8 +39,8 @@ struct MFAHandlerModifier: ViewModifier { } /// Handle MFA required - navigate to MFA resolution view - func handleMFARequired(_: MFARequired) { - authService.navigator.push(.mfaResolution) + func handleMFARequired(_ mfaRequired: MFARequired) { + authService.navigator.push(.mfaResolution(mfaRequired)) } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAResolutionView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAResolutionView.swift index 03e56a1e98..437b895bbf 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAResolutionView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAResolutionView.swift @@ -23,6 +23,8 @@ private enum FocusableField: Hashable { @MainActor public struct MFAResolutionView { + let mfaRequired: MFARequired + @Environment(AuthService.self) private var authService @Environment(\.reportError) private var reportError @@ -34,16 +36,12 @@ public struct MFAResolutionView { @FocusState private var focus: FocusableField? - public init() {} - - private var mfaRequired: MFARequired? { - // This would be set by the sign-in flow when MFA is required - authService.currentMFARequired + public init(mfaRequired: MFARequired) { + self.mfaRequired = mfaRequired } private var selectedHint: MFAHint? { - guard let mfaRequired = mfaRequired, - selectedHintIndex < mfaRequired.hints.count else { + guard selectedHintIndex < mfaRequired.hints.count else { return nil } return mfaRequired.hints[selectedHintIndex] @@ -63,7 +61,7 @@ public struct MFAResolutionView { } private func startSMSChallenge() { - guard selectedHintIndex < (mfaRequired?.hints.count ?? 0) else { return } + guard selectedHintIndex < mfaRequired.hints.count else { return } Task { isLoading = true @@ -128,7 +126,7 @@ extension MFAResolutionView: View { .padding(.horizontal) // MFA Hints Selection (if multiple available) - if let mfaRequired = mfaRequired, mfaRequired.hints.count > 1 { + if mfaRequired.hints.count > 1 { mfaHintsSelectionView(mfaRequired: mfaRequired) } @@ -368,34 +366,36 @@ private extension MFAHint { #Preview("Phone SMS Only") { FirebaseOptions.dummyConfigurationForPreview() let authService = AuthService() - authService.currentMFARequired = MFARequired(hints: [ + let mfaRequired = MFARequired(hints: [ .phone(displayName: "Work Phone", uid: "phone-uid-1", phoneNumber: "+15551234567"), ]) - return MFAResolutionView().environment(authService) + return MFAResolutionView(mfaRequired: mfaRequired).environment(authService) } #Preview("TOTP Only") { FirebaseOptions.dummyConfigurationForPreview() let authService = AuthService() - authService.currentMFARequired = MFARequired(hints: [ + let mfaRequired = MFARequired(hints: [ .totp(displayName: "Authenticator App", uid: "totp-uid-1"), ]) - return MFAResolutionView().environment(authService) + return MFAResolutionView(mfaRequired: mfaRequired).environment(authService) } #Preview("Multiple Methods") { FirebaseOptions.dummyConfigurationForPreview() let authService = AuthService() - authService.currentMFARequired = MFARequired(hints: [ + let mfaRequired = MFARequired(hints: [ .phone(displayName: "Mobile", uid: "phone-uid-1", phoneNumber: "+15551234567"), .totp(displayName: "Google Authenticator", uid: "totp-uid-1"), ]) - return MFAResolutionView().environment(authService) + return MFAResolutionView(mfaRequired: mfaRequired).environment(authService) } -#Preview("No MFA Required") { +#Preview("Single TOTP") { FirebaseOptions.dummyConfigurationForPreview() let authService = AuthService() - // currentMFARequired is nil by default - return MFAResolutionView().environment(authService) + let mfaRequired = MFARequired(hints: [ + .totp(displayName: "Authenticator", uid: "totp-uid-1"), + ]) + return MFAResolutionView(mfaRequired: mfaRequired).environment(authService) } diff --git a/samples/swiftui/FirebaseSwiftUISample/FirebaseSwiftUISample/Application/ContentView.swift b/samples/swiftui/FirebaseSwiftUISample/FirebaseSwiftUISample/Application/ContentView.swift index 76d10a1a74..2d629af39e 100644 --- a/samples/swiftui/FirebaseSwiftUISample/FirebaseSwiftUISample/Application/ContentView.swift +++ b/samples/swiftui/FirebaseSwiftUISample/FirebaseSwiftUISample/Application/ContentView.swift @@ -25,7 +25,7 @@ import FirebaseOAuthSwiftUI struct ContentView: View { init() { - Auth.auth().useEmulator(withHost: "127.0.0.1", port: 9099) +// Auth.auth().useEmulator(withHost: "127.0.0.1", port: 9099) let actionCodeSettings = ActionCodeSettings() From 097dc180b81b86e1a01ddc74b8d4c50935307206 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 14 Nov 2025 13:34:44 +0000 Subject: [PATCH 4/6] refactor: passwordPrompt should be on emailprovider, not auth service --- .../Sources/Services/AuthService.swift | 22 ++++++++---- .../Services/EmailProviderAuthUI.swift | 34 +++++++++++++++++++ .../Sources/Views/AuthPickerView.swift | 4 +-- .../Sources/Views/UpdatePasswordView.swift | 4 +-- 4 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/EmailProviderAuthUI.swift diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index fcc985b05b..cd91cf5da9 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -132,13 +132,13 @@ public final class AuthService { public var authenticationState: AuthenticationState = .unauthenticated public var authenticationFlow: AuthenticationFlow = .signIn - public var passwordPrompt: PasswordPromptCoordinator = .init() private var currentMFAResolver: MultiFactorResolver? // MARK: - Provider APIs private var listenerManager: AuthListenerManager? + internal private(set) var emailProvider: EmailProviderSwift? var emailSignInEnabled = false private var emailSignInCallback: (() -> Void)? @@ -316,14 +316,18 @@ public extension AuthService { public extension AuthService { /// Enable email sign-in with default behavior (navigates to email link view) - func withEmailSignIn() -> AuthService { - return withEmailSignIn { [weak self] in + func withEmailSignIn(_ provider: EmailProviderSwift? = nil) -> AuthService { + return withEmailSignIn(provider) { [weak self] in self?.navigator.push(.emailLink) } } /// Enable email sign-in with custom callback - func withEmailSignIn(onTap: @escaping () -> Void) -> AuthService { + func withEmailSignIn( + _ provider: EmailProviderSwift? = nil, + onTap: @escaping () -> Void + ) -> AuthService { + emailProvider = provider ?? EmailProviderSwift() emailSignInEnabled = true emailSignInCallback = onTap return self @@ -746,8 +750,14 @@ public extension AuthService { guard let email = user.email else { throw AuthServiceError.invalidCredentials("User does not have an email address") } - let password = try await passwordPrompt.confirmPassword() - let credential = EmailAuthProvider.credential(withEmail: email, password: password) + + guard let emailProvider = emailProvider else { + throw AuthServiceError.providerNotFound( + "Email provider not configured. Call withEmailSignIn() first." + ) + } + + let credential = try await emailProvider.createReauthCredential(email: email) _ = try await user.reauthenticate(with: credential) } else if providerId == PhoneAuthProviderID { // Phone auth requires manual reauthentication via sign out and sign in otherwise it will take diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/EmailProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/EmailProviderAuthUI.swift new file mode 100644 index 0000000000..e097205b0f --- /dev/null +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/EmailProviderAuthUI.swift @@ -0,0 +1,34 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FirebaseAuth + +/// Email/Password authentication provider +/// This provider is special and doesn't render in the button list +@MainActor +public class EmailProviderSwift: AuthProviderSwift { + public let passwordPrompt: PasswordPromptCoordinator + public let providerId = EmailAuthProviderID + + public init(passwordPrompt: PasswordPromptCoordinator = .init()) { + self.passwordPrompt = passwordPrompt + } + + /// Create credential for reauthentication + func createReauthCredential(email: String) async throws -> AuthCredential { + let password = try await passwordPrompt.confirmPassword() + return EmailAuthProvider.credential(withEmail: email, password: password) + } +} + diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift index 42529968da..cc387b07df 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift @@ -33,7 +33,7 @@ public struct AuthPickerView { extension AuthPickerView: View { public var body: some View { @Bindable var authService = authService - @Bindable var passwordPrompt = authService.passwordPrompt + @Bindable var passwordPrompt = authService.emailProvider?.passwordPrompt ?? PasswordPromptCoordinator() content() .sheet(isPresented: $authService.isPresented) { @Bindable var navigator = authService.navigator @@ -82,7 +82,7 @@ extension AuthPickerView: View { } // Centralized password prompt sheet to prevent conflicts .sheet(isPresented: $passwordPrompt.isPromptingPassword) { - PasswordPromptSheet(coordinator: authService.passwordPrompt) + PasswordPromptSheet(coordinator: passwordPrompt) } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift index 9e8a1cb263..d54e493c58 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift @@ -53,7 +53,7 @@ public struct UpdatePasswordView { extension UpdatePasswordView: View { public var body: some View { - @Bindable var passwordPrompt = authService.passwordPrompt + @Bindable var passwordPrompt = authService.emailProvider?.passwordPrompt ?? PasswordPromptCoordinator() VStack(spacing: 24) { AuthTextField( text: $password, @@ -116,7 +116,7 @@ extension UpdatePasswordView: View { Text("Your password has been successfully updated.") } .sheet(isPresented: $passwordPrompt.isPromptingPassword) { - PasswordPromptSheet(coordinator: authService.passwordPrompt) + PasswordPromptSheet(coordinator: passwordPrompt) } } } From 960f702cd4033698a81ceaf2ef6e8375ace8061d Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 14 Nov 2025 13:35:02 +0000 Subject: [PATCH 5/6] format --- .../Sources/Services/AuthService.swift | 12 +++++------- .../Sources/Services/EmailProviderAuthUI.swift | 5 ++--- .../Sources/Views/AuthPickerView.swift | 3 ++- .../Sources/Views/MFAResolutionView.swift | 2 +- .../Sources/Views/UpdatePasswordView.swift | 3 ++- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index cd91cf5da9..ccb0676904 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -138,7 +138,7 @@ public final class AuthService { private var listenerManager: AuthListenerManager? - internal private(set) var emailProvider: EmailProviderSwift? + private(set) var emailProvider: EmailProviderSwift? var emailSignInEnabled = false private var emailSignInCallback: (() -> Void)? @@ -323,10 +323,8 @@ public extension AuthService { } /// Enable email sign-in with custom callback - func withEmailSignIn( - _ provider: EmailProviderSwift? = nil, - onTap: @escaping () -> Void - ) -> AuthService { + func withEmailSignIn(_ provider: EmailProviderSwift? = nil, + onTap: @escaping () -> Void) -> AuthService { emailProvider = provider ?? EmailProviderSwift() emailSignInEnabled = true emailSignInCallback = onTap @@ -750,13 +748,13 @@ public extension AuthService { guard let email = user.email else { throw AuthServiceError.invalidCredentials("User does not have an email address") } - + guard let emailProvider = emailProvider else { throw AuthServiceError.providerNotFound( "Email provider not configured. Call withEmailSignIn() first." ) } - + let credential = try await emailProvider.createReauthCredential(email: email) _ = try await user.reauthenticate(with: credential) } else if providerId == PhoneAuthProviderID { diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/EmailProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/EmailProviderAuthUI.swift index e097205b0f..6149b3fdeb 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/EmailProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/EmailProviderAuthUI.swift @@ -20,15 +20,14 @@ import FirebaseAuth public class EmailProviderSwift: AuthProviderSwift { public let passwordPrompt: PasswordPromptCoordinator public let providerId = EmailAuthProviderID - + public init(passwordPrompt: PasswordPromptCoordinator = .init()) { self.passwordPrompt = passwordPrompt } - + /// Create credential for reauthentication func createReauthCredential(email: String) async throws -> AuthCredential { let password = try await passwordPrompt.confirmPassword() return EmailAuthProvider.credential(withEmail: email, password: password) } } - diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift index cc387b07df..221dc2acc9 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift @@ -33,7 +33,8 @@ public struct AuthPickerView { extension AuthPickerView: View { public var body: some View { @Bindable var authService = authService - @Bindable var passwordPrompt = authService.emailProvider?.passwordPrompt ?? PasswordPromptCoordinator() + @Bindable var passwordPrompt = authService.emailProvider? + .passwordPrompt ?? PasswordPromptCoordinator() content() .sheet(isPresented: $authService.isPresented) { @Bindable var navigator = authService.navigator diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAResolutionView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAResolutionView.swift index 437b895bbf..8ffedbe885 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAResolutionView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAResolutionView.swift @@ -24,7 +24,7 @@ private enum FocusableField: Hashable { @MainActor public struct MFAResolutionView { let mfaRequired: MFARequired - + @Environment(AuthService.self) private var authService @Environment(\.reportError) private var reportError diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift index d54e493c58..482c0eb938 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift @@ -53,7 +53,8 @@ public struct UpdatePasswordView { extension UpdatePasswordView: View { public var body: some View { - @Bindable var passwordPrompt = authService.emailProvider?.passwordPrompt ?? PasswordPromptCoordinator() + @Bindable var passwordPrompt = authService.emailProvider? + .passwordPrompt ?? PasswordPromptCoordinator() VStack(spacing: 24) { AuthTextField( text: $password, From b533c03355002c6fd62aa6590a1019c94d066765 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 14 Nov 2025 14:27:17 +0000 Subject: [PATCH 6/6] refactor: update the way password confirmation is displayed --- .../Sources/Services/AuthService.swift | 7 ++++++- .../Sources/Views/AuthPickerView.swift | 11 +++++------ .../Sources/Views/UpdatePasswordView.swift | 5 ----- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index ccb0676904..42cccfb5ea 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -138,7 +138,12 @@ public final class AuthService { private var listenerManager: AuthListenerManager? - private(set) var emailProvider: EmailProviderSwift? + private var emailProvider: EmailProviderSwift? + + public var passwordPrompt: PasswordPromptCoordinator { + emailProvider?.passwordPrompt ?? PasswordPromptCoordinator() + } + var emailSignInEnabled = false private var emailSignInCallback: (() -> Void)? diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift index 221dc2acc9..26754e6038 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift @@ -33,11 +33,10 @@ public struct AuthPickerView { extension AuthPickerView: View { public var body: some View { @Bindable var authService = authService - @Bindable var passwordPrompt = authService.emailProvider? - .passwordPrompt ?? PasswordPromptCoordinator() content() .sheet(isPresented: $authService.isPresented) { @Bindable var navigator = authService.navigator + @Bindable var passwordPrompt = authService.passwordPrompt NavigationStack(path: $navigator.routes) { authPickerViewInternal .navigationTitle(authService.authenticationState == .unauthenticated ? authService @@ -80,10 +79,10 @@ extension AuthPickerView: View { .accountConflictHandler() // Apply MFA handling at NavigationStack level .mfaHandler() - } - // Centralized password prompt sheet to prevent conflicts - .sheet(isPresented: $passwordPrompt.isPromptingPassword) { - PasswordPromptSheet(coordinator: passwordPrompt) + // Centralized password prompt sheet inside auth flow + .sheet(isPresented: $passwordPrompt.isPromptingPassword) { + PasswordPromptSheet(coordinator: passwordPrompt) + } } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift index 482c0eb938..290f616262 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift @@ -53,8 +53,6 @@ public struct UpdatePasswordView { extension UpdatePasswordView: View { public var body: some View { - @Bindable var passwordPrompt = authService.emailProvider? - .passwordPrompt ?? PasswordPromptCoordinator() VStack(spacing: 24) { AuthTextField( text: $password, @@ -116,9 +114,6 @@ extension UpdatePasswordView: View { } message: { Text("Your password has been successfully updated.") } - .sheet(isPresented: $passwordPrompt.isPromptingPassword) { - PasswordPromptSheet(coordinator: passwordPrompt) - } } }