diff --git a/.github/workflows/swiftui-auth.yml b/.github/workflows/swiftui-auth.yml index abf108ebb1..b19fcba95d 100644 --- a/.github/workflows/swiftui-auth.yml +++ b/.github/workflows/swiftui-auth.yml @@ -24,6 +24,19 @@ permissions: contents: read jobs: + format-check: + name: Swift Format Check + runs-on: macos-26 + timeout-minutes: 10 + steps: + - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 + + - name: Install swiftformat + run: brew install swiftformat + + - name: Check Swift formatting + run: bash lint-swift.sh + # Package Unit Tests (standalone, no emulator needed) unit-tests: name: Package Unit Tests @@ -192,4 +205,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: FirebaseSwiftUIExampleUITests.xcresult - path: e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.xcresult \ No newline at end of file + path: e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.xcresult diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift index c65da21167..fbdc965238 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift @@ -33,6 +33,7 @@ public struct AuthPickerView { extension AuthPickerView: View { public var body: some View { @Bindable var authService = authService + @Bindable var passwordPrompt = authService.passwordPrompt content() .sheet(isPresented: $authService.isPresented) { @Bindable var navigator = authService.navigator @@ -79,6 +80,10 @@ extension AuthPickerView: View { // Apply MFA handling at NavigationStack level .mfaHandler() } + // Centralized password prompt sheet to prevent conflicts + .sheet(isPresented: $passwordPrompt.isPromptingPassword) { + PasswordPromptSheet(coordinator: authService.passwordPrompt) + } } /// Closure for reporting errors from child views diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterVerificationCodeView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterVerificationCodeView.swift index 5d0be1a6e2..74713990ee 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterVerificationCodeView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterVerificationCodeView.swift @@ -106,7 +106,7 @@ struct EnterVerificationCodeView: View { return NavigationStack { EnterVerificationCodeView( verificationID: "mock-id", - fullPhoneNumber: "+1 5551234567", + fullPhoneNumber: "+1 5551234567" ) .environment(AuthService()) } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAManagementView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAManagementView.swift index ec5bf668dd..0614373a1c 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAManagementView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAManagementView.swift @@ -57,7 +57,6 @@ public struct MFAManagementView { extension MFAManagementView: View { public var body: some View { - @Bindable var passwordPrompt = authService.passwordPrompt VStack(spacing: 20) { // Title section VStack { @@ -135,10 +134,7 @@ extension MFAManagementView: View { .onAppear { loadEnrolledFactors() } - // Present password prompt when required for reauthentication - .sheet(isPresented: $passwordPrompt.isPromptingPassword) { - PasswordPromptSheet(coordinator: authService.passwordPrompt) - } + // Password prompt sheet now centralized in AuthPickerView } @ViewBuilder diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PrivacyTOCsView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PrivacyTOCsView.swift index d8a4da1be1..f5e6bfb638 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PrivacyTOCsView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PrivacyTOCsView.swift @@ -30,7 +30,7 @@ struct PrivacyTOCsView { let displayMode: DisplayMode - public init(displayMode: DisplayMode = .full) { + init(displayMode: DisplayMode = .full) { self.displayMode = displayMode } @@ -61,7 +61,7 @@ struct PrivacyTOCsView { } extension PrivacyTOCsView: View { - public var body: some View { + var body: some View { Group { if let tosURL = authService.configuration.tosUrl, let privacyURL = authService.configuration.privacyPolicyUrl { diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift index ec986f5558..d66848437c 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift @@ -38,7 +38,6 @@ public struct SignedInView { extension SignedInView: View { public var body: some View { - @Bindable var passwordPrompt = authService.passwordPrompt VStack { Text(authService.string.signedInTitle) .font(.largeTitle) @@ -144,9 +143,7 @@ extension SignedInView: View { ) .presentationDetents([.medium]) } - .sheet(isPresented: $passwordPrompt.isPromptingPassword) { - PasswordPromptSheet(coordinator: authService.passwordPrompt) - } + // Password prompt sheet now centralized in AuthPickerView .sheet(isPresented: $showEmailVerificationSent) { VStack(spacing: 24) { Text(authService.string.verifyEmailSheetMessage) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Tests/FirebaseAuthSwiftUITests/MFAEnrolmentUnitTests.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Tests/FirebaseAuthSwiftUITests/MFAEnrolmentUnitTests.swift index c4564dc308..aa9d53beef 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Tests/FirebaseAuthSwiftUITests/MFAEnrolmentUnitTests.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Tests/FirebaseAuthSwiftUITests/MFAEnrolmentUnitTests.swift @@ -29,7 +29,7 @@ import Testing @Suite("TOTPEnrollmentInfo Tests") struct TOTPEnrollmentInfoTests { @Test("Initialization with shared secret key") - func testInitializationWithSharedSecretKey() { + func initializationWithSharedSecretKey() { let validSecrets = [ "JBSWY3DPEHPK3PXP", "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", @@ -47,7 +47,7 @@ struct TOTPEnrollmentInfoTests { } @Test("Initialization with all parameters") - func testInitializationWithAllParameters() throws { + func initializationWithAllParameters() throws { let totpInfo = TOTPEnrollmentInfo( sharedSecretKey: "JBSWY3DPEHPK3PXP", qrCodeURL: URL( @@ -71,7 +71,7 @@ struct TOTPEnrollmentInfoTests { } @Test("Verification status transitions") - func testVerificationStatusTransitions() { + func verificationStatusTransitions() { // Default status is pending var totpInfo = TOTPEnrollmentInfo(sharedSecretKey: "JBSWY3DPEHPK3PXP") #expect(totpInfo.verificationStatus == .pending) diff --git a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/AuthTextField.swift b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/AuthTextField.swift index a60df7dee6..8e96711399 100644 --- a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/AuthTextField.swift +++ b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/AuthTextField.swift @@ -24,13 +24,13 @@ public struct AuthTextField: View { let prompt: String var textAlignment: TextAlignment = .leading var keyboardType: UIKeyboardType = .default - var contentType: UITextContentType? = nil + var contentType: UITextContentType? var isSecureTextField: Bool = false var validations: [FormValidator] = [] var maintainsValidationMessage: Bool = false - var formState: ((Bool) -> Void)? = nil - var onSubmit: ((String) -> Void)? = nil - var onChange: ((String) -> Void)? = nil + var formState: ((Bool) -> Void)? + var onSubmit: ((String) -> Void)? + var onChange: ((String) -> Void)? private let leading: () -> Leading? public init(text: Binding, diff --git a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/CountrySelector.swift b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/CountrySelector.swift index 49d94e9fe1..050d3c37ce 100644 --- a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/CountrySelector.swift +++ b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/CountrySelector.swift @@ -44,7 +44,7 @@ public struct CountryData: Equatable { public struct CountrySelector: View { @Binding var selectedCountry: CountryData var enabled: Bool = true - var allowedCountries: Set? = nil + var allowedCountries: Set? public init(selectedCountry: Binding, enabled: Bool = true, diff --git a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/VerificationCodeInputField.swift b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/VerificationCodeInputField.swift index d57041775e..cc226a33c3 100644 --- a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/VerificationCodeInputField.swift +++ b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/VerificationCodeInputField.swift @@ -145,7 +145,7 @@ public struct VerificationCodeInputField: View { commitCodeChange(truncated) } - if shouldUpdateFocus && (fieldsChanged || forceFocus) { + if shouldUpdateFocus, fieldsChanged || forceFocus { let newFocus = truncated.count < codeLength ? truncated.count : nil DispatchQueue.main.async { withAnimation(.easeInOut(duration: 0.2)) { @@ -154,7 +154,7 @@ public struct VerificationCodeInputField: View { } } - if fieldsChanged && truncated.count == codeLength { + if fieldsChanged, truncated.count == codeLength { DispatchQueue.main.async { onCodeComplete(truncated) } @@ -198,8 +198,7 @@ public struct VerificationCodeInputField: View { commitCodeChange(newCode) onCodeChange(newCode) - if !digit.isEmpty, - let nextIndex = findNextEmptyField(startingFrom: index) { + if !digit.isEmpty, let nextIndex = findNextEmptyField(startingFrom: index) { DispatchQueue.main.async { if focusedIndex != nextIndex { withAnimation(.easeInOut(duration: 0.2)) { @@ -218,7 +217,7 @@ public struct VerificationCodeInputField: View { private func handleBackspace(at index: Int) { // If current field is empty, move to previous field and clear it - if digitFields[index].isEmpty && index > 0 { + if digitFields[index].isEmpty, index > 0 { digitFields[index - 1] = "" DispatchQueue.main.async { let previousIndex = index - 1 diff --git a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Theme/ProviderStyle.swift b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Theme/ProviderStyle.swift index a26a42b9c3..e33c3f8694 100644 --- a/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Theme/ProviderStyle.swift +++ b/FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Theme/ProviderStyle.swift @@ -31,7 +31,7 @@ public struct ProviderStyle: Sendable { public let icon: Image? public let backgroundColor: Color public let contentColor: Color - public var iconTint: Color? = nil + public var iconTint: Color? public let shape: AnyShape = .init(RoundedRectangle(cornerRadius: 4, style: .continuous)) public let elevation: CGFloat diff --git a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExample/App/ContentView.swift b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExample/App/ContentView.swift index 808f825e4a..6e5ec14310 100644 --- a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExample/App/ContentView.swift +++ b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExample/App/ContentView.swift @@ -34,7 +34,7 @@ struct ContentView: View { init() { Auth.auth().useEmulator(withHost: "127.0.0.1", port: 9099) Auth.auth().settings?.isAppVerificationDisabledForTesting = true - //Auth.auth().signInAnonymously() + // Auth.auth().signInAnonymously() let actionCodeSettings = ActionCodeSettings() actionCodeSettings.handleCodeInApp = true actionCodeSettings.url = URL(string: "https://flutterfire-e2e-tests.firebaseapp.com") diff --git a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/FirebaseSwiftUIExampleTests.swift b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/FirebaseSwiftUIExampleTests.swift index 8c4dd7cb49..3fb4c90857 100644 --- a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/FirebaseSwiftUIExampleTests.swift +++ b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/FirebaseSwiftUIExampleTests.swift @@ -40,7 +40,7 @@ struct FirebaseSwiftUIExampleTests { @Test @MainActor - func testDefaultAuthConfigurationInjection() async throws { + func defaultAuthConfigurationInjection() async throws { let config = AuthConfiguration() let service = AuthService(configuration: config) @@ -58,7 +58,7 @@ struct FirebaseSwiftUIExampleTests { @Test @MainActor - func testCustomAuthConfigurationInjection() async throws { + func customAuthConfigurationInjection() async throws { let emailSettings = ActionCodeSettings() emailSettings.handleCodeInApp = true emailSettings.url = URL(string: "https://example.com/email-link") @@ -97,7 +97,7 @@ struct FirebaseSwiftUIExampleTests { @Test @MainActor - func testCreateEmailPasswordUser() async throws { + func createEmailPasswordUser() async throws { let service = try await prepareFreshAuthService() #expect(service.authenticationState == .unauthenticated) @@ -119,7 +119,7 @@ struct FirebaseSwiftUIExampleTests { @Test @MainActor - func testSignInUser() async throws { + func signInUser() async throws { let service = try await prepareFreshAuthService() let email = createEmail() try await service.createUser(email: email, password: kPassword) diff --git a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift index f39a20160b..c524097952 100644 --- a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift +++ b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift @@ -235,10 +235,7 @@ final class MFAEnrollmentUITests: XCTestCase { let displayNameField = app.textFields["display-name-field"] XCTAssertTrue(displayNameField.waitForExistence(timeout: 10)) - UIPasteboard.general.string = "test user" - displayNameField.tap() - displayNameField.press(forDuration: 1.2) - app.menuItems["Paste"].tap() + try pasteIntoField(displayNameField, text: "test user", app: app) let sendCodeButton = app.buttons["send-sms-button"] XCTAssertTrue(sendCodeButton.waitForExistence(timeout: 10)) @@ -426,16 +423,12 @@ final class MFAEnrollmentUITests: XCTestCase { let emailField = app.textFields["email-field"] XCTAssertTrue(emailField.waitForExistence(timeout: 10), "Email field should exist") // Workaround for updating SecureFields with ConnectHardwareKeyboard enabled - UIPasteboard.general.string = email - emailField.press(forDuration: 1.2) - app.menuItems["Paste"].tap() + try pasteIntoField(emailField, text: email, app: app) // Fill password field let passwordField = app.secureTextFields["password-field"] XCTAssertTrue(passwordField.exists, "Password field should exist") - UIPasteboard.general.string = password - passwordField.press(forDuration: 1.2) - app.menuItems["Paste"].tap() + try pasteIntoField(passwordField, text: password, app: app) // Create the user (sign up) let signUpButton = app diff --git a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAResolutionUITests.swift b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAResolutionUITests.swift index e1026b2663..12ce4235e4 100644 --- a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAResolutionUITests.swift +++ b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAResolutionUITests.swift @@ -71,7 +71,7 @@ final class MFAResolutionUITests: XCTestCase { ) let smsButton = app.buttons["sms-method-button"] - if smsButton.exists && smsButton.isEnabled { + if smsButton.exists, smsButton.isEnabled { smsButton.tap() } dismissAlert(app: app) @@ -421,7 +421,7 @@ final class MFAResolutionUITests: XCTestCase { password: String = "123456") throws { // Ensure we're in sign in flow let switchFlowButton = app.buttons["switch-auth-flow"] - if switchFlowButton.exists && switchFlowButton.label.contains("Sign In") { + if switchFlowButton.exists, switchFlowButton.label.contains("Sign In") { switchFlowButton.tap() } diff --git a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/TestUtils.swift b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/TestUtils.swift index 539d76a090..bef4c7f5de 100644 --- a/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/TestUtils.swift +++ b/e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/TestUtils.swift @@ -29,6 +29,45 @@ func createEmail() -> String { } } +// MARK: - Text Input Helpers + +/// Pastes text into a text field using the system paste menu +/// - Parameters: +/// - field: The XCUIElement representing the text field +/// - text: The text to paste +/// - app: The XCUIApplication instance +@MainActor func pasteIntoField(_ field: XCUIElement, text: String, app: XCUIApplication) throws { + UIPasteboard.general.string = text + field.tap() + + // Give field time to become first responder + usleep(200_000) // 0.2 seconds + + // Press and hold to bring up paste menu + field.press(forDuration: 1.5) + + let pasteMenuItem = app.menuItems["Paste"] + + // Wait for paste menu to appear + if !pasteMenuItem.waitForExistence(timeout: 3) { + // Fallback: try double tap approach + field.doubleTap() + usleep(300_000) // 0.3 seconds + + if !pasteMenuItem.waitForExistence(timeout: 2) { + throw NSError( + domain: "TestError", + code: 1, + userInfo: [ + NSLocalizedDescriptionKey: "Failed to show paste menu for field. Text was: \(text)", + ] + ) + } + } + + pasteMenuItem.tap() +} + // MARK: - User Creation /// Helper to create a test user in the emulator via REST API (avoids keychain issues) diff --git a/format-swift.sh b/format-swift.sh index 0aec197f4a..50777451a5 100755 --- a/format-swift.sh +++ b/format-swift.sh @@ -4,5 +4,7 @@ swiftformat ./FirebaseSwiftUI swiftformat ./samples/swiftui/FirebaseSwiftUIExample +swiftformat ./e2eTest + +swiftformat ./Package.swift -swiftformat ./Package.swift \ No newline at end of file diff --git a/lint-swift.sh b/lint-swift.sh new file mode 100755 index 0000000000..eecd5d1b8f --- /dev/null +++ b/lint-swift.sh @@ -0,0 +1,7 @@ +swiftformat --lint ./FirebaseSwiftUI + +swiftformat --lint ./samples/swiftui/FirebaseSwiftUIExample + +swiftformat --lint ./e2eTest + +swiftformat --lint ./Package.swift \ No newline at end of file