Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
92e1453
feat: Implement Twitter provider
russellwheatley Oct 9, 2025
819e05b
fix: twitter provider update to be pass through `withTwitterSignIn()`
russellwheatley Oct 9, 2025
c233113
fix: import twitter to allow sign-in
russellwheatley Oct 9, 2025
ef5f786
refactor: provider logic so it can be passed into button
russellwheatley Oct 10, 2025
ad137a0
fix: display name if email is unavailable, twitter sometimes user use…
russellwheatley Oct 10, 2025
4cb5f27
chore: use withTwitterSignIn() for test and content view
russellwheatley Oct 10, 2025
76e8ceb
feat: pass provider to providerWithUI
russellwheatley Oct 10, 2025
5283f65
fix: renamed arg to make it clear the distinction between provider & …
russellwheatley Oct 10, 2025
4dbd565
fix: make provider arg optional
russellwheatley Oct 10, 2025
1b6a295
feat: implement API for deleting Twitter user
russellwheatley Oct 10, 2025
7a37a33
fix: put delete function on the provider
russellwheatley Oct 10, 2025
ac7f146
format
russellwheatley Oct 10, 2025
94415ff
feat: Twitter sign-in button styling
russellwheatley Oct 10, 2025
cab823c
chore: change button name to "X"
russellwheatley Oct 10, 2025
854a506
test: sign in buttons are visible on auth picker
russellwheatley Oct 10, 2025
d1602f8
fix: sometimes email isn't available so use `as:` instead
russellwheatley Oct 10, 2025
4897996
fix: remove incorrect naming
russellwheatley Oct 10, 2025
855e40b
test: pass emulator argument
russellwheatley Oct 20, 2025
5259847
test: test setup should be from tests
russellwheatley Oct 20, 2025
a643720
test: refactor tests so setup occurs before running test runner
russellwheatley Oct 20, 2025
9c2255e
test: create test app function
russellwheatley Oct 20, 2025
d3d1f7b
test: remove duplicate test functions
russellwheatley Oct 20, 2025
0e0c4b7
ci: separate into 3 jobs so tests can easily be re-run
russellwheatley Oct 20, 2025
4a3eb71
ci: bump unit test timeout
russellwheatley Oct 20, 2025
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
147 changes: 111 additions & 36 deletions .github/workflows/swiftui-auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,35 +22,76 @@ permissions:
contents: read

jobs:
swiftui-auth:
# Package Unit Tests (standalone, no emulator needed)
unit-tests:
name: Package Unit Tests
runs-on: macos-15
timeout-minutes: 60
timeout-minutes: 20
steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938

- name: Install xcpretty
run: gem install xcpretty

- name: Select Xcode version
run: sudo xcode-select -switch /Applications/Xcode_16.4.app/Contents/Developer

- name: Run FirebaseSwiftUI Package Unit Tests
run: |
set -o pipefail
xcodebuild test \
-scheme FirebaseUI-Package \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro' \
-enableCodeCoverage YES \
-resultBundlePath FirebaseSwiftUIPackageTests.xcresult | tee FirebaseSwiftUIPackageTests.log | xcpretty --test --color --simple

- name: Upload test logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: unit-tests-logs
path: FirebaseSwiftUIPackageTests.log

- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: unit-tests-results
path: FirebaseSwiftUIPackageTests.xcresult

# Integration Tests (requires emulator)
integration-tests:
name: Integration Tests
runs-on: macos-15
timeout-minutes: 20
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
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
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.4.app/Contents/Developer
- name: Run FirebaseSwiftUI Package Unit Tests
run: |
set -o pipefail
xcodebuild test -scheme FirebaseUI-Package -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -enableCodeCoverage YES -resultBundlePath FirebaseSwiftUIPackageTests.xcresult | tee FirebaseSwiftUIPackageTests.log | xcpretty --test --color --simple
# Build for integration tests (builds app + integration test bundle)
run: sudo xcode-select -switch /Applications/Xcode_16.4.app/Contents/Developer

- name: Build for Integration Tests
run: |
cd ./samples/swiftui/FirebaseSwiftUIExample
Expand All @@ -59,7 +100,7 @@ jobs:
-scheme FirebaseSwiftUIExampleTests \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro' \
-enableCodeCoverage YES | xcpretty --color --simple
# Run integration tests
- name: Run Integration Tests
run: |
cd ./samples/swiftui/FirebaseSwiftUIExample
Expand All @@ -69,7 +110,54 @@ jobs:
-destination 'platform=iOS Simulator,name=iPhone 16 Pro' \
-enableCodeCoverage YES \
-resultBundlePath FirebaseSwiftUIExampleTests.xcresult | tee FirebaseSwiftUIExampleTests.log | xcpretty --test --color --simple
# Build for UI tests (reuses app build, builds UI test bundle)

- name: Upload test logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: integration-tests-logs
path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests.log

- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: integration-tests-results
path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleTests.xcresult

# UI Tests (requires emulator)
ui-tests:
name: UI Tests
runs-on: macos-15
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.4.app/Contents/Developer

