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
5 changes: 5 additions & 0 deletions .changeset/calm-clients-recover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/expo': patch
---

Fix JS auth state recovery after rejecting a divergent native client so signed-in sessions can still sign out.
167 changes: 69 additions & 98 deletions .github/workflows/expo-native-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ env:
jobs:
native-build:
if: ${{ github.head_ref != 'changeset-release/main' }}
name: Expo ${{ matrix.expo-sdk }} Build / ${{ matrix.platform }}
name: Expo ${{ matrix.expo-sdk }} ${{ matrix.run-e2e == true && 'Build + E2E' || 'Build' }} / ${{ matrix.platform }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
timeout-minutes: ${{ matrix.run-e2e == true && 60 || 45 }}
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -66,23 +66,65 @@ jobs:
with:
persist-credentials: false

- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- name: Compute native build cache key
id: native-build-key
env:
PLATFORM: ${{ matrix.platform }}
run: |
build_hash="$(
git ls-files -s -- \
.npmrc \
package.json \
pnpm-lock.yaml \
pnpm-workspace.yaml \
tsconfig.json \
turbo.json \
packages/clerk-js \
packages/expo \
packages/expo-google-signin \
packages/react \
packages/shared \
"$FIXTURE_DIR" |
git hash-object --stdin
)"
if [ "$PLATFORM" = "android" ]; then
artifact="$FIXTURE_DIR/android/app/build/outputs/apk/release/app-release.apk"
else
artifact="$FIXTURE_DIR/ios/build/Build/Products/Release-iphonesimulator/ClerkExpoNativeBuildFixture.app"
fi
echo "hash=$build_hash" >> "$GITHUB_OUTPUT"
echo "artifact=$artifact" >> "$GITHUB_OUTPUT"

- name: Restore native build
id: native-build-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ${{ steps.native-build-key.outputs.artifact }}
# Bump the version when native build commands change.
key: expo-native-build-v1-${{ runner.os }}-${{ matrix.expo-sdk }}-${{ matrix.platform }}-${{ matrix.run-e2e == true && 'e2e' || 'build' }}-${{ env.E2E_INSTANCE_NAME }}-${{ steps.native-build-key.outputs.hash }}

- if: steps.native-build-cache.outputs.cache-hit != 'true'
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- if: steps.native-build-cache.outputs.cache-hit != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24.15.0
cache: pnpm

- name: Install monorepo dependencies
if: steps.native-build-cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile

- name: Build and pack Clerk packages
if: steps.native-build-cache.outputs.cache-hit != 'true'
run: |
pnpm --filter @clerk/expo... build
mkdir -p "$SDK_PACK_DIR"
pnpm --filter @clerk/expo pack --pack-destination "$SDK_PACK_DIR"
pnpm --filter @clerk/expo-google-signin pack --pack-destination "$SDK_PACK_DIR"

- name: Install fixture dependencies
if: steps.native-build-cache.outputs.cache-hit != 'true'
working-directory: ${{ env.FIXTURE_DIR }}
env:
EXPO_SDK: ${{ matrix.expo-sdk }}
Expand All @@ -100,6 +142,14 @@ jobs:
DEV_CLIENT="expo-dev-client"
if [ "$RUN_E2E" = "true" ]; then DEV_CLIENT=""; fi
pnpm expo install expo-auth-session expo-constants expo-crypto $DEV_CLIENT expo-secure-store expo-web-browser
if [ "$RUN_E2E" = "true" ]; then
REACT_VERSION=$(node -p 'require("react/package.json").version')
REACT_DOM_VERSION=$(node -p 'require("react-dom/package.json").version')
if [ "$REACT_VERSION" != "$REACT_DOM_VERSION" ]; then
echo "::error::React version mismatch: react@$REACT_VERSION, react-dom@$REACT_DOM_VERSION"
exit 1
fi
fi

- name: Check e2e secret availability
id: gate
Expand All @@ -123,125 +173,46 @@ jobs:
run: node scripts/resolve-instance-keys.mjs INTEGRATION_STAGING_INSTANCE_KEYS "$E2E_INSTANCE_NAME"

- name: Write fixture .env
if: steps.native-build-cache.outputs.cache-hit != 'true'
working-directory: ${{ env.FIXTURE_DIR }}
env:
E2E_PK: ${{ steps.keys.outputs.pk }}
run: |
echo "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=${E2E_PK:-$FIXTURE_PUBLISHABLE_KEY}" > .env

- name: Set up JDK 17
if: ${{ matrix.platform == 'android' }}
if: matrix.platform == 'android'
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
distribution: temurin
java-version: 17

- name: Prebuild Android fixture
if: ${{ matrix.platform == 'android' }}
if: matrix.platform == 'android' && steps.native-build-cache.outputs.cache-hit != 'true'
working-directory: ${{ env.FIXTURE_DIR }}
run: pnpm expo prebuild --clean --platform android

