From 7225dd79cfd836ffb298ef5d8f2d3a4657150a42 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 10 Nov 2025 09:10:03 +0000 Subject: [PATCH 1/9] fix: facebook method now only works in obj-c code --- .../Sources/Services/FacebookProviderAuthUI.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift index 13f371d1c2..97abaa4c0f 100644 --- a/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift @@ -105,7 +105,7 @@ public class FacebookProviderSwift: CredentialAuthProviderSwift { "`rawNonce` has not been generated for Facebook limited login" ) } - let credential = OAuthProvider.credential(withProviderID: providerId, + let credential = OAuthProvider.credential(providerID: .facebook, idToken: idToken.tokenString, rawNonce: nonce) return credential From 0891657867e2ea034414776e873ad3cb769bedc7 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 10 Nov 2025 09:10:08 +0000 Subject: [PATCH 2/9] format --- .../Sources/Views/EmailAuthView.swift | 4 ++-- .../Sources/Views/EmailLinkView.swift | 2 +- .../Sources/Views/EnterPhoneNumberView.swift | 2 +- .../Sources/Views/EnterVerificationCodeView.swift | 2 +- .../Sources/Views/MFAEnrolmentView.swift | 10 +++++----- .../Sources/Views/PasswordRecoveryView.swift | 2 +- .../Sources/Views/UpdatePasswordView.swift | 8 +++----- .../Sources/Components/AuthTextField.swift | 11 ++++++----- .../Components/VerificationCodeInputField.swift | 3 ++- .../Sources/Validation/FormValidator.swift | 3 ++- 10 files changed, 24 insertions(+), 23 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift index 35608ca6fb..b0c88a2ab3 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift @@ -111,7 +111,7 @@ extension EmailAuthView: View { keyboardType: .emailAddress, contentType: .emailAddress, validations: [ - FormValidators.email + FormValidators.email, ], maintainsValidationMessage: authService.authenticationFlow == .signUp, onSubmit: { _ in @@ -161,7 +161,7 @@ extension EmailAuthView: View { contentType: .password, isSecureTextField: true, validations: [ - FormValidators.confirmPassword(password: password) + FormValidators.confirmPassword(password: password), ], maintainsValidationMessage: true, onSubmit: { _ in diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift index 2bcf12969f..2eca4ec7c5 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift @@ -50,7 +50,7 @@ extension EmailLinkView: View { keyboardType: .emailAddress, contentType: .emailAddress, validations: [ - FormValidators.email + FormValidators.email, ], leading: { Image(systemName: "at") diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterPhoneNumberView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterPhoneNumberView.swift index 27189e5994..95152335ca 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterPhoneNumberView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterPhoneNumberView.swift @@ -39,7 +39,7 @@ struct EnterPhoneNumberView: View { keyboardType: .phonePad, contentType: .telephoneNumber, validations: [ - FormValidators.phoneNumber + FormValidators.phoneNumber, ], onChange: { _ in } ) { diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterVerificationCodeView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterVerificationCodeView.swift index 88adc6135d..5d0be1a6e2 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterVerificationCodeView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterVerificationCodeView.swift @@ -52,7 +52,7 @@ struct EnterVerificationCodeView: View { VerificationCodeInputField( code: $verificationCode, validations: [ - FormValidators.verificationCode + FormValidators.verificationCode, ], maintainsValidationMessage: true ) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAEnrolmentView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAEnrolmentView.swift index f195edabb7..64a2399805 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAEnrolmentView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAEnrolmentView.swift @@ -375,7 +375,7 @@ extension MFAEnrolmentView: View { keyboardType: .phonePad, contentType: .telephoneNumber, validations: [ - FormValidators.phoneNumber + FormValidators.phoneNumber, ], maintainsValidationMessage: true, onChange: { _ in } @@ -393,7 +393,7 @@ extension MFAEnrolmentView: View { label: authService.string.displayNameFieldLabel, prompt: authService.string.enterDisplayNameForDevicePrompt, validations: [ - FormValidators.notEmpty(label: "Display name") + FormValidators.notEmpty(label: "Display name"), ], maintainsValidationMessage: true, leading: { @@ -441,7 +441,7 @@ extension MFAEnrolmentView: View { VerificationCodeInputField( code: $verificationCode, validations: [ - FormValidators.verificationCode + FormValidators.verificationCode, ], maintainsValidationMessage: true ) @@ -584,7 +584,7 @@ extension MFAEnrolmentView: View { label: authService.string.displayNameFieldLabel, prompt: authService.string.enterDisplayNameForAuthenticatorPrompt, validations: [ - FormValidators.notEmpty(label: "Display name") + FormValidators.notEmpty(label: "Display name"), ], maintainsValidationMessage: true, leading: { @@ -596,7 +596,7 @@ extension MFAEnrolmentView: View { VerificationCodeInputField( code: $totpCode, validations: [ - FormValidators.verificationCode + FormValidators.verificationCode, ], maintainsValidationMessage: true ) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift index b8b1692f1c..8b3a496d43 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift @@ -45,7 +45,7 @@ extension PasswordRecoveryView: View { keyboardType: .emailAddress, contentType: .emailAddress, validations: [ - FormValidators.email + FormValidators.email, ], leading: { Image(systemName: "at") diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift index 474e041567..d6ff5eea26 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift @@ -46,9 +46,7 @@ public struct UpdatePasswordView { do { try await authService.updatePassword(to: confirmPassword) showAlert = true - } catch { - - } + } catch {} } } } @@ -64,7 +62,7 @@ extension UpdatePasswordView: View { contentType: .password, isSecureTextField: true, validations: [ - FormValidators.atLeast6Characters + FormValidators.atLeast6Characters, ], maintainsValidationMessage: true, leading: { @@ -81,7 +79,7 @@ extension UpdatePasswordView: View { contentType: .password, isSecureTextField: true, validations: [ - FormValidators.confirmPassword(password: password) + FormValidators.confirmPassword(password: password), ], maintainsValidationMessage: true, leading: { diff --git a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/AuthTextField.swift b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/AuthTextField.swift index 5978fa6624..a60df7dee6 100644 --- a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/AuthTextField.swift +++ b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/AuthTextField.swift @@ -18,7 +18,7 @@ public struct AuthTextField: View { @FocusState private var isFocused: Bool @State var obscured: Bool = true @State var hasInteracted: Bool = false - + @Binding var text: String let label: String let prompt: String @@ -32,7 +32,7 @@ public struct AuthTextField: View { var onSubmit: ((String) -> Void)? = nil var onChange: ((String) -> Void)? = nil private let leading: () -> Leading? - + public init(text: Binding, label: String, prompt: String, @@ -60,11 +60,11 @@ public struct AuthTextField: View { self.onChange = onChange self.leading = leading } - + var allRequirementsMet: Bool { validations.allSatisfy { $0.isValid(input: text) } } - + public var body: some View { VStack(alignment: .leading) { Text(LocalizedStringResource(stringLiteral: label)) @@ -142,7 +142,8 @@ public struct AuthTextField: View { isFocused = true } } - if !validations.isEmpty && hasInteracted && (maintainsValidationMessage || !allRequirementsMet) { + if !validations + .isEmpty && hasInteracted && (maintainsValidationMessage || !allRequirementsMet) { VStack(alignment: .leading, spacing: 4) { ForEach(validations) { validator in let isValid = validator.isValid(input: text) diff --git a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/VerificationCodeInputField.swift b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/VerificationCodeInputField.swift index e8b6929a05..d57041775e 100644 --- a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/VerificationCodeInputField.swift +++ b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/VerificationCodeInputField.swift @@ -96,7 +96,8 @@ public struct VerificationCodeInputField: View { .frame(maxWidth: .infinity, alignment: .leading) } - if !validations.isEmpty && hasInteracted && (maintainsValidationMessage || !allRequirementsMet) { + if !validations + .isEmpty && hasInteracted && (maintainsValidationMessage || !allRequirementsMet) { VStack(alignment: .leading, spacing: 4) { ForEach(validations) { validator in let isValid = validator.isValid(input: code) diff --git a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Validation/FormValidator.swift b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Validation/FormValidator.swift index de0bdd61ec..9326fc76d9 100644 --- a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Validation/FormValidator.swift +++ b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Validation/FormValidator.swift @@ -41,7 +41,8 @@ public struct FormValidators { } ) - public static func confirmPassword(password: @autoclosure @escaping () -> String) -> FormValidator { + public static func confirmPassword(password: @autoclosure @escaping () -> String) + -> FormValidator { return FormValidator( message: "Passwords must match", validate: { input in From de5b9505452c4973a2c4bbc99a9e2dd3c3007cf9 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 10 Nov 2025 09:22:43 +0000 Subject: [PATCH 3/9] fix: increase button width --- .../FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift index 5bdf1f221a..c65da21167 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift @@ -156,7 +156,7 @@ extension AuthPickerView: View { VStack { authService.renderButtons() } - .padding(.horizontal, proxy.size.width * 0.18) + .padding(.horizontal, proxy.size.width * 0.14) } } From 35fae50f773c05f57d06c14635224042c6f688ee Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 10 Nov 2025 09:58:28 +0000 Subject: [PATCH 4/9] fix: ensure errors are reported from MFA Views --- .../Sources/Views/MFAEnrolmentView.swift | 83 +++++++++++-------- .../Sources/Views/MFAManagementView.swift | 2 + .../Sources/Views/MFAResolutionView.swift | 3 + 3 files changed, 53 insertions(+), 35 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAEnrolmentView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAEnrolmentView.swift index 64a2399805..6f11974503 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAEnrolmentView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAEnrolmentView.swift @@ -26,6 +26,7 @@ private enum FocusableField: Hashable { @MainActor public struct MFAEnrolmentView { @Environment(AuthService.self) private var authService + @Environment(\.reportError) private var reportError @State private var selectedFactorType: SecondFactorType = .sms @State private var phoneNumber = "" @@ -81,12 +82,16 @@ public struct MFAEnrolmentView { isLoading = true defer { isLoading = false } - let session = try await authService.startMfaEnrollment( - type: selectedFactorType, - accountName: authService.currentUser?.email, - issuer: authService.configuration.mfaIssuer - ) - currentSession = session + do { + let session = try await authService.startMfaEnrollment( + type: selectedFactorType, + accountName: authService.currentUser?.email, + issuer: authService.configuration.mfaIssuer + ) + currentSession = session + } catch { + reportError?(error) + } } } @@ -97,23 +102,27 @@ public struct MFAEnrolmentView { isLoading = true defer { isLoading = false } - let fullPhoneNumber = selectedCountry.dialCode + phoneNumber - let verificationId = try await authService.sendSmsVerificationForEnrollment( - session: session, - phoneNumber: fullPhoneNumber - ) - // Update session status - currentSession = EnrollmentSession( - id: session.id, - type: session.type, - session: session.session, - totpInfo: session.totpInfo, - phoneNumber: fullPhoneNumber, - verificationId: verificationId, - status: .verificationSent, - createdAt: session.createdAt, - expiresAt: session.expiresAt - ) + do { + let fullPhoneNumber = selectedCountry.dialCode + phoneNumber + let verificationId = try await authService.sendSmsVerificationForEnrollment( + session: session, + phoneNumber: fullPhoneNumber + ) + // Update session status + currentSession = EnrollmentSession( + id: session.id, + type: session.type, + session: session.session, + totpInfo: session.totpInfo, + phoneNumber: fullPhoneNumber, + verificationId: verificationId, + status: .verificationSent, + createdAt: session.createdAt, + expiresAt: session.expiresAt + ) + } catch { + reportError?(error) + } } } @@ -124,18 +133,22 @@ public struct MFAEnrolmentView { isLoading = true defer { isLoading = false } - let code = session.type == .sms ? verificationCode : totpCode - try await authService.completeEnrollment( - session: session, - verificationId: session.verificationId, - verificationCode: code, - displayName: displayName - ) - - // Reset form state on success - resetForm() - - authService.navigator.clear() + do { + let code = session.type == .sms ? verificationCode : totpCode + try await authService.completeEnrollment( + session: session, + verificationId: session.verificationId, + verificationCode: code, + displayName: displayName + ) + + // Reset form state on success + resetForm() + + authService.navigator.clear() + } catch { + reportError?(error) + } } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAManagementView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAManagementView.swift index fcbed901a8..ec5bf668dd 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAManagementView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAManagementView.swift @@ -23,6 +23,7 @@ extension MultiFactorInfo: Identifiable { @MainActor public struct MFAManagementView { @Environment(AuthService.self) private var authService + @Environment(\.reportError) private var reportError @State private var enrolledFactors: [MultiFactorInfo] = [] @State private var isLoading = false @@ -43,6 +44,7 @@ public struct MFAManagementView { enrolledFactors = freshFactors isLoading = false } catch { + reportError?(error) isLoading = false } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAResolutionView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAResolutionView.swift index a5efb32640..03e56a1e98 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAResolutionView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAResolutionView.swift @@ -24,6 +24,7 @@ private enum FocusableField: Hashable { @MainActor public struct MFAResolutionView { @Environment(AuthService.self) private var authService + @Environment(\.reportError) private var reportError @State private var verificationCode = "" @State private var totpCode = "" @@ -72,6 +73,7 @@ public struct MFAResolutionView { self.verificationId = verificationId isLoading = false } catch { + reportError?(error) isLoading = false } } @@ -93,6 +95,7 @@ public struct MFAResolutionView { authService.navigator.clear() isLoading = false } catch { + reportError?(error) isLoading = false } } From 69c3a0cd2434cc9b0e7c3adbfe74482c9854ee65 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 10 Nov 2025 12:00:04 +0000 Subject: [PATCH 5/9] test: fix tests to match latest UI --- .../FirebaseSwiftUIExample/TestView.swift | 2 - .../MFAEnrolmentUITests.swift | 27 ++++++-- .../TestUtils.swift | 69 +++++++++++++------ 3 files changed, 68 insertions(+), 30 deletions(-) diff --git a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift index 6a125f75fe..44a229ebc5 100644 --- a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift +++ b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExample/TestView.swift @@ -34,8 +34,6 @@ struct TestView: View { init() { Auth.auth().useEmulator(withHost: "localhost", port: 9099) - - Auth.auth().settings?.isAppVerificationDisabledForTesting = true Task { try signOut() } diff --git a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift index 1b8c981f03..a1de997d4e 100644 --- a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift +++ b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift @@ -196,7 +196,7 @@ final class MFAEnrollmentUITests: XCTestCase { // 6) Select UK country code and enter phone number (without dial code) // Find and tap the country selector - try multiple approaches since it's embedded in the // TextField - let countrySelector = app.buttons["🇺🇸 +1"] + let countrySelector = app.buttons["phone-number-field"].firstMatch XCTAssertTrue(countrySelector.waitForExistence(timeout: 5)) countrySelector.tap() @@ -229,18 +229,31 @@ final class MFAEnrollmentUITests: XCTestCase { sendCodeButton.tap() // 7) Retrieve verification code from the Auth Emulator and complete setup - let verificationCodeField = app.textFields["verification-code-field"] - XCTAssertTrue(verificationCodeField.waitForExistence(timeout: 15)) + let verificationCodeField1 = app.otherElements["Digit 1 of 6"].textFields.firstMatch + let verificationCodeField2 = app.otherElements["Digit 2 of 6"].textFields.firstMatch + let verificationCodeField3 = app.otherElements["Digit 3 of 6"].textFields.firstMatch + let verificationCodeField4 = app.otherElements["Digit 4 of 6"].textFields.firstMatch + let verificationCodeField5 = app.otherElements["Digit 5 of 6"].textFields.firstMatch + let verificationCodeField6 = app.otherElements["Digit 6 of 6"].textFields.firstMatch + XCTAssertTrue(verificationCodeField1.waitForExistence(timeout: 15)) // Fetch the latest SMS verification code generated by the emulator for this phone number // The emulator stores the full phone number with dial code let fullPhoneNumber = "+44\(phoneNumberWithoutDialCode)" let code = try await getLastSmsCode(specificPhone: fullPhoneNumber) - UIPasteboard.general.string = code - verificationCodeField.tap() - verificationCodeField.press(forDuration: 1.2) - app.menuItems["Paste"].tap() + // Paste each digit into the corresponding text field + let codeDigits = Array(code) + let fields = [verificationCodeField1, verificationCodeField2, verificationCodeField3, + verificationCodeField4, verificationCodeField5, verificationCodeField6] + + for (index, digit) in codeDigits.enumerated() where index < fields.count { + let field = fields[index] + UIPasteboard.general.string = String(digit) + field.tap() + field.press(forDuration: 1.2) + app.menuItems["Paste"].tap() + } // Test resend code button exists let resendButton = app.buttons["resend-code-button"] diff --git a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/TestUtils.swift b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/TestUtils.swift index 9a8c177ee0..539d76a090 100644 --- a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/TestUtils.swift +++ b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/TestUtils.swift @@ -96,20 +96,17 @@ func createEmail() -> String { "idToken": idToken, ]) - let (_, sendResp) = try await URLSession.shared.data(for: sendReq) + let (sendData, sendResp) = try await URLSession.shared.data(for: sendReq) guard let http = sendResp as? HTTPURLResponse, http.statusCode == 200 else { + let errorBody = String(data: sendData, encoding: .utf8) ?? "Unknown error" throw NSError(domain: "EmulatorError", code: 1, - userInfo: [NSLocalizedDescriptionKey: "Failed to send verification email"]) + userInfo: [NSLocalizedDescriptionKey: "Failed to send verification email: \(errorBody)"]) } - // Step 2: Fetch OOB codes from emulator - let oobURL = URL(string: "\(base)/emulator/v1/projects/\(projectID)/oobCodes")! - let (oobData, oobResp) = try await URLSession.shared.data(from: oobURL) - guard (oobResp as? HTTPURLResponse)?.statusCode == 200 else { - throw NSError(domain: "EmulatorError", code: 2, - userInfo: [NSLocalizedDescriptionKey: "Failed to fetch OOB codes"]) - } + // Add a small delay to ensure the OOB code is registered in the emulator + try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds + // Define structs for OOB response parsing struct OobEnvelope: Decodable { let oobCodes: [OobItem] } struct OobItem: Decodable { let oobCode: String @@ -118,20 +115,50 @@ func createEmail() -> String { let creationTime: String? } - let envelope = try JSONDecoder().decode(OobEnvelope.self, from: oobData) - - // Step 3: Find most recent VERIFY_EMAIL code for this email - let iso = ISO8601DateFormatter() - let codeItem = envelope.oobCodes - .filter { - $0.email.caseInsensitiveCompare(email) == .orderedSame && $0.requestType == "VERIFY_EMAIL" + // Step 2: Fetch OOB codes from emulator with retry logic + let oobURL = URL(string: "\(base)/emulator/v1/projects/\(projectID)/oobCodes")! + + var codeItem: OobItem? + var attempts = 0 + let maxAttempts = 5 + + while codeItem == nil && attempts < maxAttempts { + let (oobData, oobResp) = try await URLSession.shared.data(from: oobURL) + guard (oobResp as? HTTPURLResponse)?.statusCode == 200 else { + throw NSError(domain: "EmulatorError", code: 2, + userInfo: [NSLocalizedDescriptionKey: "Failed to fetch OOB codes"]) } - .sorted { - let d0 = $0.creationTime.flatMap { iso.date(from: $0) } ?? .distantPast - let d1 = $1.creationTime.flatMap { iso.date(from: $0) } ?? .distantPast - return d0 > d1 + + let envelope = try JSONDecoder().decode(OobEnvelope.self, from: oobData) + + // Step 3: Find most recent VERIFY_EMAIL code for this email + let iso = ISO8601DateFormatter() + codeItem = envelope.oobCodes + .filter { + $0.email.caseInsensitiveCompare(email) == .orderedSame && $0.requestType == "VERIFY_EMAIL" + } + .sorted { + let d0 = $0.creationTime.flatMap { iso.date(from: $0) } ?? .distantPast + let d1 = $1.creationTime.flatMap { iso.date(from: $0) } ?? .distantPast + return d0 > d1 + } + .first + + if codeItem == nil { + attempts += 1 + if attempts < maxAttempts { + // Wait before retrying + try await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds + } else { + // Log available codes for debugging + let availableCodes = envelope.oobCodes.map { "Email: \($0.email), Type: \($0.requestType)" }.joined(separator: "; ") + throw NSError(domain: "EmulatorError", code: 3, + userInfo: [ + NSLocalizedDescriptionKey: "No VERIFY_EMAIL OOB code found for \(email) after \(maxAttempts) attempts. Available codes: \(availableCodes)", + ]) + } } - .first + } guard let oobCode = codeItem?.oobCode else { throw NSError(domain: "EmulatorError", code: 3, From 241d7cb42fb99f7d726f17727f50cf301a334b71 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 10 Nov 2025 13:10:43 +0000 Subject: [PATCH 6/9] test: use proper teardown for enrolment tests --- .../MFAEnrolmentUITests.swift | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift index a1de997d4e..f39a20160b 100644 --- a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift +++ b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift @@ -22,9 +22,24 @@ import XCTest final class MFAEnrollmentUITests: XCTestCase { + var app: XCUIApplication! + override func setUpWithError() throws { continueAfterFailure = false } + + override func tearDownWithError() throws { + // Clean up: Terminate app + if let app = app { + app.terminate() + } + app = nil + + // Small delay between tests to allow emulator to settle + Thread.sleep(forTimeInterval: 0.5) + + try super.tearDownWithError() + } // MARK: - MFA Management Navigation Tests @@ -35,7 +50,7 @@ final class MFAEnrollmentUITests: XCTestCase { // Create user in test runner before launching app try await createTestUser(email: email) - let app = createTestApp(mfaEnabled: true) + app = createTestApp(mfaEnabled: true) app.launch() // Sign in first to access MFA management @@ -67,7 +82,7 @@ final class MFAEnrollmentUITests: XCTestCase { // Create user in test runner before launching app try await createTestUser(email: email) - let app = createTestApp(mfaEnabled: true) + app = createTestApp(mfaEnabled: true) app.launch() // Sign in and navigate to MFA management @@ -102,7 +117,7 @@ final class MFAEnrollmentUITests: XCTestCase { // Create user in test runner before launching app try await createTestUser(email: email) - let app = createTestApp(mfaEnabled: true) + app = createTestApp(mfaEnabled: true) app.launch() // Navigate to MFA enrollment @@ -131,7 +146,7 @@ final class MFAEnrollmentUITests: XCTestCase { // Create user in test runner before launching app try await createTestUser(email: email) - let app = createTestApp(mfaEnabled: true) + app = createTestApp(mfaEnabled: true) app.launch() // Navigate to MFA enrollment @@ -168,7 +183,7 @@ final class MFAEnrollmentUITests: XCTestCase { let email = createEmail() try await createTestUser(email: email, verifyEmail: true) - let app = createTestApp(mfaEnabled: true) + app = createTestApp(mfaEnabled: true) app.launch() // 2) Sign in to reach SignedInView @@ -210,7 +225,9 @@ final class MFAEnrollmentUITests: XCTestCase { // Enter phone number (without dial code) let phoneField = app.textFields["phone-number-field"] XCTAssertTrue(phoneField.waitForExistence(timeout: 10)) - let phoneNumberWithoutDialCode = "7444555666" + // Generate unique phone number using timestamp to avoid conflicts between tests + let uniqueId = Int(Date().timeIntervalSince1970 * 1000) % 1000000 + let phoneNumberWithoutDialCode = "7\(String(format: "%09d", uniqueId))" UIPasteboard.general.string = phoneNumberWithoutDialCode phoneField.tap() phoneField.press(forDuration: 1.2) @@ -298,7 +315,7 @@ final class MFAEnrollmentUITests: XCTestCase { // Create user in test runner before launching app (with email verification) try await createTestUser(email: email, verifyEmail: true) - let app = createTestApp(mfaEnabled: true) + app = createTestApp(mfaEnabled: true) app.launch() // Navigate to MFA enrollment and select TOTP @@ -357,7 +374,7 @@ final class MFAEnrollmentUITests: XCTestCase { // Create user in test runner before launching app try await createTestUser(email: email) - let app = createTestApp(mfaEnabled: true) + app = createTestApp(mfaEnabled: true) app.launch() // Navigate to MFA enrollment @@ -377,7 +394,7 @@ final class MFAEnrollmentUITests: XCTestCase { // Create user in test runner before launching app try await createTestUser(email: email) - let app = createTestApp(mfaEnabled: true) + app = createTestApp(mfaEnabled: true) app.launch() // Navigate to MFA enrollment From b256f55a65228bba54a95d3b328330c8fe66d0cb Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 10 Nov 2025 13:10:54 +0000 Subject: [PATCH 7/9] test: run enrolment tests serially --- .../xcschemes/FirebaseSwiftUIExample.xcscheme | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/xcshareddata/xcschemes/FirebaseSwiftUIExample.xcscheme b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/xcshareddata/xcschemes/FirebaseSwiftUIExample.xcscheme index 30faacec20..6ee478a6b1 100644 --- a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/xcshareddata/xcschemes/FirebaseSwiftUIExample.xcscheme +++ b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/xcshareddata/xcschemes/FirebaseSwiftUIExample.xcscheme @@ -28,7 +28,25 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" - shouldAutocreateTestPlan = "YES"> + shouldAutocreateTestPlan = "YES" + codeCoverageEnabled = "NO" + onlyGenerateCoverageForSpecifiedTargets = "NO"> + + + + + + + + Date: Mon, 10 Nov 2025 13:47:23 +0000 Subject: [PATCH 8/9] test: remove parallel testing to stop hanging --- .github/workflows/swiftui-auth.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/swiftui-auth.yml b/.github/workflows/swiftui-auth.yml index a9fef9d711..abf108ebb1 100644 --- a/.github/workflows/swiftui-auth.yml +++ b/.github/workflows/swiftui-auth.yml @@ -176,8 +176,7 @@ jobs: xcodebuild test-without-building \ -scheme FirebaseSwiftUIExampleUITests \ -destination 'platform=iOS Simulator,name=iPhone 17' \ - -parallel-testing-enabled YES \ - -maximum-concurrent-test-simulator-destinations 2 \ + -parallel-testing-enabled NO \ -enableCodeCoverage YES \ -resultBundlePath FirebaseSwiftUIExampleUITests.xcresult | tee FirebaseSwiftUIExampleUITests.log | xcpretty --test --color --simple From 16f95eb184f605cdd865f823e9842184ae29c8a6 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 10 Nov 2025 14:22:14 +0000 Subject: [PATCH 9/9] test: just wait for signed-in View --- .../FirebaseSwiftUIExampleUITests.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift index b824d7d1bc..3e00a1d1ad 100644 --- a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift +++ b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift @@ -175,15 +175,6 @@ final class FirebaseSwiftUIExampleUITests: XCTestCase { XCTAssertTrue(signUpButton.exists, "Sign-Up button should exist") signUpButton.tap() - // Wait for the auth screen to disappear (email field should no longer exist) - let emailFieldDisappeared = NSPredicate(format: "exists == false") - let expectation = XCTNSPredicateExpectation( - predicate: emailFieldDisappeared, - object: emailField - ) - let result = XCTWaiter().wait(for: [expectation], timeout: 10.0) - XCTAssertEqual(result, .completed, "Email field should disappear after sign-up") - // Wait for user creation and signed-in view to appear let signedInText = app.staticTexts["signed-in-text"] XCTAssertTrue(