Skip to content

Commit cff650f

Browse files
Merge branch 'development' into improve-tests
2 parents 78b9905 + 742f176 commit cff650f

24 files changed

Lines changed: 446 additions & 126 deletions

File tree

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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,15 @@ extension AuthPickerView: View {
7575
okButtonLabel: authService.string.okButtonLabel
7676
)
7777
.interactiveDismissDisabled(authService.configuration.interactiveDismissEnabled)
78+
// Apply account conflict handling at NavigationStack level
79+
.accountConflictHandler()
80+
// Apply MFA handling at NavigationStack level
81+
.mfaHandler()
7882
}
7983
// Centralized password prompt sheet to prevent conflicts
8084
.sheet(isPresented: $passwordPrompt.isPromptingPassword) {
8185
PasswordPromptSheet(coordinator: authService.passwordPrompt)
8286
}
83-
// Apply account conflict handling at NavigationStack level
84-
.accountConflictHandler()
8587
}
8688

8789
/// Closure for reporting errors from child views

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift

Lines changed: 35 additions & 6 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 = ""
@@ -45,15 +46,24 @@ public struct EmailAuthView {
4546

4647
private var isValid: Bool {
4748
return if authService.authenticationFlow == .signIn {
48-
!email.isEmpty && !password.isEmpty
49+
FormValidators.email.isValid(input: email) && !password.isEmpty
4950
} else {
50-
!email.isEmpty && !password.isEmpty && password == confirmPassword
51+
FormValidators.email.isValid(input: email) &&
52+
FormValidators.atLeast6Characters.isValid(input: password) &&
53+
FormValidators.confirmPassword(password: password).isValid(input: confirmPassword)
5154
}
5255
}
5356

5457
private func signInWithEmailPassword() async throws {
5558
do {
56-
_ = try await authService.signIn(email: email, password: password)
59+
let outcome = try await authService.signIn(email: email, password: password)
60+
61+
// Handle MFA at view level
62+
if case let .mfaRequired(mfaInfo) = outcome,
63+
let onMFA = mfaHandler {
64+
onMFA(mfaInfo)
65+
return
66+
}
5767
} catch {
5868
reportError?(error)
5969

@@ -69,7 +79,14 @@ public struct EmailAuthView {
6979

7080
private func createUserWithEmailPassword() async throws {
7181
do {
72-
_ = try await authService.createUser(email: email, password: password)
82+
let outcome = try await authService.createUser(email: email, password: password)
83+
84+
// Handle MFA at view level
85+
if case let .mfaRequired(mfaInfo) = outcome,
86+
let onMFA = mfaHandler {
87+
onMFA(mfaInfo)
88+
return
89+
}
7390
} catch {
7491
reportError?(error)
7592

@@ -93,6 +110,10 @@ extension EmailAuthView: View {
93110
prompt: authService.string.emailInputLabel,
94111
keyboardType: .emailAddress,
95112
contentType: .emailAddress,
113+
validations: [
114+
FormValidators.email
115+
],
116+
maintainsValidationMessage: authService.authenticationFlow == .signUp,
96117
onSubmit: { _ in
97118
self.focus = .password
98119
},
@@ -107,7 +128,11 @@ extension EmailAuthView: View {
107128
label: authService.string.passwordFieldLabel,
108129
prompt: authService.string.passwordInputLabel,
109130
contentType: .password,
110-
sensitive: true,
131+
isSecureTextField: true,
132+
validations: authService.authenticationFlow == .signUp ? [
133+
FormValidators.atLeast6Characters
134+
] : [],
135+
maintainsValidationMessage: authService.authenticationFlow == .signUp,
111136
onSubmit: { _ in
112137
Task { try await signInWithEmailPassword() }
113138
},
@@ -134,7 +159,11 @@ extension EmailAuthView: View {
134159
label: authService.string.confirmPasswordFieldLabel,
135160
prompt: authService.string.confirmPasswordInputLabel,
136161
contentType: .password,
137-
sensitive: true,
162+
isSecureTextField: true,
163+
validations: [
164+
FormValidators.confirmPassword(password: password)
165+
],
166+
maintainsValidationMessage: true,
138167
onSubmit: { _ in
139168
Task { try await createUserWithEmailPassword() }
140169
},

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ public struct EmailLinkView {
2222
@Environment(\.accountConflictHandler) private var accountConflictHandler
2323
@Environment(\.reportError) private var reportError
2424
@State private var email = ""
25-
@State private var showModal = false
25+
@State private var showAlert = false
2626

2727
public init() {}
2828

2929
private func sendEmailLink() async throws {
3030
do {
3131
try await authService.sendEmailSignInLink(email: email)
32-
showModal = true
32+
showAlert = true
3333
} catch {
3434
if let errorHandler = reportError {
3535
errorHandler(error)
@@ -49,6 +49,9 @@ extension EmailLinkView: View {
4949
prompt: authService.string.emailInputLabel,
5050
keyboardType: .emailAddress,
5151
contentType: .emailAddress,
52+
validations: [
53+
FormValidators.email
54+
],
5255
leading: {
5356
Image(systemName: "at")
5457
}
@@ -71,24 +74,15 @@ extension EmailLinkView: View {
7174
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
7275
.navigationTitle(authService.string.signInWithEmailLinkViewTitle)
7376
.safeAreaPadding()
74-
.sheet(isPresented: $showModal) {
75-
VStack(spacing: 24) {
76-
Text(authService.string.signInWithEmailLinkViewMessage)
77-
.font(.headline)
78-
Button {
79-
showModal = false
80-
} label: {
81-
Text(authService.string.okButtonLabel)
82-
.padding(.vertical, 8)
83-
.frame(maxWidth: .infinity)
84-
}
85-
.buttonStyle(.borderedProminent)
86-
.padding([.top, .bottom], 8)
87-
.frame(maxWidth: .infinity)
77+
.alert(
78+
authService.string.signInWithEmailLinkViewTitle,
79+
isPresented: $showAlert
80+
) {
81+
Button(authService.string.okButtonLabel) {
82+
showAlert = false
8883
}
89-
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
90-
.safeAreaPadding()
91-
.presentationDetents([.medium])
84+
} message: {
85+
Text(authService.string.signInWithEmailLinkViewMessage)
9286
}
9387
.onOpenURL { url in
9488
Task {

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterPhoneNumberView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ struct EnterPhoneNumberView: View {
3838
prompt: authService.string.enterPhoneNumberPlaceholder,
3939
keyboardType: .phonePad,
4040
contentType: .telephoneNumber,
41+
validations: [
42+
FormValidators.phoneNumber
43+
],
4144
onChange: { _ in }
4245
) {
4346
CountrySelector(

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterVerificationCodeView.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,13 @@ struct EnterVerificationCodeView: View {
4949
.padding(.bottom)
5050
.frame(maxWidth: .infinity, alignment: .leading)
5151

52-
VerificationCodeInputField(code: $verificationCode)
52+
VerificationCodeInputField(
53+
code: $verificationCode,
54+
validations: [
55+
FormValidators.verificationCode
56+
],
57+
maintainsValidationMessage: true
58+
)
5359

5460
Button(action: {
5561
Task {

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAEnrolmentView.swift

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,10 @@ extension MFAEnrolmentView: View {
374374
prompt: authService.string.enterPhoneNumberPrompt,
375375
keyboardType: .phonePad,
376376
contentType: .telephoneNumber,
377+
validations: [
378+
FormValidators.phoneNumber
379+
],
380+
maintainsValidationMessage: true,
377381
onChange: { _ in }
378382
) {
379383
CountrySelector(
@@ -388,6 +392,10 @@ extension MFAEnrolmentView: View {
388392
text: $displayName,
389393
label: authService.string.displayNameFieldLabel,
390394
prompt: authService.string.enterDisplayNameForDevicePrompt,
395+
validations: [
396+
FormValidators.notEmpty(label: "Display name")
397+
],
398+
maintainsValidationMessage: true,
391399
leading: {
392400
Image(systemName: "person")
393401
}
@@ -430,17 +438,13 @@ extension MFAEnrolmentView: View {
430438
.multilineTextAlignment(.center)
431439
}
432440

433-
AuthTextField(
434-
text: $verificationCode,
435-
label: authService.string.verificationCodeFieldLabel,
436-
prompt: "Enter 6-digit code",
437-
keyboardType: .numberPad,
438-
contentType: .oneTimeCode,
439-
leading: {
440-
Image(systemName: "number")
441-
}
441+
VerificationCodeInputField(
442+
code: $verificationCode,
443+
validations: [
444+
FormValidators.verificationCode
445+
],
446+
maintainsValidationMessage: true
442447
)
443-
.focused($focus, equals: .verificationCode)
444448
.accessibilityIdentifier("verification-code-field")
445449

446450
Button {
@@ -579,23 +583,23 @@ extension MFAEnrolmentView: View {
579583
text: $displayName,
580584
label: authService.string.displayNameFieldLabel,
581585
prompt: authService.string.enterDisplayNameForAuthenticatorPrompt,
586+
validations: [
587+
FormValidators.notEmpty(label: "Display name")
588+
],
589+
maintainsValidationMessage: true,
582590
leading: {
583591
Image(systemName: "person")
584592
}
585593
)
586594
.accessibilityIdentifier("display-name-field")
587595

588-
AuthTextField(
589-
text: $totpCode,
590-
label: authService.string.verificationCodeFieldLabel,
591-
prompt: authService.string.enterCodeFromAppPrompt,
592-
keyboardType: .numberPad,
593-
contentType: .oneTimeCode,
594-
leading: {
595-
Image(systemName: "number")
596-
}
596+
VerificationCodeInputField(
597+
code: $totpCode,
598+
validations: [
599+
FormValidators.verificationCode
600+
],
601+
maintainsValidationMessage: true
597602
)
598-
.focused($focus, equals: .totpCode)
599603
.accessibilityIdentifier("totp-code-field")
600604

601605
Button {

0 commit comments

Comments
 (0)