- name: Build Android fixture
if: ${{ matrix.platform == 'android' }}
if: matrix.platform == 'android' && steps.native-build-cache.outputs.cache-hit != 'true'
working-directory: ${{ env.FIXTURE_DIR }}
run: pnpm build:android

- name: Prebuild iOS fixture
if: ${{ matrix.platform == 'ios' }}
if: matrix.platform == 'ios' && steps.native-build-cache.outputs.cache-hit != 'true'
working-directory: ${{ env.FIXTURE_DIR }}
run: pnpm expo prebuild --clean --platform ios

- name: Build iOS fixture
if: ${{ matrix.platform == 'ios' }}
if: matrix.platform == 'ios' && steps.native-build-cache.outputs.cache-hit != 'true'
working-directory: ${{ env.FIXTURE_DIR }}
run: pnpm build:ios

# tar the .app so its bundle structure survives the artifact round-trip.
- name: Package iOS app for e2e
if: matrix.platform == 'ios' && matrix.run-e2e == true && steps.keys.outputs.pk != ''
working-directory: ${{ env.FIXTURE_DIR }}
run: |
tar -C ios/build/Build/Products/Release-iphonesimulator \
-cf "$RUNNER_TEMP/ClerkExpoNativeBuildFixture.app.tar" \
ClerkExpoNativeBuildFixture.app

- name: Upload iOS fixture app
if: matrix.platform == 'ios' && matrix.run-e2e == true && steps.keys.outputs.pk != ''
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: expo-fixture-ios
path: ${{ runner.temp }}/ClerkExpoNativeBuildFixture.app.tar
retention-days: 1
if-no-files-found: error

- name: Upload Android fixture app
if: matrix.platform == 'android' && matrix.run-e2e == true && steps.keys.outputs.pk != ''
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: expo-fixture-android
path: ${{ env.FIXTURE_DIR }}/android/app/build/outputs/apk/release/app-release.apk
retention-days: 1
if-no-files-found: error

e2e:
name: E2E / ${{ matrix.platform }}
needs: native-build
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- platform: ios
runner: blacksmith-6vcpu-macos-26
- platform: android
runner: blacksmith-8vcpu-ubuntu-2204

steps:
- name: Checkout repo
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Save native build
if: steps.native-build-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
persist-credentials: false

# Only Node (for the key resolver) is needed; the e2e job builds nothing.
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24.15.0

- name: Check e2e secret availability
id: gate
env:
KEYS: ${{ secrets.INTEGRATION_STAGING_INSTANCE_KEYS }}
run: |
if [ -n "$KEYS" ]; then
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "ok=false" >> "$GITHUB_OUTPUT"
echo "::notice::INTEGRATION_STAGING_INSTANCE_KEYS unavailable (fork PR?); skipping e2e."
fi

- name: Resolve Clerk instance keys
id: keys
if: steps.gate.outputs.ok == 'true'
continue-on-error: true
env:
INTEGRATION_STAGING_INSTANCE_KEYS: ${{ secrets.INTEGRATION_STAGING_INSTANCE_KEYS }}
run: node scripts/resolve-instance-keys.mjs INTEGRATION_STAGING_INSTANCE_KEYS "$E2E_INSTANCE_NAME"

- name: Download fixture app
if: steps.keys.outputs.pk != ''
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: expo-fixture-${{ matrix.platform }}
path: ${{ runner.temp }}/fixture-app

# Maestro is a JVM app and needs a JDK on the Linux runner (macOS ships one).
- name: Set up JDK 17
if: matrix.platform == 'android' && steps.keys.outputs.pk != ''
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
distribution: temurin
java-version: 17
path: ${{ steps.native-build-key.outputs.artifact }}
key: ${{ steps.native-build-cache.outputs.cache-primary-key }}

- name: Cache Maestro
if: steps.keys.outputs.pk != ''
Expand Down Expand Up @@ -314,6 +285,7 @@ jobs:
# Burn-in: e2e cannot fail the check yet. Flip to hard-fail in a
# follow-up once the suite has proven quiet.
continue-on-error: true
working-directory: ${{ env.FIXTURE_DIR }}
env:
CLERK_TEST_EMAIL: ${{ steps.user.outputs.email }}
CLERK_TEST_PASSWORD: ${{ steps.user.outputs.password }}
Expand All @@ -328,8 +300,7 @@ jobs:
xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.AutoCapitalization -bool NO || true
xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.AutoCorrection -bool NO || true
xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.Prediction -bool NO || true
tar -C "$RUNNER_TEMP/fixture-app" -xf "$RUNNER_TEMP/fixture-app/ClerkExpoNativeBuildFixture.app.tar"
xcrun simctl install "$SIM_UDID" "$RUNNER_TEMP/fixture-app/ClerkExpoNativeBuildFixture.app"
xcrun simctl install "$SIM_UDID" ios/build/Build/Products/Release-iphonesimulator/ClerkExpoNativeBuildFixture.app
# Stream the app's console output into the debug artifact so a hang has
# actionable evidence (keychain/network errors) instead of just screenshots.
mkdir -p "$RUNNER_TEMP/maestro-debug"
Expand Down Expand Up @@ -391,9 +362,9 @@ jobs:
# The action runs each script line in a separate sh -c; the folded
# scalar (>-) plus && keeps everything in one shell invocation.
script: >-
adb install -r "$RUNNER_TEMP/fixture-app/app-release.apk" &&
cd integration/tests/expo-native &&
MAESTRO_DEBUG_OUTPUT="$RUNNER_TEMP/maestro-debug" ./run-flows.sh adb shell am force-stop com.clerk.exponativebuildfixture
MAESTRO_DEBUG_OUTPUT="$RUNNER_TEMP/maestro-debug" ./run-android-flows.sh
"$GITHUB_WORKSPACE/$FIXTURE_DIR/android/app/build/outputs/apk/release/app-release.apk"

