diff --git a/.github/workflows/swiftui-auth.yml b/.github/workflows/swiftui-auth.yml
new file mode 100644
index 0000000000..c5aa722b7c
--- /dev/null
+++ b/.github/workflows/swiftui-auth.yml
@@ -0,0 +1,78 @@
+name: SwiftUI Auth
+
+on:
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/swiftui-auth.yml'
+ - 'samples/swiftui/**'
+ - 'FirebaseSwiftUI/**'
+ - 'Package.swift'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/swiftui-auth.yml'
+ - 'samples/swiftui/**'
+ - 'FirebaseSwiftUI/**'
+ - 'Package.swift'
+
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ swiftui-auth:
+ runs-on: macOS-latest
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938
+ - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a
+ name: Install Node.js 20
+ with:
+ node-version: '20'
+ - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+ - name: Install Firebase
+ run: |
+ sudo npm i -g firebase-tools
+ - name: Start Firebase Emulator
+ run: |
+ sudo chown -R 501:20 "/Users/runner/.npm" && cd ./samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample && ./start-firebase-emulator.sh
+ - name: Install xcpretty
+ run: gem install xcpretty
+ - name: Select Xcode version
+ run: |
+ sudo xcode-select -switch /Applications/Xcode_16.1.app/Contents/Developer
+ - name: Run Integration Tests
+ run: |
+ cd ./samples/swiftui/FirebaseSwiftUIExample
+ set -o pipefail
+ xcodebuild test -scheme FirebaseSwiftUIExampleTests -destination 'platform=iOS Simulator,name=iPhone 16 Plus' -enableCodeCoverage YES -resultBundlePath FirebaseSwiftUIExampleTests.xcresult | tee FirebaseSwiftUIExampleTests.log | xcpretty --test --color --simple
+ - name: Run View UI Tests
+ run: |
+ cd ./samples/swiftui/FirebaseSwiftUIExample
+ set -o pipefail
+ xcodebuild test -scheme FirebaseSwiftUIExampleUITests -destination 'platform=iOS Simulator,name=iPhone 16 Plus' -enableCodeCoverage YES -resultBundlePath FirebaseSwiftUIExampleUITests.xcresult | tee FirebaseSwiftUIExampleUITests.log | xcpretty --test --color --simple
+ - name: Upload test logs
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: swiftui-auth-test-logs
+ path: |
+ samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests.log
+ samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.log
+ - name: Upload FirebaseSwiftUIExampleUITests.xcresult bundle
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: FirebaseSwiftUIExampleUITests.xcresult
+ path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.xcresult
+ - name: Upload FirebaseSwiftUIExampleTests.xcresult bundle
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: FirebaseSwiftUIExampleTests.xcresult
+ path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests.xcresult
\ No newline at end of file
diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthConfiguration.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthConfiguration.swift
index 3c857d43ed..c1d4b24e01 100644
--- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthConfiguration.swift
+++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthConfiguration.swift
@@ -16,14 +16,14 @@ import FirebaseAuth
import Foundation
public struct AuthConfiguration {
- let shouldHideCancelButton: Bool
- let interactiveDismissEnabled: Bool
- let shouldAutoUpgradeAnonymousUsers: Bool
- let customStringsBundle: Bundle?
- let tosUrl: URL?
- let privacyPolicyUrl: URL?
- let emailLinkSignInActionCodeSettings: ActionCodeSettings?
- let verifyEmailActionCodeSettings: ActionCodeSettings?
+ public let shouldHideCancelButton: Bool
+ public let interactiveDismissEnabled: Bool
+ public let shouldAutoUpgradeAnonymousUsers: Bool
+ public let customStringsBundle: Bundle?
+ public let tosUrl: URL?
+ public let privacyPolicyUrl: URL?
+ public let emailLinkSignInActionCodeSettings: ActionCodeSettings?
+ public let verifyEmailActionCodeSettings: ActionCodeSettings?
public init(shouldHideCancelButton: Bool = false,
interactiveDismissEnabled: Bool = true,
diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift
index 54a93c0c69..0a656f2490 100644
--- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift
+++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift
@@ -142,7 +142,7 @@ public final class AuthService {
private var unsafePhoneAuthProvider: (any PhoneAuthProviderAuthUIProtocol)?
private var listenerManager: AuthListenerManager?
- private var signedInCredential: AuthCredential?
+ public var signedInCredential: AuthCredential?
var emailSignInEnabled = false
diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift
index 9d589726cd..e48b8ddc32 100644
--- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift
+++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift
@@ -82,7 +82,7 @@ extension AuthPickerView: View {
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
.fontWeight(.semibold)
.foregroundColor(.blue)
- }
+ }.accessibilityIdentifier("switch-auth-flow")
}
}
PrivacyTOCsView(displayMode: .footer)
diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift
index 812185f59a..739bbebe11 100644
--- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift
+++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift
@@ -79,10 +79,13 @@ extension EmailAuthView: View {
.padding(.vertical, 6)
.background(Divider(), alignment: .bottom)
.padding(.bottom, 4)
+ .accessibilityIdentifier("email-field")
LabeledContent {
SecureField(authService.string.passwordInputLabel, text: $password)
.focused($focus, equals: .password)
+ .textInputAutocapitalization(.never)
+ .disableAutocorrection(true)
.submitLabel(.go)
.onSubmit {
Task { await signInWithEmailPassword() }
@@ -93,19 +96,22 @@ extension EmailAuthView: View {
.padding(.vertical, 6)
.background(Divider(), alignment: .bottom)
.padding(.bottom, 8)
+ .accessibilityIdentifier("password-field")
if authService.authenticationFlow == .login {
Button(action: {
authService.authView = .passwordRecovery
}) {
Text(authService.string.passwordButtonLabel)
- }
+ }.accessibilityIdentifier("password-recovery-button")
}
if authService.authenticationFlow == .signUp {
LabeledContent {
SecureField(authService.string.confirmPasswordInputLabel, text: $confirmPassword)
.focused($focus, equals: .confirmPassword)
+ .textInputAutocapitalization(.never)
+ .disableAutocorrection(true)
.submitLabel(.go)
.onSubmit {
Task { await createUserWithEmailPassword() }
@@ -116,6 +122,7 @@ extension EmailAuthView: View {
.padding(.vertical, 6)
.background(Divider(), alignment: .bottom)
.padding(.bottom, 8)
+ .accessibilityIdentifier("confirm-password-field")
}
Button(action: {
@@ -140,11 +147,12 @@ extension EmailAuthView: View {
.padding([.top, .bottom, .horizontal], 8)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
+ .accessibilityIdentifier("sign-in-button")
Button(action: {
authService.authView = .emailLink
}) {
Text(authService.string.signUpWithEmailLinkButtonLabel)
- }
+ }.accessibilityIdentifier("sign-in-with-email-link-button")
}
}
}
diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift
index 07241b3f2b..5e242f8d62 100644
--- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift
+++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift
@@ -35,6 +35,7 @@ extension EmailLinkView: View {
public var body: some View {
VStack {
Text(authService.string.signInWithEmailLinkViewTitle)
+ .accessibilityIdentifier("email-link-title-text")
LabeledContent {
TextField(authService.string.emailInputLabel, text: $email)
.textInputAutocapitalization(.never)
@@ -84,7 +85,7 @@ extension EmailLinkView: View {
.foregroundColor(.blue)
Text(authService.string.backButtonLabel)
.foregroundColor(.blue)
- })
+ }.accessibilityIdentifier("email-link-back-button"))
}
}
diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift
index cc1207a77c..d58f2be1f7 100644
--- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift
+++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift
@@ -46,6 +46,7 @@ extension PasswordRecoveryView: View {
.font(.largeTitle)
.fontWeight(.bold)
.padding()
+ .accessibilityIdentifier("password-recovery-text")
Divider()
@@ -85,7 +86,7 @@ extension PasswordRecoveryView: View {
.foregroundColor(.blue)
Text(authService.string.backButtonLabel)
.foregroundColor(.blue)
- })
+ }.accessibilityIdentifier("password-recovery-back-button"))
}
@ViewBuilder
diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift
index b903a53219..a8d3acca9f 100644
--- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift
+++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/SignedInView.swift
@@ -37,6 +37,7 @@ extension SignedInView: View {
.font(.largeTitle)
.fontWeight(.bold)
.padding()
+ .accessibilityIdentifier("signed-in-text")
Text(authService.string.accountSettingsEmailLabel)
Text("\(authService.currentUser?.email ?? "Unknown")")
@@ -54,7 +55,7 @@ extension SignedInView: View {
try await authService.signOut()
} catch {}
}
- }
+ }.accessibilityIdentifier("sign-out-button")
Divider()
Button(authService.string.deleteAccountButtonLabel) {
Task {
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj
index 2fb44425f3..812f7b770c 100644
--- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/project.pbxproj
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
+ 4600E5522DD777BE00EED5F3 /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 4600E5512DD777BE00EED5F3 /* FirebaseAuth */; };
+ 4600E5542DD777BE00EED5F3 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = 4600E5532DD777BE00EED5F3 /* FirebaseCore */; };
4607CC9C2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4607CC9B2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI */; };
4607CC9E2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4607CC9D2D9BFE29009EC3F5 /* FirebaseGoogleSwiftUI */; };
46CB7B252D773F2100F1FD0A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 46CB7B242D773F2100F1FD0A /* GoogleService-Info.plist */; };
@@ -96,6 +98,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 4600E5542DD777BE00EED5F3 /* FirebaseCore in Frameworks */,
+ 4600E5522DD777BE00EED5F3 /* FirebaseAuth in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -203,6 +207,8 @@
);
name = FirebaseSwiftUIExampleUITests;
packageProductDependencies = (
+ 4600E5512DD777BE00EED5F3 /* FirebaseAuth */,
+ 4600E5532DD777BE00EED5F3 /* FirebaseCore */,
);
productName = FirebaseSwiftUIExampleUITests;
productReference = 46F89C242D64A86D000F8BC0 /* FirebaseSwiftUIExampleUITests.xctest */;
@@ -242,6 +248,7 @@
minimizedProjectReferenceProxies = 1;
packageReferences = (
8D808CB52DB07EBD00D2293F /* XCLocalSwiftPackageReference "../../../../FirebaseUI-iOS" */,
+ 4600E5502DD777BE00EED5F3 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 46F89C092D64A86C000F8BC0 /* Products */;
@@ -511,7 +518,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = YYX2P3XVJ7;
GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 18.2;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.invertase.testing.FirebaseSwiftUIExampleTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -530,7 +537,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = YYX2P3XVJ7;
GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 18.2;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.invertase.testing.FirebaseSwiftUIExampleTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -623,7 +630,28 @@
};
/* End XCLocalSwiftPackageReference section */
+/* Begin XCRemoteSwiftPackageReference section */
+ 4600E5502DD777BE00EED5F3 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/firebase/firebase-ios-sdk";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 11.12.0;
+ };
+ };
+/* End XCRemoteSwiftPackageReference section */
+
/* Begin XCSwiftPackageProductDependency section */
+ 4600E5512DD777BE00EED5F3 /* FirebaseAuth */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 4600E5502DD777BE00EED5F3 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+ productName = FirebaseAuth;
+ };
+ 4600E5532DD777BE00EED5F3 /* FirebaseCore */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 4600E5502DD777BE00EED5F3 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+ productName = FirebaseCore;
+ };
4607CC9B2D9BFE29009EC3F5 /* FirebaseAuthSwiftUI */ = {
isa = XCSwiftPackageProductDependency;
productName = FirebaseAuthSwiftUI;
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/xcshareddata/xcschemes/FirebaseSwiftUIExampleTests.xcscheme b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/xcshareddata/xcschemes/FirebaseSwiftUIExampleTests.xcscheme
new file mode 100644
index 0000000000..5a80b5361c
--- /dev/null
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/xcshareddata/xcschemes/FirebaseSwiftUIExampleTests.xcscheme
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/xcshareddata/xcschemes/FirebaseSwiftUIExampleUITests.xcscheme b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/xcshareddata/xcschemes/FirebaseSwiftUIExampleUITests.xcscheme
new file mode 100644
index 0000000000..3bd50f3ad0
--- /dev/null
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample.xcodeproj/xcshareddata/xcschemes/FirebaseSwiftUIExampleUITests.xcscheme
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/.firebaserc b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/.firebaserc
new file mode 100644
index 0000000000..6aa25141ca
--- /dev/null
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/.firebaserc
@@ -0,0 +1,5 @@
+{
+ "projects": {
+ "default": "flutterfire-e2e-tests"
+ }
+}
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/.gitignore b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/.gitignore
new file mode 100644
index 0000000000..b17f631075
--- /dev/null
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/.gitignore
@@ -0,0 +1,69 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+firebase-debug.log*
+firebase-debug.*.log*
+
+# Firebase cache
+.firebase/
+
+# Firebase config
+
+# Uncomment this if you'd like others to create their own Firebase project.
+# For a team working on the same Firebase project(s), it is recommended to leave
+# it commented so all members can deploy to the same project(s) in .firebaserc.
+# .firebaserc
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+# dataconnect generated files
+.dataconnect
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift
index 0a4a67644c..e51ef876cd 100644
--- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift
@@ -39,10 +39,12 @@ struct ContentView: View {
actionCodeSettings.linkDomain = "flutterfire-e2e-tests.firebaseapp.com"
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
let configuration = AuthConfiguration(
+ shouldAutoUpgradeAnonymousUsers: !uiAuthEmulator,
tosUrl: URL(string: "https://example.com/tos"),
privacyPolicyUrl: URL(string: "https://example.com/privacy"),
emailLinkSignInActionCodeSettings: actionCodeSettings
)
+
authService = AuthService(
configuration: configuration
)
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExampleApp.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExampleApp.swift
index 93d24cb5ae..630fdb6603 100644
--- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExampleApp.swift
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/FirebaseSwiftUIExampleApp.swift
@@ -30,6 +30,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
UIApplication.LaunchOptionsKey: Any
]?) -> Bool {
FirebaseApp.configure()
+ if uiAuthEmulator {
+ Auth.auth().useEmulator(withHost: "localhost", port: 9099)
+ }
+
ApplicationDelegate.shared.application(
application,
didFinishLaunchingWithOptions: launchOptions
@@ -72,6 +76,12 @@ class AppDelegate: NSObject, UIApplicationDelegate {
struct FirebaseSwiftUIExampleApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
+ init() {
+ Task {
+ try await testCreateUser()
+ }
+ }
+
var body: some Scene {
WindowGroup {
NavigationView {
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/UITestUtils.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/UITestUtils.swift
new file mode 100644
index 0000000000..2116c2f26a
--- /dev/null
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/UITestUtils.swift
@@ -0,0 +1,27 @@
+//
+// UITestUtils.swift
+// FirebaseSwiftUIExample
+//
+// Created by Russell Wheatley on 16/05/2025.
+//
+import FirebaseAuth
+import SwiftUI
+
+// UI Test Runner keys
+public let uiAuthEmulator = CommandLine.arguments.contains("--auth-emulator")
+
+public var testEmail: String? {
+ guard let emailIndex = CommandLine.arguments.firstIndex(of: "--create-user"),
+ CommandLine.arguments.indices.contains(emailIndex + 1)
+ else { return nil }
+ return CommandLine.arguments[emailIndex + 1]
+}
+
+func testCreateUser() async throws {
+ if let email = testEmail {
+ let password = "123456"
+ let auth = Auth.auth()
+ try await auth.createUser(withEmail: email, password: password)
+ try auth.signOut()
+ }
+}
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/firebase.json b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/firebase.json
new file mode 100644
index 0000000000..2fb2a16b0d
--- /dev/null
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/firebase.json
@@ -0,0 +1,11 @@
+{
+ "emulators": {
+ "auth": {
+ "port": 9099
+ },
+ "ui": {
+ "enabled": true
+ },
+ "singleProjectMode": true
+ }
+}
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/start-firebase-emulator.sh b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/start-firebase-emulator.sh
new file mode 100755
index 0000000000..fb1280e438
--- /dev/null
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/start-firebase-emulator.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+if ! [ -x "$(command -v firebase)" ]; then
+ echo "β Firebase tools CLI is missing."
+ exit 1
+fi
+
+if ! [ -x "$(command -v node)" ]; then
+ echo "β Node.js is missing."
+ exit 1
+fi
+
+if ! [ -x "$(command -v npm)" ]; then
+ echo "β NPM is missing."
+ exit 1
+fi
+
+EMU_START_COMMAND="firebase emulators:start --only auth --project flutterfire-e2e-tests"
+
+MAX_RETRIES=3
+MAX_CHECKATTEMPTS=60
+CHECKATTEMPTS_WAIT=1
+
+RETRIES=1
+while [ $RETRIES -le $MAX_RETRIES ]; do
+
+ if [[ -z "${CI}" ]]; then
+ echo "Starting Firebase Emulator in foreground."
+ $EMU_START_COMMAND
+ exit 0
+ else
+ echo "Starting Firebase Emulator in background."
+ $EMU_START_COMMAND &
+ CHECKATTEMPTS=1
+ while [ $CHECKATTEMPTS -le $MAX_CHECKATTEMPTS ]; do
+ sleep $CHECKATTEMPTS_WAIT
+ if curl --output /dev/null --silent --fail http://localhost:9099; then
+ # Check again since it can exit before the emulator is ready.
+ sleep 15
+ if curl --output /dev/null --silent --fail http://localhost:9099; then
+ echo "Firebase Emulator is online!"
+ exit 0
+ else
+ echo "β Firebase Emulator exited after startup."
+ exit 1
+ fi
+ fi
+ echo "Waiting for Firebase Emulator to come online, check $CHECKATTEMPTS of $MAX_CHECKATTEMPTS..."
+ ((CHECKATTEMPTS = CHECKATTEMPTS + 1))
+ done
+ fi
+
+ echo "Firebase Emulator did not come online in $MAX_CHECKATTEMPTS checks. Try $RETRIES of $MAX_RETRIES."
+ ((RETRIES = RETRIES + 1))
+
+done
+echo "Firebase Emulator did not come online after $MAX_RETRIES attempts."
+exit 1
\ No newline at end of file
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/FirebaseSwiftUIExampleTests.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/FirebaseSwiftUIExampleTests.swift
index cc3ad84890..e43a002927 100644
--- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/FirebaseSwiftUIExampleTests.swift
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/FirebaseSwiftUIExampleTests.swift
@@ -18,12 +18,124 @@
//
// Created by Russell Wheatley on 18/02/2025.
//
-
+import FirebaseAuth
+import FirebaseAuthSwiftUI
+import FirebaseCore
@testable import FirebaseSwiftUIExample
import Testing
+let kPassword = "123456"
+
struct FirebaseSwiftUIExampleTests {
- @Test func example() async throws {
- // Write your test here and use APIs like `#expect(...)` to check expected conditions.
+ @MainActor
+ func prepareFreshAuthService(configuration: AuthConfiguration? = nil) async throws
+ -> AuthService {
+ configureFirebaseIfNeeded()
+ try await clearAuthEmulatorState()
+
+ let resolvedConfiguration = configuration ?? AuthConfiguration()
+
+ return AuthService(configuration: resolvedConfiguration)
+ }
+
+ @Test
+ @MainActor
+ func testDefaultAuthConfigurationInjection() async throws {
+ let config = AuthConfiguration()
+ let service = AuthService(configuration: config)
+
+ let actual = service.configuration
+
+ #expect(actual.shouldHideCancelButton == false)
+ #expect(actual.interactiveDismissEnabled == true)
+ #expect(actual.shouldAutoUpgradeAnonymousUsers == false)
+ #expect(actual.customStringsBundle == nil)
+ #expect(actual.tosUrl == nil)
+ #expect(actual.privacyPolicyUrl == nil)
+ #expect(actual.emailLinkSignInActionCodeSettings == nil)
+ #expect(actual.verifyEmailActionCodeSettings == nil)
+ }
+
+ @Test
+ @MainActor
+ func testCustomAuthConfigurationInjection() async throws {
+ let emailSettings = ActionCodeSettings()
+ emailSettings.handleCodeInApp = true
+ emailSettings.url = URL(string: "https://example.com/email-link")
+ emailSettings.setIOSBundleID("com.example.test")
+
+ let verifySettings = ActionCodeSettings()
+ verifySettings.handleCodeInApp = true
+ verifySettings.url = URL(string: "https://example.com/verify-email")
+ verifySettings.setIOSBundleID("com.example.test")
+
+ let config = AuthConfiguration(
+ shouldHideCancelButton: true,
+ interactiveDismissEnabled: false,
+ shouldAutoUpgradeAnonymousUsers: true,
+ customStringsBundle: .main,
+ tosUrl: URL(string: "https://example.com/tos"),
+ privacyPolicyUrl: URL(string: "https://example.com/privacy"),
+ emailLinkSignInActionCodeSettings: emailSettings,
+ verifyEmailActionCodeSettings: verifySettings
+ )
+
+ let service = AuthService(configuration: config)
+
+ let actual = service.configuration
+ #expect(actual.shouldHideCancelButton == true)
+ #expect(actual.interactiveDismissEnabled == false)
+ #expect(actual.shouldAutoUpgradeAnonymousUsers == true)
+ #expect(actual.customStringsBundle === Bundle.main)
+ #expect(actual.tosUrl == URL(string: "https://example.com/tos"))
+ #expect(actual.privacyPolicyUrl == URL(string: "https://example.com/privacy"))
+
+ // Optional action code settings checks
+ #expect(actual.emailLinkSignInActionCodeSettings?.url == emailSettings.url)
+ #expect(actual.verifyEmailActionCodeSettings?.url == verifySettings.url)
+ }
+
+ @Test
+ @MainActor
+ func testCreateEmailPasswordUser() async throws {
+ let service = try await prepareFreshAuthService()
+
+ #expect(service.authenticationState == .unauthenticated)
+ #expect(service.authView == .authPicker)
+ #expect(service.errorMessage.isEmpty)
+ #expect(service.signedInCredential == nil)
+ #expect(service.currentUser == nil)
+ try await service.createUser(withEmail: createEmail(), password: kPassword)
+ try await Task.sleep(nanoseconds: 4_000_000_000)
+ #expect(service.authenticationState == .authenticated)
+ #expect(service.authView == .authPicker)
+ #expect(service.errorMessage.isEmpty)
+ #expect(service.currentUser != nil)
+ // TODO: - reinstate once this PR is merged: https://github.com/firebase/FirebaseUI-iOS/pull/1256
+// #expect(service.signedInCredential is AuthCredential)
+ }
+
+ @Test
+ @MainActor
+ func testSignInUser() async throws {
+ let service = try await prepareFreshAuthService()
+ let email = createEmail()
+ try await service.createUser(withEmail: email, password: kPassword)
+ try await service.signOut()
+ try await Task.sleep(nanoseconds: 2_000_000_000)
+ #expect(service.authenticationState == .unauthenticated)
+ #expect(service.authView == .authPicker)
+ #expect(service.errorMessage.isEmpty)
+ #expect(service.signedInCredential == nil)
+ #expect(service.currentUser == nil)
+
+ try await service.signIn(withEmail: email, password: kPassword)
+
+ #expect(service.authenticationState == .authenticated)
+ #expect(service.authView == .authPicker)
+ #expect(service.errorMessage.isEmpty)
+ #expect(service.currentUser != nil)
+ // TODO: - reinstate once this PR is merged: https://github.com/firebase/FirebaseUI-iOS/pull/1256
+ // #expect(service.signedInCredential is AuthCredential)
}
}
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/TestHarness.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/TestHarness.swift
new file mode 100644
index 0000000000..1b474daf80
--- /dev/null
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests/TestHarness.swift
@@ -0,0 +1,63 @@
+//
+// TestHarness.swift
+// FirebaseSwiftUIExample
+//
+// Created by Russell Wheatley on 16/05/2025.
+//
+
+import FirebaseAuth
+import FirebaseCore
+
+@MainActor
+func configureFirebaseIfNeeded() {
+ if FirebaseApp.app() == nil {
+ FirebaseApp.configure()
+ }
+}
+
+private var hasCheckedEmulatorAvailability = false
+
+@MainActor
+func isEmulatorRunning() async throws {
+ if hasCheckedEmulatorAvailability { return }
+ let healthCheckURL = URL(string: "http://localhost:9099/")!
+ var healthRequest = URLRequest(url: healthCheckURL)
+ healthRequest.httpMethod = "HEAD"
+
+ let session = URLSession(configuration: .ephemeral)
+ let (_, response) = try await session.data(for: healthRequest)
+
+ guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
+ throw NSError(
+ domain: "FirebaseAuthSwiftUITests",
+ code: 1,
+ userInfo: [
+ NSLocalizedDescriptionKey: """
+ π Firebase Auth Emulator is not running on localhost:9099.
+ Please run: `firebase emulators:start --only auth`
+ """,
+ ]
+ )
+ }
+ Auth.auth().useEmulator(withHost: "localhost", port: 9099)
+ hasCheckedEmulatorAvailability = true
+}
+
+@MainActor
+func clearAuthEmulatorState(projectID: String = "flutterfire-e2e-tests") async throws {
+ try await isEmulatorRunning()
+ let url = URL(string: "http://localhost:9099/emulator/v1/projects/\(projectID)/accounts")!
+ var request = URLRequest(url: url)
+ request.httpMethod = "DELETE"
+ let (_, response) = try await URLSession.shared.data(for: request)
+
+ if let httpResponse = response as? HTTPURLResponse {
+ print("π₯ clearAuthEmulatorState: status = \(httpResponse.statusCode)")
+ }
+}
+
+func createEmail() -> String {
+ let before = UUID().uuidString.prefix(8)
+ let after = UUID().uuidString.prefix(6)
+ return "\(before)@\(after).com"
+}
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift
index 4370378fe7..1252d35519 100644
--- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift
+++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITests.swift
@@ -19,41 +19,167 @@
// Created by Russell Wheatley on 18/02/2025.
//
+import FirebaseAuth
+import FirebaseCore
import XCTest
+func createEmail() -> String {
+ let before = UUID().uuidString.prefix(8)
+ let after = UUID().uuidString.prefix(6)
+ return "\(before)@\(after).com"
+}
+
+func dismissAlert(app: XCUIApplication) {
+ if app.scrollViews.otherElements.buttons["Not Now"].waitForExistence(timeout: 2) {
+ app.scrollViews.otherElements.buttons["Not Now"].tap()
+ }
+}
+
final class FirebaseSwiftUIExampleUITests: XCTestCase {
override func setUpWithError() throws {
- // Put setup code here. This method is called before the invocation of each test method in the
- // class.
-
- // In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
-
- // In UI tests itβs important to set the initial state - such as interface orientation -
- // required for your tests before they run. The setUp method is a good place to do this.
}
- override func tearDownWithError() throws {
- // Put teardown code here. This method is called after the invocation of each test method in the
- // class.
- }
+ override func tearDownWithError() throws {}
@MainActor
func testExample() throws {
- // UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
-
- // Use XCTAssert and related functions to verify your tests produce the correct results.
}
@MainActor
func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
- // This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
+
+ @MainActor
+ func testSignInDisplaysSignedInView() async throws {
+ let app = XCUIApplication()
+ let email = createEmail()
+ app.launchArguments.append("--auth-emulator")
+ app.launchArguments.append("--create-user")
+ app.launchArguments.append("\(email)")
+ app.launch()
+
+ let emailField = app.textFields["email-field"]
+ XCTAssertTrue(emailField.waitForExistence(timeout: 6), "Email field should exist")
+ emailField.tap()
+ emailField.typeText(email)
+
+ let passwordField = app.secureTextFields["password-field"]
+ XCTAssertTrue(passwordField.exists, "Password field should exist")
+ passwordField.tap()
+ passwordField.typeText("123456")
+
+ let signInButton = app.buttons["sign-in-button"]
+ XCTAssertTrue(signInButton.exists, "Sign-In button should exist")
+ signInButton.tap()
+
+ let signedInText = app.staticTexts["signed-in-text"]
+ XCTAssertTrue(
+ signedInText.waitForExistence(timeout: 10),
+ "SignedInView should be visible after login"
+ )
+
+ dismissAlert(app: app)
+ // Check the Views are updated
+ let signOutButton = app.buttons["sign-out-button"]
+ XCTAssertTrue(
+ signOutButton.waitForExistence(timeout: 10),
+ "Sign-Out button should exist and be visible"
+ )
+
+ signOutButton.tap()
+ XCTAssertTrue(
+ signInButton.waitForExistence(timeout: 20),
+ "Sign-In button should exist after logout"
+ )
+
+ let passwordRecoveryButton = app.buttons["password-recovery-button"]
+ XCTAssertTrue(passwordRecoveryButton.exists, "Password recovery button should exist")
+ passwordRecoveryButton.tap()
+ let passwordRecoveryText = app.staticTexts["password-recovery-text"]
+ XCTAssertTrue(
+ passwordRecoveryText.waitForExistence(timeout: 10),
+ "Password recovery text should exist after routing to PasswordRecoveryView"
+ )
+
+ let passwordRecoveryBackButton = app.buttons["password-recovery-back-button"]
+ XCTAssertTrue(passwordRecoveryBackButton.exists, "Password back button should exist")
+ passwordRecoveryBackButton.tap()
+
+ let signInButton2 = app.buttons["sign-in-button"]
+ XCTAssertTrue(
+ signInButton2.waitForExistence(timeout: 10),
+ "Sign-In button should exist after pressing password recovery back button"
+ )
+
+ let emailLinkSignInButton = app.buttons["sign-in-with-email-link-button"]
+ XCTAssertTrue(emailLinkSignInButton.exists, "Email link sign-in button should exist")
+ emailLinkSignInButton.tap()
+
+ let emailLinkText = app.staticTexts["email-link-title-text"]
+
+ XCTAssertTrue(
+ emailLinkText.waitForExistence(timeout: 10),
+ "Email link text should exist after pressing email link button in AuthPickerView"
+ )
+
+ let emailLinkBackButton = app.buttons["email-link-back-button"]
+ XCTAssertTrue(emailLinkBackButton.exists, "Email link back button should exist")
+ emailLinkBackButton.tap()
+
+ let signInButton3 = app.buttons["sign-in-button"]
+ XCTAssertTrue(
+ signInButton3.waitForExistence(timeout: 10),
+ "Sign-In button should exist after pressing password recovery back button"
+ )
+ }
+
+ @MainActor
+ func testCreateUserDisplaysSignedInView() throws {
+ let app = XCUIApplication()
+ let email = createEmail()
+ let password = "qwerty321@"
+ app.launchArguments.append("--auth-emulator")
+ app.launch()
+
+ let switchFlowButton = app.buttons["switch-auth-flow"]
+ switchFlowButton.tap()
+
+ let emailField = app.textFields["email-field"]
+
+ XCTAssertTrue(emailField.waitForExistence(timeout: 2), "Email field should exist")
+ // Workaround for updating SecureFields with ConnectHardwareKeyboard enabled
+ UIPasteboard.general.string = email
+ emailField.press(forDuration: 1.2)
+ app.menuItems["Paste"].tap()
+
+ 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()
+
+ let confirmPasswordField = app.secureTextFields["confirm-password-field"]
+ XCTAssertTrue(confirmPasswordField.exists, "Confirm password field should exist")
+ UIPasteboard.general.string = password
+ confirmPasswordField.press(forDuration: 1.2)
+ app.menuItems["Paste"].tap()
+
+ let signInButton = app.buttons["sign-in-button"]
+ XCTAssertTrue(signInButton.exists, "Sign-In button should exist")
+ signInButton.tap()
+
+ let signedInText = app.staticTexts["signed-in-text"]
+ XCTAssertTrue(
+ signedInText.waitForExistence(timeout: 20),
+ "SignedInView should be visible after login"
+ )
+ }
}
diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITestsLaunchTests.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITestsLaunchTests.swift
deleted file mode 100644
index 5518d8c0e5..0000000000
--- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/FirebaseSwiftUIExampleUITestsLaunchTests.swift
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2025 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//
-// FirebaseSwiftUIExampleUITestsLaunchTests.swift
-// FirebaseSwiftUIExampleUITests
-//
-// Created by Russell Wheatley on 18/02/2025.
-//
-
-import XCTest
-
-final class FirebaseSwiftUIExampleUITestsLaunchTests: XCTestCase {
- override class var runsForEachTargetApplicationUIConfiguration: Bool {
- true
- }
-
- override func setUpWithError() throws {
- continueAfterFailure = false
- }
-
- @MainActor
- func testLaunch() throws {
- let app = XCUIApplication()
- app.launch()
-
- // Insert steps here to perform after app launch but before taking a screenshot,
- // such as logging into a test account or navigating somewhere in the app
-
- let attachment = XCTAttachment(screenshot: app.screenshot())
- attachment.name = "Launch Screen"
- attachment.lifetime = .keepAlways
- add(attachment)
- }
-}