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 }
@@ -211,7 +251,6 @@ public final class AuthService {
211251 }
212252 do {
213253 let result = try await currentUser? . link ( with: credentials)
214- signedInCredential = credentials
215254 updateAuthenticationState ( )
216255 return . signedIn( result)
217256 } catch let error as NSError {
@@ -251,7 +290,6 @@ public final class AuthService {
251290 return try await handleAutoUpgradeAnonymousUser ( credentials: credentials)
252291 } else {
253292 let result = try await auth. signIn ( with: credentials)
254- signedInCredential = result. credential ?? credentials
255293 updateAuthenticationState ( )
256294 return . signedIn( result)
257295 }
@@ -261,8 +299,6 @@ public final class AuthService {
261299 if error. code == AuthErrorCode . secondFactorRequired. rawValue {
262300 if let resolver = error
263301 . userInfo [ AuthErrorUserInfoMultiFactorResolverKey] as? MultiFactorResolver {
264- // Preserve the original credential for use after MFA resolution
265- pendingMFACredential = credentials
266302 return handleMFARequiredError ( resolver: resolver)
267303 }
268304 } else {
@@ -351,7 +387,6 @@ public extension AuthService {
351387 return try await handleAutoUpgradeAnonymousUser ( credentials: credential)
352388 } else {
353389 let result = try await auth. createUser ( withEmail: email, password: password)
354- signedInCredential = result. credential
355390 updateAuthenticationState ( )
356391 return . signedIn( result)
357392 }
@@ -728,12 +763,41 @@ public extension AuthService {
728763 }
729764 }
730765
731- func reauthenticateCurrentUser ( on user : User ) async throws {
732- guard let providerId = signedInCredential ? . provider else {
733- throw AuthServiceError
734- . reauthenticationRequired ( " Recent login required to perform this operation. " )
766+ /// Gets the provider ID that was used for the current sign-in session
767+ private func getCurrentSignInProvider ( ) async throws -> String {
768+ guard let user = currentUser else {
769+ throw AuthServiceError . noCurrentUser
735770 }
736771
772+ // Get the ID token result which contains the signInProvider claim
773+ let tokenResult = try await user. getIDTokenResult ( forcingRefresh: false )
774+
775+ // The signInProvider property tells us which provider was used for this session
776+ let signInProvider = tokenResult. signInProvider
777+
778+ // If signInProvider is not empty, use it
779+ if !signInProvider. isEmpty {
780+ return signInProvider
781+ }
782+
783+ // Fallback: if signInProvider is empty, try to infer from providerData
784+ // Prefer non-password providers as they're more specific
785+ let providerId = user. providerData. first ( where: { $0. providerID != " password " } ) ? . providerID
786+ ?? user. providerData. first? . providerID
787+
788+ guard let providerId = providerId else {
789+ throw AuthServiceError . reauthenticationRequired (
790+ " Unable to determine sign-in provider for reauthentication "
791+ )
792+ }
793+
794+ return providerId
795+ }
796+
797+ func reauthenticateCurrentUser( on user: User ) async throws {
798+ // Get the provider from the token instead of stored credential
799+ let providerId = try await getCurrentSignInProvider ( )
800+
737801 if providerId == EmailAuthProviderID {
738802 guard let email = user. email else {
739803 throw AuthServiceError . invalidCredentials ( " User does not have an email address " )
@@ -815,7 +879,7 @@ public extension AuthService {
815879 let hints = extractMFAHints ( from: resolver)
816880 currentMFARequired = MFARequired ( hints: hints)
817881 currentMFAResolver = resolver
818- authView = . mfaResolution
882+ navigator . push ( . mfaResolution)
819883 return . mfaRequired( MFARequired ( hints: hints) )
820884 }
821885
@@ -895,16 +959,11 @@ public extension AuthService {
895959
896960 do {
897961 let result = try await resolver. resolveSignIn ( with: assertion)
898-
899- // After MFA resolution, result.credential is nil, so restore the original credential
900- // that was used before MFA was triggered
901- signedInCredential = result. credential ?? pendingMFACredential
902962 updateAuthenticationState ( )
903963
904964 // Clear MFA resolution state
905965 currentMFARequired = nil
906966 currentMFAResolver = nil
907- pendingMFACredential = nil
908967
909968 } catch {
910969 throw AuthServiceError
0 commit comments