1313// limitations under the License.
1414
1515@preconcurrency import FirebaseAuth
16+ import FirebaseAuthUIComponents
1617import FirebaseCore
1718import SwiftUI
1819
@@ -28,6 +29,7 @@ public protocol AuthProviderUI {
2829
2930public protocol PhoneAuthProviderSwift : AuthProviderSwift {
3031 @MainActor func verifyPhoneNumber( phoneNumber: String ) async throws -> String
32+ func setVerificationCode( verificationID: String , code: String )
3133}
3234
3335public enum AuthenticationState {
@@ -41,14 +43,15 @@ public enum AuthenticationFlow {
4143 case signUp
4244}
4345
44- public enum AuthView {
45- case authPicker
46+ public enum AuthView : Hashable {
4647 case passwordRecovery
4748 case emailLink
4849 case updatePassword
4950 case mfaEnrollment
5051 case mfaManagement
5152 case mfaResolution
53+ case enterPhoneNumber
54+ case enterVerificationCode( verificationID: String , fullPhoneNumber: String )
5255}
5356
5457public enum SignInOutcome : @unchecked Sendable {
@@ -82,6 +85,24 @@ private final class AuthListenerManager {
8285 }
8386}
8487
88+ @Observable
89+ public class Navigator {
90+ var routes : [ AuthView ] = [ ]
91+
92+ public func push( _ route: AuthView ) {
93+ routes. append ( route)
94+ }
95+
96+ @discardableResult
97+ public func pop( ) -> AuthView ? {
98+ routes. popLast ( )
99+ }
100+
101+ public func clear( ) {
102+ routes. removeAll ( )
103+ }
104+ }
105+
85106@MainActor
86107@Observable
87108public final class AuthService {
@@ -96,7 +117,16 @@ public final class AuthService {
96117 @ObservationIgnored @AppStorage ( " email-link " ) public var emailLink : String ?
97118 public let configuration : AuthConfiguration
98119 public let auth : Auth
99- public var authView : AuthView = . authPicker
120+ public var isPresented : Bool = false
121+ public private( set) var navigator = Navigator ( )
122+ public var authView : AuthView ? {
123+ navigator. routes. last
124+ }
125+
126+ var authViewRoutes : [ AuthView ] {
127+ navigator. routes
128+ }
129+
100130 public let string : StringUtils
101131 public var currentUser : User ?
102132 public var authenticationState : AuthenticationState = . unauthenticated
@@ -105,23 +135,33 @@ public final class AuthService {
105135 public let passwordPrompt : PasswordPromptCoordinator = . init( )
106136 public var currentMFARequired : MFARequired ?
107137 private var currentMFAResolver : MultiFactorResolver ?
108- private var pendingMFACredential : AuthCredential ?
109138
110139 // MARK: - Provider APIs
111140
112141 private var listenerManager : AuthListenerManager ?
113- public var signedInCredential : AuthCredential ?
114142
115143 var emailSignInEnabled = false
116144
117145 private var providers : [ AuthProviderUI ] = [ ]
146+
147+ public var currentPhoneProvider : PhoneAuthProviderSwift ? {
148+ providers. compactMap { $0. provider as? PhoneAuthProviderSwift } . first
149+ }
150+
118151 public func registerProvider( providerWithButton: AuthProviderUI ) {
119152 providers. append ( providerWithButton)
120153 }
121154
122155 public func renderButtons( spacing: CGFloat = 16 ) -> AnyView {
123156 AnyView (
124157 VStack ( spacing: spacing) {
158+ AuthProviderButton (
159+ label: string. signInWithEmailLinkViewTitle,
160+ style: . email,
161+ accessibilityId: " sign-in-with-email-link-button "
162+ ) {
163+ self . navigator. push ( . emailLink)
164+ }
125165 ForEach ( providers, id: \. id) { provider in
126166 provider. authButton ( )
127167 }
@@ -209,7 +249,6 @@ public final class AuthService {
209249 }
210250 do {
211251 let result = try await currentUser? . link ( with: credentials)
212- signedInCredential = credentials
213252 updateAuthenticationState ( )
214253 return . signedIn( result)
215254 } catch let error as NSError {
@@ -233,7 +272,6 @@ public final class AuthService {
233272 return try await handleAutoUpgradeAnonymousUser ( credentials: credentials)
234273 } else {
235274 let result = try await auth. signIn ( with: credentials)
236- signedInCredential = result. credential ?? credentials
237275 updateAuthenticationState ( )
238276 return . signedIn( result)
239277 }
@@ -243,8 +281,6 @@ public final class AuthService {
243281 if error. code == AuthErrorCode . secondFactorRequired. rawValue {
244282 if let resolver = error
245283 . userInfo [ AuthErrorUserInfoMultiFactorResolverKey] as? MultiFactorResolver {
246- // Preserve the original credential for use after MFA resolution
247- pendingMFACredential = credentials
248284 return handleMFARequiredError ( resolver: resolver)
249285 }
250286 } else {
@@ -333,7 +369,6 @@ public extension AuthService {
333369 return try await handleAutoUpgradeAnonymousUser ( credentials: credential)
334370 } else {
335371 let result = try await auth. createUser ( withEmail: email, password: password)
336- signedInCredential = result. credential
337372 updateAuthenticationState ( )
338373 return . signedIn( result)
339374 }
@@ -710,12 +745,41 @@ public extension AuthService {
710745 }
711746 }
712747
713- func reauthenticateCurrentUser ( on user : User ) async throws {
714- guard let providerId = signedInCredential ? . provider else {
715- throw AuthServiceError
716- . reauthenticationRequired ( " Recent login required to perform this operation. " )
748+ /// Gets the provider ID that was used for the current sign-in session
749+ private func getCurrentSignInProvider ( ) async throws -> String {
750+ guard let user = currentUser else {
751+ throw AuthServiceError . noCurrentUser
717752 }
718753
754+ // Get the ID token result which contains the signInProvider claim
755+ let tokenResult = try await user. getIDTokenResult ( forcingRefresh: false )
756+
757+ // The signInProvider property tells us which provider was used for this session
758+ let signInProvider = tokenResult. signInProvider
759+
760+ // If signInProvider is not empty, use it
761+ if !signInProvider. isEmpty {
762+ return signInProvider
763+ }
764+
765+ // Fallback: if signInProvider is empty, try to infer from providerData
766+ // Prefer non-password providers as they're more specific
767+ let providerId = user. providerData. first ( where: { $0. providerID != " password " } ) ? . providerID
768+ ?? user. providerData. first? . providerID
769+
770+ guard let providerId = providerId else {
771+ throw AuthServiceError . reauthenticationRequired (
772+ " Unable to determine sign-in provider for reauthentication "
773+ )
774+ }
775+
776+ return providerId
777+ }
778+
779+ func reauthenticateCurrentUser( on user: User ) async throws {
780+ // Get the provider from the token instead of stored credential
781+ let providerId = try await getCurrentSignInProvider ( )
782+
719783 if providerId == EmailAuthProviderID {
720784 guard let email = user. email else {
721785 throw AuthServiceError . invalidCredentials ( " User does not have an email address " )
@@ -797,7 +861,7 @@ public extension AuthService {
797861 let hints = extractMFAHints ( from: resolver)
798862 currentMFARequired = MFARequired ( hints: hints)
799863 currentMFAResolver = resolver
800- authView = . mfaResolution
864+ navigator . push ( . mfaResolution)
801865 return . mfaRequired( MFARequired ( hints: hints) )
802866 }
803867
@@ -877,16 +941,11 @@ public extension AuthService {
877941
878942 do {
879943 let result = try await resolver. resolveSignIn ( with: assertion)
880-
881- // After MFA resolution, result.credential is nil, so restore the original credential
882- // that was used before MFA was triggered
883- signedInCredential = result. credential ?? pendingMFACredential
884944 updateAuthenticationState ( )
885945
886946 // Clear MFA resolution state
887947 currentMFARequired = nil
888948 currentMFAResolver = nil
889- pendingMFACredential = nil
890949
891950 } catch {
892951 throw AuthServiceError
0 commit comments