Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -21,6 +21,7 @@ import SwiftUI
public struct SignInWithAppleButton {
@Environment(AuthService.self) private var authService
@Environment(\.accountConflictHandler) private var accountConflictHandler
@Environment(\.mfaHandler) private var mfaHandler
@Environment(\.reportError) private var reportError
let provider: AppleProviderSwift
public init(provider: AppleProviderSwift) {
Expand All @@ -37,7 +38,14 @@ extension SignInWithAppleButton: View {
) {
Task {
do {
_ = try await authService.signIn(provider)
let outcome = try await authService.signIn(provider)

// Handle MFA at view level
if case let .mfaRequired(mfaInfo) = outcome,
let onMFA = mfaHandler {
onMFA(mfaInfo)
return
}
} catch {
reportError?(error)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ public final class AuthService {
private var listenerManager: AuthListenerManager?

var emailSignInEnabled = false
private var emailSignInCallback: (@MainActor () -> Void)?

private var providers: [AuthProviderUI] = []

Expand All @@ -151,12 +152,18 @@ public final class AuthService {
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)
if emailSignInEnabled {
AuthProviderButton(
label: string.signInWithEmailLinkViewTitle,
style: .email,
accessibilityId: "sign-in-with-email-link-button"
) {
if let callback = self.emailSignInCallback {
callback()
} else {
self.navigator.push(.emailLink)
}
}
}
ForEach(providers, id: \.id) { provider in
provider.authButton()
Expand Down Expand Up @@ -309,8 +316,17 @@ public extension AuthService {
// MARK: - Email/Password Sign In

public extension AuthService {
/// Enable email sign-in with default behavior (navigates to email link view)
func withEmailSignIn() -> AuthService {
return withEmailSignIn { [weak self] in
self?.navigator.push(.emailLink)
}
}

/// Enable email sign-in with custom callback
func withEmailSignIn(onTap: @escaping @MainActor () -> Void) -> AuthService {
emailSignInEnabled = true
emailSignInCallback = onTap
return self
}

Expand Down Expand Up @@ -865,7 +881,6 @@ public extension AuthService {
let hints = extractMFAHints(from: resolver)
currentMFARequired = MFARequired(hints: hints)
currentMFAResolver = resolver
navigator.push(.mfaResolution)
return .mfaRequired(MFARequired(hints: hints))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public extension EnvironmentValues {
@MainActor
struct AccountConflictModifier: ViewModifier {
@Environment(AuthService.self) private var authService
@Environment(\.mfaHandler) private var mfaHandler
@Environment(\.reportError) private var reportError
@State private var pendingCredentialForLinking: AuthCredential?

Expand All @@ -56,7 +57,13 @@ struct AccountConflictModifier: ViewModifier {
try await authService.signOut()

// Sign in with the new credential
_ = try await authService.signIn(credentials: conflict.credential)
let outcome = try await authService.signIn(credentials: conflict.credential)

// Handle MFA at view level
if case let .mfaRequired(mfaInfo) = outcome,
let onMFA = mfaHandler {
onMFA(mfaInfo)
}
} catch {
// Report error to parent view for display
reportError?(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ extension AuthPickerView: View {
.interactiveDismissDisabled(authService.configuration.interactiveDismissEnabled)
// Apply account conflict handling at NavigationStack level
.accountConflictHandler()
// Apply MFA handling at NavigationStack level
.mfaHandler()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ private enum FocusableField: Hashable {
public struct EmailAuthView {
@Environment(AuthService.self) private var authService
@Environment(\.accountConflictHandler) private var accountConflictHandler
@Environment(\.mfaHandler) private var mfaHandler
@Environment(\.reportError) private var reportError

@State private var email = ""
Expand All @@ -53,7 +54,14 @@ public struct EmailAuthView {

private func signInWithEmailPassword() async throws {
do {
_ = try await authService.signIn(email: email, password: password)
let outcome = try await authService.signIn(email: email, password: password)

// Handle MFA at view level
if case let .mfaRequired(mfaInfo) = outcome,
let onMFA = mfaHandler {
onMFA(mfaInfo)
return
}
} catch {
reportError?(error)

Expand All @@ -69,7 +77,14 @@ public struct EmailAuthView {

private func createUserWithEmailPassword() async throws {
do {
_ = try await authService.createUser(email: email, password: password)
let outcome = try await authService.createUser(email: email, password: password)

// Handle MFA at view level
if case let .mfaRequired(mfaInfo) = outcome,
let onMFA = mfaHandler {
onMFA(mfaInfo)
return
}
} catch {
reportError?(error)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import FirebaseAuth
import SwiftUI

/// Environment key for accessing the MFA handler
public struct MFAHandlerKey: @preconcurrency EnvironmentKey {
@MainActor public static let defaultValue: ((MFARequired) -> Void)? = nil
}

public extension EnvironmentValues {
var mfaHandler: ((MFARequired) -> Void)? {
get { self[MFAHandlerKey.self] }
set { self[MFAHandlerKey.self] = newValue }
}
}

/// View modifier that handles MFA requirements at the view layer
/// Automatically navigates to MFA resolution when MFA is required
@MainActor
struct MFAHandlerModifier: ViewModifier {
@Environment(AuthService.self) private var authService

func body(content: Content) -> some View {
content
.environment(\.mfaHandler, handleMFARequired)
}

/// Handle MFA required - navigate to MFA resolution view
func handleMFARequired(_: MFARequired) {
authService.navigator.push(.mfaResolution)
}
}

extension View {
/// Adds MFA handling to the view hierarchy
/// Should be applied at the NavigationStack level to handle MFA requirements throughout the auth
/// flow
func mfaHandler() -> some View {
modifier(MFAHandlerModifier())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import SwiftUI
public struct SignInWithFacebookButton {
@Environment(AuthService.self) private var authService
@Environment(\.accountConflictHandler) private var accountConflictHandler
@Environment(\.mfaHandler) private var mfaHandler
@Environment(\.reportError) private var reportError
let facebookProvider: FacebookProviderSwift

Expand All @@ -40,7 +41,14 @@ extension SignInWithFacebookButton: View {
) {
Task {
do {
_ = try await authService.signIn(facebookProvider)
let outcome = try await authService.signIn(facebookProvider)

// Handle MFA at view level
if case let .mfaRequired(mfaInfo) = outcome,
let onMFA = mfaHandler {
onMFA(mfaInfo)
return
}
} catch {
reportError?(error)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import SwiftUI
public struct SignInWithGoogleButton {
@Environment(AuthService.self) private var authService
@Environment(\.accountConflictHandler) private var accountConflictHandler
@Environment(\.mfaHandler) private var mfaHandler
@Environment(\.reportError) private var reportError
let googleProvider: GoogleProviderSwift

Expand All @@ -44,7 +45,14 @@ extension SignInWithGoogleButton: View {
) {
Task {
do {
_ = try await authService.signIn(googleProvider)
let outcome = try await authService.signIn(googleProvider)

// Handle MFA at view level
if case let .mfaRequired(mfaInfo) = outcome,
let onMFA = mfaHandler {
onMFA(mfaInfo)
return
}
} catch {
reportError?(error)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import SwiftUI
public struct GenericOAuthButton {
@Environment(AuthService.self) private var authService
@Environment(\.accountConflictHandler) private var accountConflictHandler
@Environment(\.mfaHandler) private var mfaHandler
@Environment(\.reportError) private var reportError
let provider: OAuthProviderSwift
public init(provider: OAuthProviderSwift) {
Expand All @@ -47,7 +48,14 @@ extension GenericOAuthButton: View {
) {
Task {
do {
_ = try await authService.signIn(provider)
let outcome = try await authService.signIn(provider)

// Handle MFA at view level
if case let .mfaRequired(mfaInfo) = outcome,
let onMFA = mfaHandler {
onMFA(mfaInfo)
return
}
} catch {
reportError?(error)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,18 @@
import FirebaseAuthSwiftUI

public extension AuthService {
/// Register phone sign-in with default behavior (navigates to enter phone number view)
@discardableResult
func withPhoneSignIn() -> AuthService {
registerProvider(providerWithButton: PhoneAuthProviderAuthUI())
return withPhoneSignIn { [weak self] in
self?.navigator.push(.enterPhoneNumber)
}
}

/// Register phone sign-in with custom behavior
@discardableResult
func withPhoneSignIn(onTap: @escaping @MainActor () -> Void) -> AuthService {
registerProvider(providerWithButton: PhoneAuthProviderAuthUI(onTap: onTap))
return self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ public class PhoneAuthProviderAuthUI: AuthProviderUI {
public var provider: AuthProviderSwift { typedProvider }
public let id: String = "phone"

public init() {
// Callback for when the phone auth button is tapped
private let onTap: @MainActor () -> Void

public init(onTap: @escaping @MainActor () -> Void) {
typedProvider = PhoneProviderSwift()
self.onTap = onTap
}

@MainActor public func authButton() -> AnyView {
AnyView(PhoneAuthButtonView())
AnyView(PhoneAuthButtonView(onTap: onTap))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ import SwiftUI
@MainActor
public struct PhoneAuthButtonView {
@Environment(AuthService.self) private var authService
private let onTap: @MainActor () -> Void

public init() {}
public init(onTap: @escaping @MainActor () -> Void) {
self.onTap = onTap
}
}

extension PhoneAuthButtonView: View {
Expand All @@ -31,13 +34,15 @@ extension PhoneAuthButtonView: View {
style: .phone,
accessibilityId: "sign-in-with-phone-button"
) {
authService.navigator.push(.enterPhoneNumber)
onTap()
}
}
}

#Preview {
FirebaseOptions.dummyConfigurationForPreview()
return PhoneAuthButtonView()
.environment(AuthService())
return PhoneAuthButtonView {
print("Phone auth tapped")
}
.environment(AuthService())
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import SwiftUI
public struct SignInWithTwitterButton {
@Environment(AuthService.self) private var authService
@Environment(\.accountConflictHandler) private var accountConflictHandler
@Environment(\.mfaHandler) private var mfaHandler
@Environment(\.reportError) private var reportError
let provider: TwitterProviderSwift
public init(provider: TwitterProviderSwift) {
Expand All @@ -37,7 +38,14 @@ extension SignInWithTwitterButton: View {
) {
Task {
do {
_ = try await authService.signIn(provider)
let outcome = try await authService.signIn(provider)

// Handle MFA at view level
if case let .mfaRequired(mfaInfo) = outcome,
let onMFA = mfaHandler {
onMFA(mfaInfo)
return
}
} catch {
reportError?(error)

Expand Down
Loading