Skip to content

Commit 0a8514e

Browse files
Merge pull request #1302 from firebase/phone-custom-tap
2 parents b634cba + 380a299 commit 0a8514e

File tree

13 files changed

+173
-22
lines changed

13 files changed

+173
-22
lines changed

FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Views/SignInWithAppleButton.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import SwiftUI
2121
public struct SignInWithAppleButton {
2222
@Environment(AuthService.self) private var authService
2323
@Environment(\.accountConflictHandler) private var accountConflictHandler
24+
@Environment(\.mfaHandler) private var mfaHandler
2425
@Environment(\.reportError) private var reportError
2526
let provider: AppleProviderSwift
2627
public init(provider: AppleProviderSwift) {
@@ -37,7 +38,14 @@ extension SignInWithAppleButton: View {
3738
) {
3839
Task {
3940
do {
40-
_ = try await authService.signIn(provider)
41+
let outcome = try await authService.signIn(provider)
42+
43+
// Handle MFA at view level
44+
if case let .mfaRequired(mfaInfo) = outcome,
45+
let onMFA = mfaHandler {
46+
onMFA(mfaInfo)
47+
return
48+
}
4149
} catch {
4250
reportError?(error)
4351

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ public final class AuthService {
141141
private var listenerManager: AuthListenerManager?
142142

143143
var emailSignInEnabled = false
144+
private var emailSignInCallback: (@MainActor () -> Void)?
144145

145146
private var providers: [AuthProviderUI] = []
146147

@@ -151,12 +152,18 @@ public final class AuthService {
151152
public func renderButtons(spacing: CGFloat = 16) -> AnyView {
152153
AnyView(
153154
VStack(spacing: spacing) {
154-
AuthProviderButton(
155-
label: string.signInWithEmailLinkViewTitle,
156-
style: .email,
157-
accessibilityId: "sign-in-with-email-link-button"
158-
) {
159-
self.navigator.push(.emailLink)
155+
if emailSignInEnabled {
156+
AuthProviderButton(
157+
label: string.signInWithEmailLinkViewTitle,
158+
style: .email,
159+
accessibilityId: "sign-in-with-email-link-button"
160+
) {
161+
if let callback = self.emailSignInCallback {
162+
callback()
163+
} else {
164+
self.navigator.push(.emailLink)
165+
}
166+
}
160167
}
161168
ForEach(providers, id: \.id) { provider in
162169
provider.authButton()
@@ -309,8 +316,17 @@ public extension AuthService {
309316
// MARK: - Email/Password Sign In
310317

311318
public extension AuthService {
319+
/// Enable email sign-in with default behavior (navigates to email link view)
312320
func withEmailSignIn() -> AuthService {
321+
return withEmailSignIn { [weak self] in
322+
self?.navigator.push(.emailLink)
323+
}
324+
}
325+
326+
/// Enable email sign-in with custom callback
327+
func withEmailSignIn(onTap: @escaping @MainActor () -> Void) -> AuthService {
313328
emailSignInEnabled = true
329+
emailSignInCallback = onTap
314330
return self
315331
}
316332

@@ -865,7 +881,6 @@ public extension AuthService {
865881
let hints = extractMFAHints(from: resolver)
866882
currentMFARequired = MFARequired(hints: hints)
867883
currentMFAResolver = resolver
868-
navigator.push(.mfaResolution)
869884
return .mfaRequired(MFARequired(hints: hints))
870885
}
871886

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AccountConflictModifier.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public extension EnvironmentValues {
3232
@MainActor
3333
struct AccountConflictModifier: ViewModifier {
3434
@Environment(AuthService.self) private var authService
35+
@Environment(\.mfaHandler) private var mfaHandler
3536
@Environment(\.reportError) private var reportError
3637
@State private var pendingCredentialForLinking: AuthCredential?
3738

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

5859
// Sign in with the new credential
59-
_ = try await authService.signIn(credentials: conflict.credential)
60+
let outcome = try await authService.signIn(credentials: conflict.credential)
61+
62+
// Handle MFA at view level
63+
if case let .mfaRequired(mfaInfo) = outcome,
64+
let onMFA = mfaHandler {
65+
onMFA(mfaInfo)
66+
}
6067
} catch {
6168
// Report error to parent view for display
6269
reportError?(error)

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ extension AuthPickerView: View {
7676
.interactiveDismissDisabled(authService.configuration.interactiveDismissEnabled)
7777
// Apply account conflict handling at NavigationStack level
7878
.accountConflictHandler()
79+
// Apply MFA handling at NavigationStack level
80+
.mfaHandler()
7981
}
8082
}
8183

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ private enum FocusableField: Hashable {
3333
public struct EmailAuthView {
3434
@Environment(AuthService.self) private var authService
3535
@Environment(\.accountConflictHandler) private var accountConflictHandler
36+
@Environment(\.mfaHandler) private var mfaHandler
3637
@Environment(\.reportError) private var reportError
3738

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

5455
private func signInWithEmailPassword() async throws {
5556
do {
56-
_ = try await authService.signIn(email: email, password: password)
57+
let outcome = try await authService.signIn(email: email, password: password)
58+
59+
// Handle MFA at view level
60+
if case let .mfaRequired(mfaInfo) = outcome,
61+
let onMFA = mfaHandler {
62+
onMFA(mfaInfo)
63+
return
64+
}
5765
} catch {
5866
reportError?(error)
5967

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

7078
private func createUserWithEmailPassword() async throws {
7179
do {
72-
_ = try await authService.createUser(email: email, password: password)
80+
let outcome = try await authService.createUser(email: email, password: password)
81+
82+
// Handle MFA at view level
83+
if case let .mfaRequired(mfaInfo) = outcome,
84+
let onMFA = mfaHandler {
85+
onMFA(mfaInfo)
86+
return
87+
}
7388
} catch {
7489
reportError?(error)
7590

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import FirebaseAuth
16+
import SwiftUI
17+
18+
/// Environment key for accessing the MFA handler
19+
public struct MFAHandlerKey: @preconcurrency EnvironmentKey {
20+
@MainActor public static let defaultValue: ((MFARequired) -> Void)? = nil
21+
}
22+
23+
public extension EnvironmentValues {
24+
var mfaHandler: ((MFARequired) -> Void)? {
25+
get { self[MFAHandlerKey.self] }
26+
set { self[MFAHandlerKey.self] = newValue }
27+
}
28+
}
29+
30+
/// View modifier that handles MFA requirements at the view layer
31+
/// Automatically navigates to MFA resolution when MFA is required
32+
@MainActor
33+
struct MFAHandlerModifier: ViewModifier {
34+
@Environment(AuthService.self) private var authService
35+
36+
func body(content: Content) -> some View {
37+
content
38+
.environment(\.mfaHandler, handleMFARequired)
39+
}
40+
41+
/// Handle MFA required - navigate to MFA resolution view
42+
func handleMFARequired(_: MFARequired) {
43+
authService.navigator.push(.mfaResolution)
44+
}
45+
}
46+
47+
extension View {
48+
/// Adds MFA handling to the view hierarchy
49+
/// Should be applied at the NavigationStack level to handle MFA requirements throughout the auth
50+
/// flow
51+
func mfaHandler() -> some View {
52+
modifier(MFAHandlerModifier())
53+
}
54+
}

FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Views/SignInWithFacebookButton.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import SwiftUI
2323
public struct SignInWithFacebookButton {
2424
@Environment(AuthService.self) private var authService
2525
@Environment(\.accountConflictHandler) private var accountConflictHandler
26+
@Environment(\.mfaHandler) private var mfaHandler
2627
@Environment(\.reportError) private var reportError
2728
let facebookProvider: FacebookProviderSwift
2829

@@ -40,7 +41,14 @@ extension SignInWithFacebookButton: View {
4041
) {
4142
Task {
4243
do {
43-
_ = try await authService.signIn(facebookProvider)
44+
let outcome = try await authService.signIn(facebookProvider)
45+
46+
// Handle MFA at view level
47+
if case let .mfaRequired(mfaInfo) = outcome,
48+
let onMFA = mfaHandler {
49+
onMFA(mfaInfo)
50+
return
51+
}
4452
} catch {
4553
reportError?(error)
4654

FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import SwiftUI
2727
public struct SignInWithGoogleButton {
2828
@Environment(AuthService.self) private var authService
2929
@Environment(\.accountConflictHandler) private var accountConflictHandler
30+
@Environment(\.mfaHandler) private var mfaHandler
3031
@Environment(\.reportError) private var reportError
3132
let googleProvider: GoogleProviderSwift
3233

@@ -44,7 +45,14 @@ extension SignInWithGoogleButton: View {
4445
) {
4546
Task {
4647
do {
47-
_ = try await authService.signIn(googleProvider)
48+
let outcome = try await authService.signIn(googleProvider)
49+
50+
// Handle MFA at view level
51+
if case let .mfaRequired(mfaInfo) = outcome,
52+
let onMFA = mfaHandler {
53+
onMFA(mfaInfo)
54+
return
55+
}
4856
} catch {
4957
reportError?(error)
5058

FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Views/GenericOAuthButton.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import SwiftUI
2121
public struct GenericOAuthButton {
2222
@Environment(AuthService.self) private var authService
2323
@Environment(\.accountConflictHandler) private var accountConflictHandler
24+
@Environment(\.mfaHandler) private var mfaHandler
2425
@Environment(\.reportError) private var reportError
2526
let provider: OAuthProviderSwift
2627
public init(provider: OAuthProviderSwift) {
@@ -47,7 +48,14 @@ extension GenericOAuthButton: View {
4748
) {
4849
Task {
4950
do {
50-
_ = try await authService.signIn(provider)
51+
let outcome = try await authService.signIn(provider)
52+
53+
// Handle MFA at view level
54+
if case let .mfaRequired(mfaInfo) = outcome,
55+
let onMFA = mfaHandler {
56+
onMFA(mfaInfo)
57+
return
58+
}
5159
} catch {
5260
reportError?(error)
5361

FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/AuthService+Phone.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,18 @@
2222
import FirebaseAuthSwiftUI
2323

2424
public extension AuthService {
25+
/// Register phone sign-in with default behavior (navigates to enter phone number view)
2526
@discardableResult
2627
func withPhoneSignIn() -> AuthService {
27-
registerProvider(providerWithButton: PhoneAuthProviderAuthUI())
28+
return withPhoneSignIn { [weak self] in
29+
self?.navigator.push(.enterPhoneNumber)
30+
}
31+
}
32+
33+
/// Register phone sign-in with custom behavior
34+
@discardableResult
35+
func withPhoneSignIn(onTap: @escaping @MainActor () -> Void) -> AuthService {
36+
registerProvider(providerWithButton: PhoneAuthProviderAuthUI(onTap: onTap))
2837
return self
2938
}
3039
}

0 commit comments

Comments
 (0)