Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .changeset/large-aliens-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
21 changes: 19 additions & 2 deletions integration/templates/expo-native/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down Expand Up @@ -111,6 +119,15 @@ function NativeBuildFixture() {
}

export default function App() {
if (verifyLaunch) {
return (
<VerifyHost
launch={verifyLaunch}
home={<NativeBuildFixture />}
/>
);
}

return (
<ClerkProvider
publishableKey={publishableKey}
Expand Down
6 changes: 6 additions & 0 deletions integration/templates/expo-native/app.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { version: expoVersion } = require('expo/package.json');

module.exports = ({ config }) =>
expoVersion.startsWith('57.')
? { ...config, plugins: [...config.plugins, ['expo-build-properties', { ios: { enableSceneSupport: true } }]] }
: config;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
37 changes: 37 additions & 0 deletions integration/templates/expo-native/metro.config.js
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -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<String, String>()
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()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"platforms": ["apple", "android"],
"apple": {
"modules": ["VerifyLaunchConfigModule"]
},
"android": {
"modules": ["expo.modules.verifylaunchconfig.VerifyLaunchConfigModule"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { requireOptionalNativeModule } from 'expo';

type VerifyLaunchConfigModule = {
readLaunchInputs(): Record<string, string>;
applyStorageScope(scope: string): void;
};

export const VerifyLaunchConfig = requireOptionalNativeModule<VerifyLaunchConfigModule>('VerifyLaunchConfig');
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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")

OnCreate {
if Self.launchInputs()["verifyLaunchId"] != nil {
Self.keepPasswordAutoFillOffNativeClerkViews()
}
}

Function("readLaunchInputs") { () -> [String: String] in
Self.launchInputs()
}

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)
}
}

private static func launchInputs() -> [String: String] {
UserDefaults.standard
.volatileDomain(forName: UserDefaults.argumentDomain)
.filter { $0.key.hasPrefix("verify") }
.compactMapValues { $0 as? String }
}

private static func keepPasswordAutoFillOffNativeClerkViews() {
setenv("CLERK_E2E_MODE", "1", 1)
}
}
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 @@ -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"
Expand Down
74 changes: 74 additions & 0 deletions integration/templates/expo-native/screens/CustomSignIn.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(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 (
<View style={styles.form}>
<Text style={styles.title}>Custom sign-in</Text>
{codeSent ? (
<>
<TextInput
testID='verify.customSignIn.code'
style={styles.input}
value={code}
onChangeText={setCode}
placeholder='Email code'
keyboardType='number-pad'
autoComplete='one-time-code'
/>
<Button
testID='verify.customSignIn.verifyCode'
title='Verify code'
disabled={busy}
onPress={() => void verifyCode()}
/>
</>
) : (
<>
<TextInput
testID='verify.customSignIn.emailAddress'
style={styles.input}
value={emailAddress}
onChangeText={setEmailAddress}
placeholder='Email address'
autoCapitalize='none'
keyboardType='email-address'
/>
<Button
testID='verify.customSignIn.sendCode'
title='Send code'
disabled={busy}
onPress={() => void sendCode()}
/>
</>
)}
{error && <Text testID='verify.customSignIn.error'>{error}</Text>}
</View>
);
}
Loading
Loading