- name: Build for UI Tests
run: |
cd ./samples/swiftui/FirebaseSwiftUIExample
Expand All @@ -78,8 +166,8 @@ jobs:
-scheme FirebaseSwiftUIExampleUITests \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro' \
-enableCodeCoverage YES | xcpretty --color --simple
# Run UI tests
- name: Run View UI Tests
- name: Run UI Tests
run: |
cd ./samples/swiftui/FirebaseSwiftUIExample
set -o pipefail
Expand All @@ -90,30 +178,17 @@ jobs:
-maximum-concurrent-test-simulator-destinations 2 \
-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: |
FirebaseSwiftUIPackageTests.log
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
- name: Upload FirebaseSwiftUIPackageTests.xcresult bundle
name: ui-tests-logs
path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.log

- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: FirebaseSwiftUIPackageTests.xcresult
path: FirebaseSwiftUIPackageTests.xcresult
name: ui-tests-results
path: samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests.xcresult
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ public final class AuthService {
var emailSignInEnabled = false

private var providers: [AuthProviderUI] = []
public func registerProvider(provider: AuthProviderUI) {
providers.append(provider)
public func registerProvider(providerWithButton: AuthProviderUI) {
providers.append(providerWithButton)
}

public func renderButtons(spacing: CGFloat = 16) -> AnyView {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ extension SignedInView: View {
.fontWeight(.bold)
.padding()
.accessibilityIdentifier("signed-in-text")
Text(authService.string.accountSettingsEmailLabel)
Text("\(authService.currentUser?.email ?? "Unknown")")

Text("as:")
Text("\(authService.currentUser?.email ?? authService.currentUser?.displayName ?? "Unknown")")
if authService.currentUser?.isEmailVerified == false {
VerifyEmailView()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public extension AuthService {
@discardableResult
func withFacebookSignIn(scopes scopes: [String]? = nil) -> AuthService {
FacebookProviderAuthUI.configureProvider(scopes: scopes)
registerProvider(provider: FacebookProviderAuthUI.shared)
registerProvider(providerWithButton: FacebookProviderAuthUI.shared)
return self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ extension SignInWithFacebookButton: View {
.background(Color.blue)
.cornerRadius(8)
}
.accessibilityIdentifier("sign-in-with-facebook-button")
.alert(isPresented: $showCanceledAlert) {
Alert(
title: Text(authService.string.facebookLoginCancelledLabel),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public extension AuthService {
@discardableResult
func withGoogleSignIn(scopes scopes: [String]? = nil) -> AuthService {
let clientID = auth.app?.options.clientID ?? ""
registerProvider(provider: GoogleProviderAuthUI(scopes: scopes, clientID: clientID))
registerProvider(providerWithButton: GoogleProviderAuthUI(scopes: scopes, clientID: clientID))
return self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ extension SignInWithGoogleButton: View {
try await authService.signIn(googleProvider)
}
}
.accessibilityIdentifier("sign-in-with-google-button")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it for testing? Maybe we could define a variable for it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test process is separate from the app used to test the UI so I don't think it's worth it.

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import FirebaseAuthSwiftUI
public extension AuthService {
@discardableResult
func withPhoneSignIn() -> AuthService {
registerProvider(provider: PhoneAuthProviderAuthUI())
registerProvider(providerWithButton: PhoneAuthProviderAuthUI())
return self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ extension PhoneAuthButtonView: View {
.background(Color.green.opacity(0.8)) // Light green
.cornerRadius(8)
}
.accessibilityIdentifier("sign-in-with-phone-button")
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "ic_twitter-white.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// AccountService+Twitter.swift
// FirebaseUI
//
// Created by Russell Wheatley on 10/10/2025.
//

@preconcurrency import FirebaseAuth
import FirebaseAuthSwiftUI
import Observation

protocol TwitterOperationReauthentication {
var twitterProvider: TwitterProviderSwift { get }
}

extension TwitterOperationReauthentication {
@MainActor func reauthenticate() async throws -> AuthenticationToken {
guard let user = Auth.auth().currentUser else {
throw AuthServiceError.reauthenticationRequired("No user currently signed-in")
}

do {
let credential = try await twitterProvider.createAuthCredential()
try await user.reauthenticate(with: credential)

return .firebase("")
} catch {
throw AuthServiceError.signInFailed(underlying: error)
}
}
}

@MainActor
class TwitterDeleteUserOperation: AuthenticatedOperation,
@preconcurrency TwitterOperationReauthentication {
let twitterProvider: TwitterProviderSwift
init(twitterProvider: TwitterProviderSwift) {
self.twitterProvider = twitterProvider
}

func callAsFunction(on user: User) async throws {
try await callAsFunction(on: user) {
try await user.delete()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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.

//
// AuthService+Twitter.swift
// FirebaseUI
//
// Created by Russell Wheatley on 01/05/2025.
//

import FirebaseAuthSwiftUI

public extension AuthService {
@discardableResult
func withTwitterSignIn(_ provider: TwitterProviderSwift? = nil) -> AuthService {
registerProvider(providerWithButton: TwitterProviderAuthUI(provider: provider ??
TwitterProviderSwift()))
return self
}
}
Loading