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
15 changes: 14 additions & 1 deletion .github/workflows/swiftui-auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -192,4 +205,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: FirebaseSwiftUIExampleUITests.xcresult
path: e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.xcresult
path: e2eTest/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.xcresult
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public struct AuthPickerView<Content: View> {
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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ struct EnterVerificationCodeView: View {
return NavigationStack {
EnterVerificationCodeView(
verificationID: "mock-id",
fullPhoneNumber: "+1 5551234567",
fullPhoneNumber: "+1 5551234567"
)
.environment(AuthService())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ struct PrivacyTOCsView {

let displayMode: DisplayMode

public init(displayMode: DisplayMode = .full) {
init(displayMode: DisplayMode = .full) {
self.displayMode = displayMode
}

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -47,7 +47,7 @@ struct TOTPEnrollmentInfoTests {
}

@Test("Initialization with all parameters")
func testInitializationWithAllParameters() throws {
func initializationWithAllParameters() throws {
let totpInfo = TOTPEnrollmentInfo(
sharedSecretKey: "JBSWY3DPEHPK3PXP",
qrCodeURL: URL(
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ public struct AuthTextField<Leading: View>: 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<String>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public struct CountryData: Equatable {
public struct CountrySelector: View {
@Binding var selectedCountry: CountryData
var enabled: Bool = true
var allowedCountries: Set<String>? = nil
var allowedCountries: Set<String>?

public init(selectedCountry: Binding<CountryData>,
enabled: Bool = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -154,7 +154,7 @@ public struct VerificationCodeInputField: View {
}
}

if fieldsChanged && truncated.count == codeLength {
if fieldsChanged, truncated.count == codeLength {
DispatchQueue.main.async {
onCodeComplete(truncated)
}
Expand Down Expand Up @@ -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)) {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ struct FirebaseSwiftUIExampleTests {

@Test
@MainActor
func testDefaultAuthConfigurationInjection() async throws {
func defaultAuthConfigurationInjection() async throws {
let config = AuthConfiguration()
let service = AuthService(configuration: config)

Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion format-swift.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ swiftformat ./FirebaseSwiftUI

swiftformat ./samples/swiftui/FirebaseSwiftUIExample

swiftformat ./e2eTest

swiftformat ./Package.swift

swiftformat ./Package.swift
7 changes: 7 additions & 0 deletions lint-swift.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
swiftformat --lint ./FirebaseSwiftUI

swiftformat --lint ./samples/swiftui/FirebaseSwiftUIExample

swiftformat --lint ./e2eTest

swiftformat --lint ./Package.swift
Loading