Skip to content

Commit 2add795

Browse files
mikepitreclaude
andcommitted
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 <noreply@anthropic.com>
1 parent fc85d55 commit 2add795

19 files changed

Lines changed: 800 additions & 3 deletions

File tree

‎.changeset/large-aliens-arrive.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

‎integration/templates/expo-native/App.tsx‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@ import { ClerkProvider, useAuth, useUser } from '@clerk/expo';
22
import { AuthView, UserButton, UserProfileView } from '@clerk/expo/native';
33
import { tokenCache } from '@clerk/expo/token-cache';
44
import { useState } from 'react';
5-
import { Button, Modal, StyleSheet, Text, View } from 'react-native';
5+
import { Button, LogBox, Modal, StyleSheet, Text, View } from 'react-native';
66

77
import { BiometricAvailabilityButton } from './components/BiometricAvailabilityButton';
88
import { GoogleSignInButton } from './components/GoogleSignInButton';
9+
import { readVerifyLaunch } from './verify/launch';
10+
import { logRequests, VerifyHost } from './verify/VerifyHost';
911

1012
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
13+
const verifyLaunch = readVerifyLaunch();
1114

12-
if (!publishableKey) {
15+
if (verifyLaunch) {
16+
LogBox.ignoreAllLogs();
17+
if (verifyLaunch.debugLogging) {
18+
logRequests();
19+
}
20+
} else if (!publishableKey) {
1321
throw new Error('Missing EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY');
1422
}
1523

@@ -111,6 +119,15 @@ function NativeBuildFixture() {
111119
}
112120

113121
export default function App() {
122+
if (verifyLaunch) {
123+
return (
124+
<VerifyHost
125+
launch={verifyLaunch}
126+
home={<NativeBuildFixture />}
127+
/>
128+
);
129+
}
130+
114131
return (
115132
<ClerkProvider
116133
publishableKey={publishableKey}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const { version: expoVersion } = require('expo/package.json');
2+
3+
module.exports = ({ config }) =>
4+
expoVersion.startsWith('57.')
5+
? { ...config, plugins: [...config.plugins, ['expo-build-properties', { ios: { enableSceneSupport: true } }]] }
6+
: config;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const fs = require('node:fs');
2+
const path = require('node:path');
3+
const { getDefaultConfig } = require('expo/metro-config');
4+
const { dependencies, devDependencies } = require('./package.json');
5+
6+
const config = getDefaultConfig(__dirname);
7+
const monorepoRoot = path.resolve(__dirname, '../../..');
8+
const fixtureEntry = path.join(__dirname, 'index.js');
9+
const fixturePackages = new Set(Object.keys({ ...dependencies, ...devDependencies }));
10+
11+
function linksToWorkspace() {
12+
try {
13+
return (
14+
fs.realpathSync(path.join(__dirname, 'node_modules/@clerk/expo')) === path.join(monorepoRoot, 'packages/expo')
15+
);
16+
} catch {
17+
return false;
18+
}
19+
}
20+
21+
function packageName(moduleName) {
22+
const [scope, name] = moduleName.split('/');
23+
return scope.startsWith('@') ? `${scope}/${name}` : scope;
24+
}
25+
26+
if (linksToWorkspace()) {
27+
config.watchFolders = [monorepoRoot];
28+
config.resolver.resolveRequest = (context, moduleName, platform) => {
29+
const fromWorkspace = !context.originModulePath.startsWith(__dirname + path.sep);
30+
if (fromWorkspace && fixturePackages.has(packageName(moduleName))) {
31+
return context.resolveRequest({ ...context, originModulePath: fixtureEntry }, moduleName, platform);
32+
}
33+
return context.resolveRequest(context, moduleName, platform);
34+
};
35+
}
36+
37+
module.exports = config;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
plugins {
2+
id 'com.android.library'
3+
id 'expo-module-gradle-plugin'
4+
}
5+
6+
group = 'expo.modules.verifylaunchconfig'
7+
version = '1.0.0'
8+
9+
android {
10+
namespace "expo.modules.verifylaunchconfig"
11+
defaultConfig {
12+
versionCode 1
13+
versionName "1.0.0"
14+
}
15+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<manifest>
2+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package expo.modules.verifylaunchconfig
2+
3+
import android.content.Context
4+
import expo.modules.kotlin.modules.Module
5+
import expo.modules.kotlin.modules.ModuleDefinition
6+
7+
private const val PREFERENCES = "verify_launch_config"
8+
private const val STORAGE_SCOPE_KEY = "storageScope"
9+
private val CLERK_STORAGE = listOf("clerk_preferences", "SecureStore")
10+
11+
class VerifyLaunchConfigModule : Module() {
12+
override fun definition() = ModuleDefinition {
13+
Name("VerifyLaunchConfig")
14+
15+
Function("readLaunchInputs") {
16+
val extras = appContext.currentActivity?.intent?.extras ?: return@Function emptyMap<String, String>()
17+
extras.keySet()
18+
.filter { it.startsWith("verify") }
19+
.mapNotNull { key -> extras.getString(key)?.let { key to it } }
20+
.toMap()
21+
}
22+
23+
Function("applyStorageScope") { scope: String ->
24+
val context = appContext.reactContext ?: return@Function
25+
val preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
26+
if (preferences.getString(STORAGE_SCOPE_KEY, null) == scope) {
27+
return@Function
28+
}
29+
30+
CLERK_STORAGE.forEach { context.deleteSharedPreferences(it) }
31+
preferences.edit().putString(STORAGE_SCOPE_KEY, scope).commit()
32+
}
33+
}
34+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"platforms": ["apple", "android"],
3+
"apple": {
4+
"modules": ["VerifyLaunchConfigModule"]
5+
},
6+
"android": {
7+
"modules": ["expo.modules.verifylaunchconfig.VerifyLaunchConfigModule"]
8+
}
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { requireOptionalNativeModule } from 'expo';
2+
3+
type VerifyLaunchConfigModule = {
4+
readLaunchInputs(): Record<string, string>;
5+
applyStorageScope(scope: string): void;
6+
};
7+
8+
export const VerifyLaunchConfig = requireOptionalNativeModule<VerifyLaunchConfigModule>('VerifyLaunchConfig');
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Pod::Spec.new do |s|
2+
s.name = 'VerifyLaunchConfig'
3+
s.version = '1.0.0'
4+
s.summary = 'Reads verify launch inputs for the Expo native fixture'
5+
s.author = ''
6+
s.homepage = 'https://github.com/clerk/javascript'
7+
s.platforms = { :ios => '15.1' }
8+
s.source = { git: '' }
9+
s.static_framework = true
10+
11+
s.dependency 'ExpoModulesCore'
12+
13+
s.pod_target_xcconfig = {
14+
'DEFINES_MODULE' => 'YES',
15+
}
16+
17+
s.source_files = "**/*.swift"
18+
end

0 commit comments

Comments
 (0)