From 2add7953d0fe99c3ce80ecc70d8121e13c6a1724 Mon Sep 17 00:00:00 2001 From: Mike Pitre <12040919+mikepitre@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:24:16 -0400 Subject: [PATCH 1/5] test(expo): drive the expo-native fixture from verify launch inputs The expo-native fixture reads `verify*` launch inputs: iOS launch arguments and Android string extras. A caller can open one screen, sign in with a ticket, and read auth state from a footer and the log without tapping through the app. With no `verify*` input the fixture renders as before. - `modules/verify-launch-config` is a local Expo module. It reads the inputs and clears stored Clerk state when `verifyStorageScope` changes. - `verify/launch.ts` parses the inputs. `verify/VerifyHost.tsx` picks the screen, signs in with `verifySignInTicket`, and renders the state as one line of JSON. - `screens/` adds the custom sign-in, custom sign-up, and token cache screens. - `metro.config.js` watches the monorepo when `@clerk/expo` links to the workspace, so a Debug dev client serves the local package. - On SDK 57, `app.config.js` turns on `ios.enableSceneSupport` through `expo-build-properties`. Co-Authored-By: Claude Opus 5.5 --- .changeset/large-aliens-arrive.md | 2 + integration/templates/expo-native/App.tsx | 21 +- .../templates/expo-native/app.config.js | 6 + .../templates/expo-native/metro.config.js | 37 +++ .../verify-launch-config/android/build.gradle | 15 + .../android/src/main/AndroidManifest.xml | 2 + .../VerifyLaunchConfigModule.kt | 34 +++ .../expo-module.config.json | 9 + .../modules/verify-launch-config/index.ts | 8 + .../ios/VerifyLaunchConfig.podspec | 18 ++ .../ios/VerifyLaunchConfigModule.swift | 34 +++ .../templates/expo-native/package.sdk-57.json | 3 +- .../expo-native/screens/CustomSignIn.tsx | 74 +++++ .../expo-native/screens/CustomSignUp.tsx | 84 ++++++ .../expo-native/screens/TokenCache.tsx | 27 ++ .../templates/expo-native/screens/styles.ts | 22 ++ .../expo-native/verify/VerifyHost.tsx | 283 ++++++++++++++++++ .../templates/expo-native/verify/launch.ts | 91 ++++++ .../templates/expo-native/verify/state.ts | 33 ++ 19 files changed, 800 insertions(+), 3 deletions(-) create mode 100644 .changeset/large-aliens-arrive.md create mode 100644 integration/templates/expo-native/app.config.js create mode 100644 integration/templates/expo-native/metro.config.js create mode 100644 integration/templates/expo-native/modules/verify-launch-config/android/build.gradle create mode 100644 integration/templates/expo-native/modules/verify-launch-config/android/src/main/AndroidManifest.xml create mode 100644 integration/templates/expo-native/modules/verify-launch-config/android/src/main/java/expo/modules/verifylaunchconfig/VerifyLaunchConfigModule.kt create mode 100644 integration/templates/expo-native/modules/verify-launch-config/expo-module.config.json create mode 100644 integration/templates/expo-native/modules/verify-launch-config/index.ts create mode 100644 integration/templates/expo-native/modules/verify-launch-config/ios/VerifyLaunchConfig.podspec create mode 100644 integration/templates/expo-native/modules/verify-launch-config/ios/VerifyLaunchConfigModule.swift create mode 100644 integration/templates/expo-native/screens/CustomSignIn.tsx create mode 100644 integration/templates/expo-native/screens/CustomSignUp.tsx create mode 100644 integration/templates/expo-native/screens/TokenCache.tsx create mode 100644 integration/templates/expo-native/screens/styles.ts create mode 100644 integration/templates/expo-native/verify/VerifyHost.tsx create mode 100644 integration/templates/expo-native/verify/launch.ts create mode 100644 integration/templates/expo-native/verify/state.ts diff --git a/.changeset/large-aliens-arrive.md b/.changeset/large-aliens-arrive.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/large-aliens-arrive.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/integration/templates/expo-native/App.tsx b/integration/templates/expo-native/App.tsx index bfac480af41..13198a4f14a 100644 --- a/integration/templates/expo-native/App.tsx +++ b/integration/templates/expo-native/App.tsx @@ -2,14 +2,22 @@ import { ClerkProvider, useAuth, useUser } from '@clerk/expo'; import { AuthView, UserButton, UserProfileView } from '@clerk/expo/native'; import { tokenCache } from '@clerk/expo/token-cache'; import { useState } from 'react'; -import { Button, Modal, StyleSheet, Text, View } from 'react-native'; +import { Button, LogBox, Modal, StyleSheet, Text, View } from 'react-native'; import { BiometricAvailabilityButton } from './components/BiometricAvailabilityButton'; import { GoogleSignInButton } from './components/GoogleSignInButton'; +import { readVerifyLaunch } from './verify/launch'; +import { logRequests, VerifyHost } from './verify/VerifyHost'; const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY; +const verifyLaunch = readVerifyLaunch(); -if (!publishableKey) { +if (verifyLaunch) { + LogBox.ignoreAllLogs(); + if (verifyLaunch.debugLogging) { + logRequests(); + } +} else if (!publishableKey) { throw new Error('Missing EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY'); } @@ -111,6 +119,15 @@ function NativeBuildFixture() { } export default function App() { + if (verifyLaunch) { + return ( + } + /> + ); + } + return ( + expoVersion.startsWith('57.') + ? { ...config, plugins: [...config.plugins, ['expo-build-properties', { ios: { enableSceneSupport: true } }]] } + : config; diff --git a/integration/templates/expo-native/metro.config.js b/integration/templates/expo-native/metro.config.js new file mode 100644 index 00000000000..58609d9362b --- /dev/null +++ b/integration/templates/expo-native/metro.config.js @@ -0,0 +1,37 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const { getDefaultConfig } = require('expo/metro-config'); +const { dependencies, devDependencies } = require('./package.json'); + +const config = getDefaultConfig(__dirname); +const monorepoRoot = path.resolve(__dirname, '../../..'); +const fixtureEntry = path.join(__dirname, 'index.js'); +const fixturePackages = new Set(Object.keys({ ...dependencies, ...devDependencies })); + +function linksToWorkspace() { + try { + return ( + fs.realpathSync(path.join(__dirname, 'node_modules/@clerk/expo')) === path.join(monorepoRoot, 'packages/expo') + ); + } catch { + return false; + } +} + +function packageName(moduleName) { + const [scope, name] = moduleName.split('/'); + return scope.startsWith('@') ? `${scope}/${name}` : scope; +} + +if (linksToWorkspace()) { + config.watchFolders = [monorepoRoot]; + config.resolver.resolveRequest = (context, moduleName, platform) => { + const fromWorkspace = !context.originModulePath.startsWith(__dirname + path.sep); + if (fromWorkspace && fixturePackages.has(packageName(moduleName))) { + return context.resolveRequest({ ...context, originModulePath: fixtureEntry }, moduleName, platform); + } + return context.resolveRequest(context, moduleName, platform); + }; +} + +module.exports = config; diff --git a/integration/templates/expo-native/modules/verify-launch-config/android/build.gradle b/integration/templates/expo-native/modules/verify-launch-config/android/build.gradle new file mode 100644 index 00000000000..308e30af523 --- /dev/null +++ b/integration/templates/expo-native/modules/verify-launch-config/android/build.gradle @@ -0,0 +1,15 @@ +plugins { + id 'com.android.library' + id 'expo-module-gradle-plugin' +} + +group = 'expo.modules.verifylaunchconfig' +version = '1.0.0' + +android { + namespace "expo.modules.verifylaunchconfig" + defaultConfig { + versionCode 1 + versionName "1.0.0" + } +} diff --git a/integration/templates/expo-native/modules/verify-launch-config/android/src/main/AndroidManifest.xml b/integration/templates/expo-native/modules/verify-launch-config/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..bdae66c8f5a --- /dev/null +++ b/integration/templates/expo-native/modules/verify-launch-config/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/integration/templates/expo-native/modules/verify-launch-config/android/src/main/java/expo/modules/verifylaunchconfig/VerifyLaunchConfigModule.kt b/integration/templates/expo-native/modules/verify-launch-config/android/src/main/java/expo/modules/verifylaunchconfig/VerifyLaunchConfigModule.kt new file mode 100644 index 00000000000..776aeadb730 --- /dev/null +++ b/integration/templates/expo-native/modules/verify-launch-config/android/src/main/java/expo/modules/verifylaunchconfig/VerifyLaunchConfigModule.kt @@ -0,0 +1,34 @@ +package expo.modules.verifylaunchconfig + +import android.content.Context +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +private const val PREFERENCES = "verify_launch_config" +private const val STORAGE_SCOPE_KEY = "storageScope" +private val CLERK_STORAGE = listOf("clerk_preferences", "SecureStore") + +class VerifyLaunchConfigModule : Module() { + override fun definition() = ModuleDefinition { + Name("VerifyLaunchConfig") + + Function("readLaunchInputs") { + val extras = appContext.currentActivity?.intent?.extras ?: return@Function emptyMap() + extras.keySet() + .filter { it.startsWith("verify") } + .mapNotNull { key -> extras.getString(key)?.let { key to it } } + .toMap() + } + + Function("applyStorageScope") { scope: String -> + val context = appContext.reactContext ?: return@Function + val preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) + if (preferences.getString(STORAGE_SCOPE_KEY, null) == scope) { + return@Function + } + + CLERK_STORAGE.forEach { context.deleteSharedPreferences(it) } + preferences.edit().putString(STORAGE_SCOPE_KEY, scope).commit() + } + } +} diff --git a/integration/templates/expo-native/modules/verify-launch-config/expo-module.config.json b/integration/templates/expo-native/modules/verify-launch-config/expo-module.config.json new file mode 100644 index 00000000000..0d8cc5ab8c0 --- /dev/null +++ b/integration/templates/expo-native/modules/verify-launch-config/expo-module.config.json @@ -0,0 +1,9 @@ +{ + "platforms": ["apple", "android"], + "apple": { + "modules": ["VerifyLaunchConfigModule"] + }, + "android": { + "modules": ["expo.modules.verifylaunchconfig.VerifyLaunchConfigModule"] + } +} diff --git a/integration/templates/expo-native/modules/verify-launch-config/index.ts b/integration/templates/expo-native/modules/verify-launch-config/index.ts new file mode 100644 index 00000000000..ca15ce01a7b --- /dev/null +++ b/integration/templates/expo-native/modules/verify-launch-config/index.ts @@ -0,0 +1,8 @@ +import { requireOptionalNativeModule } from 'expo'; + +type VerifyLaunchConfigModule = { + readLaunchInputs(): Record; + applyStorageScope(scope: string): void; +}; + +export const VerifyLaunchConfig = requireOptionalNativeModule('VerifyLaunchConfig'); diff --git a/integration/templates/expo-native/modules/verify-launch-config/ios/VerifyLaunchConfig.podspec b/integration/templates/expo-native/modules/verify-launch-config/ios/VerifyLaunchConfig.podspec new file mode 100644 index 00000000000..0974bd2db66 --- /dev/null +++ b/integration/templates/expo-native/modules/verify-launch-config/ios/VerifyLaunchConfig.podspec @@ -0,0 +1,18 @@ +Pod::Spec.new do |s| + s.name = 'VerifyLaunchConfig' + s.version = '1.0.0' + s.summary = 'Reads verify launch inputs for the Expo native fixture' + s.author = '' + s.homepage = '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/clerk/javascript' + s.platforms = { :ios => '15.1' } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + } + + s.source_files = "**/*.swift" +end diff --git a/integration/templates/expo-native/modules/verify-launch-config/ios/VerifyLaunchConfigModule.swift b/integration/templates/expo-native/modules/verify-launch-config/ios/VerifyLaunchConfigModule.swift new file mode 100644 index 00000000000..3a6f26efbea --- /dev/null +++ b/integration/templates/expo-native/modules/verify-launch-config/ios/VerifyLaunchConfigModule.swift @@ -0,0 +1,34 @@ +import ExpoModulesCore +import Security + +public class VerifyLaunchConfigModule: Module { + private static let storageScopeKey = "VerifyLaunchConfig.storageScope" + private static let clerkDefaultsKeys = [ + "authStartIdentifier", + "authStartPhoneNumber", + "authStartPhoneNumberFieldIsActive", + "clerk_last_used_identifier_type", + ] + + public func definition() -> ModuleDefinition { + Name("VerifyLaunchConfig") + + Function("readLaunchInputs") { () -> [String: String] in + UserDefaults.standard + .volatileDomain(forName: UserDefaults.argumentDomain) + .filter { $0.key.hasPrefix("verify") } + .compactMapValues { $0 as? String } + } + + Function("applyStorageScope") { (scope: String) in + let defaults = UserDefaults.standard + guard defaults.string(forKey: Self.storageScopeKey) != scope else { + return + } + + SecItemDelete([kSecClass: kSecClassGenericPassword] as CFDictionary) + Self.clerkDefaultsKeys.forEach(defaults.removeObject(forKey:)) + defaults.set(scope, forKey: Self.storageScopeKey) + } + } +} diff --git a/integration/templates/expo-native/package.sdk-57.json b/integration/templates/expo-native/package.sdk-57.json index 9523431c830..070c763e076 100644 --- a/integration/templates/expo-native/package.sdk-57.json +++ b/integration/templates/expo-native/package.sdk-57.json @@ -10,7 +10,8 @@ "prebuild:ios": "expo prebuild --clean --platform ios" }, "dependencies": { - "expo": "~57.0.1", + "expo": "~57.0.23", + "expo-build-properties": "~57.0.22", "react": "19.2.8", "react-dom": "19.2.8", "react-native": "0.86.0" diff --git a/integration/templates/expo-native/screens/CustomSignIn.tsx b/integration/templates/expo-native/screens/CustomSignIn.tsx new file mode 100644 index 00000000000..55dbb6cd34b --- /dev/null +++ b/integration/templates/expo-native/screens/CustomSignIn.tsx @@ -0,0 +1,74 @@ +import { useSignIn } from '@clerk/expo'; +import { useState } from 'react'; +import { Button, Text, TextInput, View } from 'react-native'; + +import { styles } from './styles'; + +export function CustomSignIn() { + const { signIn, fetchStatus } = useSignIn(); + const [emailAddress, setEmailAddress] = useState(''); + const [code, setCode] = useState(''); + const [codeSent, setCodeSent] = useState(false); + const [error, setError] = useState(null); + const busy = fetchStatus === 'fetching'; + + const sendCode = async () => { + const { error } = await signIn.emailCode.sendCode({ emailAddress }); + setError(error?.message ?? null); + setCodeSent(!error); + }; + + const verifyCode = async () => { + const { error } = await signIn.emailCode.verifyCode({ code }); + if (error || signIn.status !== 'complete') { + setError(error?.message ?? `Sign-in is ${signIn.status}`); + return; + } + const finalized = await signIn.finalize(); + setError(finalized.error?.message ?? null); + }; + + return ( + + Custom sign-in + {codeSent ? ( + <> + +