Skip to content

Commit 06a3688

Browse files
Merge pull request #1285 from demolaf/ui-updates
2 parents ff8ff47 + fd4d28b commit 06a3688

File tree

112 files changed

+3166
-1299
lines changed

Some content is hidden

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

112 files changed

+3166
-1299
lines changed

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
@@ -27,27 +28,14 @@ public struct SignInWithAppleButton {
2728

2829
extension SignInWithAppleButton: View {
2930
public var body: some View {
30-
Button(action: {
31+
AuthProviderButton(
32+
label: "Sign in with Apple",
33+
style: .apple,
34+
accessibilityId: "sign-in-with-apple-button"
35+
) {
3136
Task {
3237
try? await authService.signIn(provider)
3338
}
34-
}) {
35-
HStack {
36-
Image(systemName: "apple.logo")
37-
.resizable()
38-
.renderingMode(.template)
39-
.scaledToFit()
40-
.frame(width: 24, height: 24)
41-
.foregroundColor(.white)
42-
Text("Sign in with Apple")
43-
.fontWeight(.semibold)
44-
.foregroundColor(.white)
45-
}
46-
.frame(maxWidth: .infinity, alignment: .leading)
47-
.padding()
48-
.background(Color.black)
49-
.cornerRadius(8)
5039
}
51-
.accessibilityIdentifier("sign-in-with-apple-button")
5240
}
5341
}

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
}
@@ -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

Comments
 (0)