Skip to content

Commit c072ad8

Browse files
Merge branch 'development' into anonymous-upgrade
2 parents a09e615 + 06a3688 commit c072ad8

111 files changed

Lines changed: 3166 additions & 1307 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Views/SignInWithAppleButton.swift

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
import FirebaseAuthSwiftUI
16+
import FirebaseAuthUIComponents
1617
import SwiftUI
1718

1819
/// A button for signing in with Apple
@@ -28,7 +29,11 @@ public struct SignInWithAppleButton {
2829

2930
extension SignInWithAppleButton: View {
3031
public var body: some View {
31-
Button(action: {
32+
AuthProviderButton(
33+
label: "Sign in with Apple",
34+
style: .apple,
35+
accessibilityId: "sign-in-with-apple-button"
36+
) {
3237
Task {
3338
if let handler = signInHandler {
3439
try? await handler(authService) {
@@ -38,23 +43,6 @@ extension SignInWithAppleButton: View {
3843
try? await authService.signIn(provider)
3944
}
4045
}
41-
}) {
42-
HStack {
43-
Image(systemName: "apple.logo")
44-
.resizable()
45-
.renderingMode(.template)
46-
.scaledToFit()
47-
.frame(width: 24, height: 24)
48-
.foregroundColor(.white)
49-
Text("Sign in with Apple")
50-
.fontWeight(.semibold)
51-
.foregroundColor(.white)
52-
}
53-
.frame(maxWidth: .infinity, alignment: .leading)
54-
.padding()
55-
.background(Color.black)
56-
.cornerRadius(8)
5746
}
58-
.accessibilityIdentifier("sign-in-with-apple-button")
5947
}
6048
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
@preconcurrency import FirebaseAuth
16+
import FirebaseAuthUIComponents
1617
import FirebaseCore
1718
import SwiftUI
1819

@@ -28,6 +29,7 @@ public protocol AuthProviderUI {
2829

2930
public protocol PhoneAuthProviderSwift: AuthProviderSwift {
3031
@MainActor func verifyPhoneNumber(phoneNumber: String) async throws -> String
32+
func setVerificationCode(verificationID: String, code: String)
3133
}
3234

3335
public 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

5457
public 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
87108
public 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

Comments
 (0)