Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d698c29
ui changes
demolaf Oct 27, 2025
47b3a60
refactor: use provider styles
demolaf Oct 28, 2025
d5745c3
fix: move limited login to sample app
demolaf Oct 28, 2025
ef278eb
refactor: AuthPickerView layout and styling
demolaf Oct 28, 2025
94f1a36
refactor: use sheet and navigation links
demolaf Oct 29, 2025
4f997f9
refactor: phone auth view and use navigation path instead
demolaf Oct 29, 2025
2ceeb7a
add firebase auth logo
demolaf Oct 29, 2025
61ae6dc
refactor: auto resolve limited login from ATT
demolaf Oct 30, 2025
d192d4e
fix: button for email link auth
demolaf Oct 30, 2025
ce4a146
Merge branch 'development' of https://github.com/firebase/FirebaseUI-…
demolaf Oct 30, 2025
cee4579
fix package.swift errors
demolaf Oct 30, 2025
08eeaf6
Update samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/…
demolaf Oct 31, 2025
3259d7f
format
russellwheatley Oct 31, 2025
6846c4d
fix: remove duplicate back buttons
russellwheatley Oct 31, 2025
ebaeb87
fix: password recovery email button label
russellwheatley Oct 31, 2025
969a9da
fix: descriptive text of what it does
russellwheatley Oct 31, 2025
557a005
chore: add button to open authpickerview
russellwheatley Oct 31, 2025
c6469d7
chore: update SignedInView UI
russellwheatley Oct 31, 2025
c97bcec
chore: remove sign in title when authenticated
russellwheatley Oct 31, 2025
0825807
chore: remove "as"
russellwheatley Oct 31, 2025
c53cfcf
fix: update password view to match UI
russellwheatley Oct 31, 2025
4f6e0f3
fix: MFA management View UI fix
russellwheatley Oct 31, 2025
0e532d0
fix: mfa enrolment View
russellwheatley Oct 31, 2025
ad4a0f6
fix: mfa enrolment phone number
russellwheatley Oct 31, 2025
cf5a6cc
fix: ensure everything can fit on the page
russellwheatley Oct 31, 2025
815d8ca
fix: delete account confirmation sheet
russellwheatley Oct 31, 2025
7285f12
refactor: remove signedInCredential and used persisted tokenresult to…
russellwheatley Oct 31, 2025
ffc8436
refactor: send email view to match UI and remove surplus file
russellwheatley Oct 31, 2025
cd9f261
format
russellwheatley Oct 31, 2025
1550af0
refactor: improve sheet when email link sent
russellwheatley Oct 31, 2025
207f13d
update facebook brand logo
demolaf Nov 1, 2025
54d9b33
add sign in with LINE example
demolaf Nov 3, 2025
4066b13
use @bindable for binding observable objects
demolaf Nov 3, 2025
2a0dc03
chore: update TestView
russellwheatley Nov 3, 2025
669360d
test: fix up UI testa and format
russellwheatley Nov 3, 2025
eb6291a
test: UI runner updates
russellwheatley Nov 3, 2025
dfdbf44
test: fix more tests after UI refactor
russellwheatley Nov 3, 2025
ec83755
format
russellwheatley Nov 3, 2025
fd4d28b
test: mainactor annotations
russellwheatley Nov 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

import FirebaseAuthSwiftUI
import FirebaseAuthUIComponents
import SwiftUI