# Runs before the artifact upload so the credentials are already dead
# by the time the debug files become downloadable.
Expand Down
31 changes: 26 additions & 5 deletions integration/templates/expo-native/components/JsSignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ import { useSignIn } from '@clerk/expo';
import { useState } from 'react';
import { Button, StyleSheet, TextInput } from 'react-native';

/**
* Headless JS-runtime sign-in so the JS client owns the session, which the
* native-token-divergence flow requires (the native AuthView would create the
* session on the native client instead).
*/
export function JsSignInForm({ onStatus }: { onStatus: (status: string) => void }) {
const { signIn } = useSignIn();
const [identifier, setIdentifier] = useState('');
Expand All @@ -21,6 +16,32 @@ export function JsSignInForm({ onStatus }: { onStatus: (status: string) => void
reportError(error.message ?? '');
return;
}

if (signIn.status === 'needs_second_factor' || signIn.status === 'needs_client_trust') {
const supportsEmailCode = signIn.supportedSecondFactors.some(factor => factor.strategy === 'email_code');
if (!supportsEmailCode) {
reportError(`Unsupported sign-in state: ${signIn.status}`);
return;
}

const { error: sendCodeError } = await signIn.mfa.sendEmailCode();
if (sendCodeError) {
reportError(sendCodeError.message ?? '');
return;
}

const { error: verifyCodeError } = await signIn.mfa.verifyEmailCode({ code: '424242' });
if (verifyCodeError) {
reportError(verifyCodeError.message ?? '');
return;
}
}

if (signIn.status !== 'complete' || !signIn.createdSessionId) {
reportError(`Sign-in incomplete: ${signIn.status}`);
return;
}

const { error: finalizeError } = await signIn.finalize();
if (finalizeError) {
reportError(finalizeError.message ?? '');
Expand Down
3 changes: 2 additions & 1 deletion integration/templates/expo-native/package.sdk-57.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
},
"dependencies": {
"expo": "~57.0.1",
"react": "19.2.7",
"react": "19.2.8",
"react-dom": "19.2.8",
"react-native": "0.86.0"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions integration/tests/expo-native/flows/subflows/_warmup.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ appId: com.clerk.exponativebuildfixture
---
- launchApp:
clearState: true
clearKeychain: true
- extendedWaitUntil:
visible:
id: 'auth-state'
Expand Down
12 changes: 1 addition & 11 deletions integration/tests/expo-native/flows/subflows/open-app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ appId: com.clerk.exponativebuildfixture
---
- launchApp:
clearState: true
clearKeychain: true
- extendedWaitUntil:
visible:
id: 'auth-state'
Expand All @@ -13,15 +14,4 @@ appId: com.clerk.exponativebuildfixture
- extendedWaitUntil:
visible: 'signed (in|out)'
timeout: 45000
# clearState does not wipe the iOS Keychain, so a session from a previous
# flow (or retry attempt) can survive the relaunch. Sign out via JS first.
- runFlow:
when:
visible: 'signed in'
commands:
- tapOn:
id: 'sign-out-button'
- extendedWaitUntil:
visible: 'signed out'
timeout: 15000
- assertVisible: 'signed out'
28 changes: 28 additions & 0 deletions integration/tests/expo-native/run-android-flows.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"

apk_path=${1:?APK path is required}
command -v adb >/dev/null 2>&1 || {
echo 'adb is required'
exit 1
}

adb install -r "$apk_path"

logcat_pid=
stop_logcat() {
[ -n "$logcat_pid" ] || return 0
kill "$logcat_pid" >/dev/null 2>&1 || true
wait "$logcat_pid" 2>/dev/null || true
}
trap stop_logcat EXIT

if [ -n "${MAESTRO_DEBUG_OUTPUT:-}" ]; then
mkdir -p "$MAESTRO_DEBUG_OUTPUT"
adb logcat -c || true
adb logcat -v threadtime > "$MAESTRO_DEBUG_OUTPUT/android-logcat.log" 2>&1 &
logcat_pid=$!
fi

./run-flows.sh adb shell am force-stop com.clerk.exponativebuildfixture
Loading
Loading