/// A button for signing in with Apple
Expand All @@ -27,27 +28,14 @@ public struct SignInWithAppleButton {

extension SignInWithAppleButton: View {
public var body: some View {
Button(action: {
AuthProviderButton(
label: "Sign in with Apple",
style: .apple,
accessibilityId: "sign-in-with-apple-button"
) {
Task {
try? await authService.signIn(provider)
}
}) {
HStack {
Image(systemName: "apple.logo")
.resizable()
.renderingMode(.template)
.scaledToFit()
.frame(width: 24, height: 24)
.foregroundColor(.white)
Text("Sign in with Apple")
.fontWeight(.semibold)
.foregroundColor(.white)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.black)
.cornerRadius(8)
}
.accessibilityIdentifier("sign-in-with-apple-button")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

@preconcurrency import FirebaseAuth
import FirebaseAuthUIComponents
import FirebaseCore
import SwiftUI

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

public protocol PhoneAuthProviderSwift: AuthProviderSwift {
@MainActor func verifyPhoneNumber(phoneNumber: String) async throws -> String
func setVerificationCode(verificationID: String, code: String)
}

public enum AuthenticationState {
Expand All @@ -41,14 +43,15 @@ public enum AuthenticationFlow {
case signUp
}

public enum AuthView {
case authPicker
public enum AuthView: Hashable {
case passwordRecovery
case emailLink
case updatePassword
case mfaEnrollment
case mfaManagement
case mfaResolution
case enterPhoneNumber
case enterVerificationCode(verificationID: String, fullPhoneNumber: String)
}

public enum SignInOutcome: @unchecked Sendable {
Expand Down Expand Up @@ -82,6 +85,24 @@ private final class AuthListenerManager {
}
}

@Observable
public class Navigator {
var routes: [AuthView] = []

public func push(_ route: AuthView) {
routes.append(route)
}

@discardableResult
public func pop() -> AuthView? {
routes.popLast()
}

public func clear() {
routes.removeAll()
}
}

@MainActor
@Observable
public final class AuthService {
Expand All @@ -96,7 +117,16 @@ public final class AuthService {
@ObservationIgnored @AppStorage("email-link") public var emailLink: String?
public let configuration: AuthConfiguration
public let auth: Auth
public var authView: AuthView = .authPicker
public var isPresented: Bool = false
public private(set) var navigator = Navigator()
public var authView: AuthView? {
navigator.routes.last
}

var authViewRoutes: [AuthView] {
navigator.routes
}

public let string: StringUtils
public var currentUser: User?
public var authenticationState: AuthenticationState = .unauthenticated
Expand All @@ -105,23 +135,33 @@ public final class AuthService {
public let passwordPrompt: PasswordPromptCoordinator = .init()
public var currentMFARequired: MFARequired?
private var currentMFAResolver: MultiFactorResolver?
private var pendingMFACredential: AuthCredential?

// MARK: - Provider APIs

private var listenerManager: AuthListenerManager?
public var signedInCredential: AuthCredential?

var emailSignInEnabled = false

private var providers: [AuthProviderUI] = []

public var currentPhoneProvider: PhoneAuthProviderSwift? {
providers.compactMap { $0.provider as? PhoneAuthProviderSwift }.first
}

public func registerProvider(providerWithButton: AuthProviderUI) {
providers.append(providerWithButton)
}

public func renderButtons(spacing: CGFloat = 16) -> AnyView {
AnyView(
VStack(spacing: spacing) {
AuthProviderButton(
label: string.signInWithEmailLinkViewTitle,
style: .email,
accessibilityId: "sign-in-with-email-link-button"
) {
self.navigator.push(.emailLink)
}
ForEach(providers, id: \.id) { provider in
provider.authButton()
}
Expand Down Expand Up @@ -209,7 +249,6 @@ public final class AuthService {
}
do {
let result = try await currentUser?.link(with: credentials)
signedInCredential = credentials
updateAuthenticationState()
return .signedIn(result)
} catch let error as NSError {
Expand All @@ -233,7 +272,6 @@ public final class AuthService {
return try await handleAutoUpgradeAnonymousUser(credentials: credentials)
} else {
let result = try await auth.signIn(with: credentials)
signedInCredential = result.credential ?? credentials
updateAuthenticationState()
return .signedIn(result)
}
Expand All @@ -243,8 +281,6 @@ public final class AuthService {
if error.code == AuthErrorCode.secondFactorRequired.rawValue {
if let resolver = error
.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as? MultiFactorResolver {
// Preserve the original credential for use after MFA resolution
pendingMFACredential = credentials
return handleMFARequiredError(resolver: resolver)
}
} else {
Expand Down Expand Up @@ -333,7 +369,6 @@ public extension AuthService {
return try await handleAutoUpgradeAnonymousUser(credentials: credential)
} else {
let result = try await auth.createUser(withEmail: email, password: password)
signedInCredential = result.credential
updateAuthenticationState()
return .signedIn(result)
}
Expand Down Expand Up @@ -710,12 +745,41 @@ public extension AuthService {
}
}

func reauthenticateCurrentUser(on user: User) async throws {
guard let providerId = signedInCredential?.provider else {
throw AuthServiceError
.reauthenticationRequired("Recent login required to perform this operation.")
/// Gets the provider ID that was used for the current sign-in session
private func getCurrentSignInProvider() async throws -> String {
guard let user = currentUser else {
throw AuthServiceError.noCurrentUser
}

// Get the ID token result which contains the signInProvider claim
let tokenResult = try await user.getIDTokenResult(forcingRefresh: false)

// The signInProvider property tells us which provider was used for this session
let signInProvider = tokenResult.signInProvider

// If signInProvider is not empty, use it
if !signInProvider.isEmpty {
return signInProvider
}

// Fallback: if signInProvider is empty, try to infer from providerData
// Prefer non-password providers as they're more specific
let providerId = user.providerData.first(where: { $0.providerID != "password" })?.providerID
?? user.providerData.first?.providerID

guard let providerId = providerId else {
throw AuthServiceError.reauthenticationRequired(
"Unable to determine sign-in provider for reauthentication"
)
}

return providerId
}

func reauthenticateCurrentUser(on user: User) async throws {
// Get the provider from the token instead of stored credential
let providerId = try await getCurrentSignInProvider()

if providerId == EmailAuthProviderID {
guard let email = user.email else {
throw AuthServiceError.invalidCredentials("User does not have an email address")
Expand Down Expand Up @@ -797,7 +861,7 @@ public extension AuthService {
let hints = extractMFAHints(from: resolver)
currentMFARequired = MFARequired(hints: hints)
currentMFAResolver = resolver
authView = .mfaResolution
navigator.push(.mfaResolution)
return .mfaRequired(MFARequired(hints: hints))
}

Expand Down Expand Up @@ -877,16 +941,11 @@ public extension AuthService {

do {
let result = try await resolver.resolveSignIn(with: assertion)

// After MFA resolution, result.credential is nil, so restore the original credential
// that was used before MFA was triggered
signedInCredential = result.credential ?? pendingMFACredential
updateAuthenticationState()

// Clear MFA resolution state
currentMFARequired = nil
currentMFAResolver = nil
pendingMFACredential = nil

} catch {
throw AuthServiceError
Expand Down
Loading
Loading