
Integrating Clerk Expo
- 66 installs
- 3 repo stars
- Updated June 29, 2026
- tristanmanchester/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
integrating-clerk-expo is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- integrating-clerk-expo
- AI & Agent Building
- AI-coding skill
Integrating Clerk Expo by the numbers
- 66 all-time installs (skills.sh)
- Ranked #6,006 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tristanmanchester/agent-skills --skill integrating-clerk-expoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 66 |
|---|---|
| repo stars | ★ 3 |
| Last updated | June 29, 2026 |
| Repository | tristanmanchester/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Clerk authentication in Expo (React Native)
Key constraints (read first)
- Expo native apps do not support Clerk email links. Prefer email verification codes (
email_code) or other strategies. - Clerk prebuilt UI components are not supported on Expo native. Use custom flows (your own screens) plus the control components:
<ClerkLoaded>, <ClerkLoading>, <SignedIn>, <SignedOut>, <Protect>.
- Default session tokens are in-memory; for native apps you almost always want a secure token cache (Expo SecureStore).
What this skill does
When implementing Clerk in an Expo app, follow these workflows to:
- Install and configure
@clerk/clerk-expoand environment keys - Wrap the app in
<ClerkProvider>with a securetokenCache - Build custom sign-in/sign-up screens (email + password, email codes)
- Protect routes/screens in Expo Router or React Navigation
- Add OAuth / Enterprise SSO flows via
useSSO()(and fix redirect/deeplink pitfalls) - Add native Sign in with Apple (iOS, native build) via
useSignInWithApple() - Add optional biometric re-auth via
useLocalCredentials() - Optionally enable experimental offline bootstrapping via
__experimental_resourceCache - Prepare for production deployment (domain + allowlisted redirect URLs)
Fast checklist (default: Expo Router)
Copy/paste and tick off:
- [ ] In Clerk Dashboard, enable Native API for the application.
- [ ] Install:
@clerk/clerk-expo - [ ] Add
.env:EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=... - [ ] Wrap the root with
<ClerkProvider tokenCache={tokenCache}> - [ ] Install SecureStore and use Clerk’s
tokenCachehelper - [ ] Create
(auth)route group withsign-in.tsx,sign-up.tsx, and an(auth)/_layout.tsxredirect guard - [ ] Add a sign-out button using
useClerk().signOut() - [ ] Protect signed-in content using
<SignedIn>/<SignedOut>/<Protect>oruseAuth()+ redirects - [ ] If using OAuth/SSO: implement
useSSO()+expo-auth-sessionredirect URL +WebBrowser.maybeCompleteAuthSession() - [ ] If iOS Apple sign-in is required: native build +
useSignInWithApple() - [ ] If you need resilience offline:
__experimental_resourceCache={resourceCache}and handlenetwork_error - [ ] Production: acquire a domain and allowlist mobile SSO redirect URLs
If you need full code examples, open:
- `references/QUICKSTART.md`
- `references/CUSTOM_FLOWS.md`
- `references/SSO_OAUTH.md`
- `references/HOOKS_AND_COMPONENTS.md`
---
Decide the routing setup
If the project uses Expo Router
Signals:
expo-routerdependency- an
app/directory with route files likeapp/_layout.tsx
➡️ Follow: Expo Router workflow (below).
If the project uses React Navigation directly (no Expo Router)
Signals:
App.tsxwithNavigationContainerand stacks- no
app/directory
➡️ Follow: React Navigation workflow (below).
---
Workflow A — Expo Router (recommended default)
1) Install + env key
- Install
@clerk/clerk-expo - Set
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEYin.env
2) Root layout: ClerkProvider + secure token cache
In app/_layout.tsx (or the project’s root layout), wrap your app:
import { ClerkProvider } from '@clerk/clerk-expo'
import { tokenCache } from '@clerk/clerk-expo/token-cache'
import { Slot } from 'expo-router'
export default function RootLayout() {
return (
<ClerkProvider tokenCache={tokenCache}>
<Slot />
</ClerkProvider>
)
}Notes:
- Ensure
expo-secure-storeis installed (required by thetokenCachehelper). - Keep only the publishable key in the client.
3) Auth route group + redirect guard
Create app/(auth)/_layout.tsx that redirects signed-in users away from auth screens:
import { Redirect, Stack } from 'expo-router'
import { useAuth } from '@clerk/clerk-expo'
export default function AuthLayout() {
const { isSignedIn } = useAuth()
if (isSignedIn) return <Redirect href="/" />
return <Stack />
}4) Build custom sign-in / sign-up screens
Use Clerk hooks (useSignIn, useSignUp) and prefer email_code verification.
See:
- `references/CUSTOM_FLOWS.md`
5) Protect signed-in routes
Options (pick one, don’t mix randomly):
- Declarative: Wrap signed-in areas with
<SignedIn>and<SignedOut>. - Gate a subtree: Use
<Protect>around content that requires auth. - Imperative: In a layout, check
useAuth()and<Redirect>to/sign-in.
See:
- `references/ROUTING.md`
---
Workflow B — React Navigation (no Expo Router)
1) Wrap the root
Wrap your NavigationContainer (or your app root) with <ClerkProvider tokenCache={tokenCache}>.
2) Split navigation by auth state
- While
!isLoaded: render a splash/loading screen - When
isSignedIn: render your “app” stack - Else: render your “auth” stack
See:
- `references/ROUTING.md`
---
OAuth / Enterprise SSO in Expo (useSSO)
Use useSSO() for OAuth + enterprise SSO. In native, you must provide a valid redirect URL (often via AuthSession.makeRedirectUri(...)) and ensure your redirect URLs are allowlisted for production.
See:
- `references/SSO_OAUTH.md`
- `references/DEPLOYMENT.md`
---
Sign in with Apple (iOS native build)
useSignInWithApple() is iOS-only and requires a native build (won’t work in Expo Go). Always handle ERR_REQUEST_CANCELED.
See:
- `references/APPLE_SIGNIN.md`
---
Biometrics (store local credentials)
useLocalCredentials() can store password credentials on-device and allow biometric sign-in later (Face ID / fingerprint). Only works for password-based sign-in attempts.
See:
- `references/BIOMETRICS.md`
Passkeys
Clerk supports passkeys (WebAuthn). In Expo apps, you’ll integrate passkeys through custom flows; follow Clerk’s passkeys reference for the latest supported strategies and platform constraints.
See:
- `references/PASSKEYS.md`
---
Offline support (experimental)
You can enable experimental offline bootstrapping via __experimental_resourceCache. Treat as experimental; add good error handling for network_error.
See:
- `references/OFFLINE_SUPPORT.md`
---
Validation loop (recommended)
1) Run the setup verifier:
python scripts/verify_expo_clerk_setup.py .2) Start Expo with a clean cache:
npx expo start -c3) Test flows:
- fresh install → sign up → verify code → app screen
- app restart → user still signed in (token cache working)
- sign out → user returns to auth stack (token cleared)
---
THE EXACT PROMPT — Implement Clerk in an Expo app
Use this when delegating to another coding agent:
You are implementing Clerk authentication in a React Native Expo app.
1) Detect whether this app uses Expo Router (app/ directory) or React Navigation.
2) Install and configure @clerk/clerk-expo with EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY.
3) Wrap the root with <ClerkProvider tokenCache={tokenCache}> and ensure expo-secure-store is installed.
4) Implement custom sign-in and sign-up screens (email + password, with email verification code strategy 'email_code').
5) Protect signed-in routes appropriately for the chosen router.
6) If OAuth/SSO is requested, implement useSSO() with expo-auth-session redirectUrl and WebBrowser.maybeCompleteAuthSession().
7) Add sign-out.
8) Provide a short test plan and run scripts/verify_expo_clerk_setup.py.
Be precise and keep changes minimal. Do not use email link flows on native.---
Quick search (when reading bundled references)
grep -Rni "useSSO" references/
grep -Rni "tokenCache" references/
grep -Rni "enterprise_sso" references/
grep -Rni "__experimental_resourceCache" references/Sign in with Apple (Expo) — useSignInWithApple()
Requirements
- iOS only.
- Requires a native build (won’t work in Expo Go).
- You must add native Sign in with Apple support for Expo (including
expo-apple-authentication).
Hook API
useSignInWithApple() returns startAppleAuthenticationFlow(params?).
Returned value includes:
createdSessionId(string | null)setActive(params)(to activate session)- optional
signIn/signUpresources (for remaining requirements)
Minimal button component
import { useSignInWithApple } from '@clerk/clerk-expo'
import { useRouter } from 'expo-router'
import { Alert, Platform, TouchableOpacity, Text } from 'react-native'
export function AppleSignInButton() {
const { startAppleAuthenticationFlow } = useSignInWithApple()
const router = useRouter()
if (Platform.OS !== 'ios') return null
const onPress = async () => {
try {
const { createdSessionId, setActive } = await startAppleAuthenticationFlow()
if (createdSessionId && setActive) {
await setActive({ session: createdSessionId })
router.replace('/')
}
} catch (err: any) {
// User cancelled the system prompt
if (err.code === 'ERR_REQUEST_CANCELED') return
Alert.alert('Apple Sign-In failed', err.message ?? 'Unknown error')
}
}
return (
<TouchableOpacity onPress={onPress}>
<Text>Sign in with Apple</Text>
</TouchableOpacity>
)
}Unsafe metadata during sign-up
You can pass unsafeMetadata to startAppleAuthenticationFlow({ unsafeMetadata: ... }). It’s copied to the created user’s User.unsafeMetadata after completion.
Error handling must-dos
- Always wrap
startAppleAuthenticationFlow()intry/catch. - Handle
ERR_REQUEST_CANCELEDseparately (don’t show an error UI). - If you see “missing package” errors, ensure
expo-apple-authenticationis installed and you’re on a native build.
Biometrics / local credentials (useLocalCredentials)
useLocalCredentials() lets you store a user’s password credentials on-device and later sign them in using biometrics (Face ID / fingerprint).
Constraints
- Native only (not supported on web).
- Requires
@clerk/clerk-expo >= 2.2.0. - Works only for password sign-in attempts.
- Credentials are removed if the device passcode is removed.
Returned fields/methods (high level)
hasCredentials: boolean— any credentials stored?userOwnsCredentials: boolean— do stored creds belong to the signed-in user?biometricType: 'face-recognition' | 'fingerprint' | nullsetCredentials({ identifier?, password })clearCredentials()authenticate()— reads stored creds and starts a password sign-in attempt (returns SignInResource)
Suggested UX pattern
After a successful password sign-in
Offer a “Enable Face ID / Touch ID” toggle:
import { useLocalCredentials } from '@clerk/clerk-expo'
const { setCredentials } = useLocalCredentials()
await setCredentials({ identifier: email, password })On the next app launch (signed out)
Show a “Sign in with Face ID” button if hasCredentials is true:
import { useLocalCredentials } from '@clerk/clerk-expo'
import { useSignIn } from '@clerk/clerk-expo'
const { authenticate, hasCredentials } = useLocalCredentials()
const { setActive } = useSignIn()
const onBiometricPress = async () => {
const signInAttempt = await authenticate()
if (signInAttempt.status === 'complete') {
await setActive({ session: signInAttempt.createdSessionId })
} else {
// handle other states (MFA etc)
}
}Signing out
If you want to remove local creds on sign-out, call clearCredentials() explicitly (this is an app decision).
Custom auth flows for Expo native (no prebuilt UI)
Expo native apps require you to build your own UI using Clerk hooks/resources.
Core rules
- Always check
isLoadedbefore calling Clerk resource methods. - Expo native does not support email links → prefer
email_codeverification. - Treat Clerk resource status as a state machine; branch on
status.
---
Email + password sign-up with email code verification
High-level state machine:
1) Collect emailAddress + password 2) signUp.create({ emailAddress, password }) 3) signUp.prepareEmailAddressVerification({ strategy: 'email_code' }) 4) Collect code input 5) signUp.attemptEmailAddressVerification({ code }) 6) If status === 'complete', call setActive({ session: createdSessionId }) 7) If session.currentTask exists, route to a “tasks” screen
Minimal skeleton:
import * as React from 'react'
import { useSignUp } from '@clerk/clerk-expo'
export function SignUpScreen() {
const { isLoaded, signUp, setActive } = useSignUp()
const [emailAddress, setEmailAddress] = React.useState('')
const [password, setPassword] = React.useState('')
const [code, setCode] = React.useState('')
const [pendingVerification, setPendingVerification] = React.useState(false)
const onSignUpPress = async () => {
if (!isLoaded) return
await signUp.create({ emailAddress, password })
await signUp.prepareEmailAddressVerification({ strategy: 'email_code' })
setPendingVerification(true)
}
const onVerifyPress = async () => {
if (!isLoaded) return
const attempt = await signUp.attemptEmailAddressVerification({ code })
if (attempt.status === 'complete') {
await setActive({ session: attempt.createdSessionId })
}
}
// render based on pendingVerification...
}---
Email + password sign-in
High-level flow:
1) signIn.create({ identifier, password }) 2) If status === 'complete', call setActive({ session: createdSessionId }) 3) Otherwise, branch based on status:
needs_first_factorneeds_second_factorneeds_identifier- etc
Minimal skeleton:
import * as React from 'react'
import { useSignIn } from '@clerk/clerk-expo'
export function SignInScreen() {
const { isLoaded, signIn, setActive } = useSignIn()
const [identifier, setIdentifier] = React.useState('')
const [password, setPassword] = React.useState('')
const onSignInPress = async () => {
if (!isLoaded) return
const attempt = await signIn.create({ identifier, password })
if (attempt.status === 'complete') {
await setActive({ session: attempt.createdSessionId })
} else {
// handle MFA / other requirements based on attempt.status
}
}
}---
Sign out
Use useClerk():
import { useClerk } from '@clerk/clerk-expo'
export function SignOutButton() {
const { signOut } = useClerk()
return <Button title="Sign out" onPress={() => signOut()} />
}---
Error handling
- Wrap resource calls in
try/catch. - Prefer displaying Clerk errors to the user (mapped to form fields).
- For offline support, specifically detect
network_error(seereferences/OFFLINE_SUPPORT.md).
---
Session tasks
Some sign-in/up flows may require session tasks after authentication. When you call setActive(), you can provide a navigate callback and route users to a custom “tasks” UI if session.currentTask exists.
See references/SSO_OAUTH.md for a concrete setActive({ navigate }) pattern.
Deploying an Expo app with Clerk (production notes)
1) You still need a domain
Even if your product is “mobile-only”, Clerk production instances require a domain.
2) Allowlist redirect URLs for mobile SSO
Clerk ensures security-critical nonces are only passed to allowlisted URLs when SSO completes in native browsers/webviews. For production you should allowlist your mobile redirect URLs.
Where:
- Clerk Dashboard → Native applications
- Section: “Allowlist for mobile SSO redirect”
Default:
- Clerk typically uses
{bundleIdentifier}://callbackas the redirect URL.
3) Build and release
After domain + allowlists are configured, proceed with your normal Expo release pipeline (EAS builds / store submission).
4) OTA updates (recommended)
Expo over-the-air updates make it easier to ship Clerk SDK updates (security patches, behaviour changes) without resubmitting binaries.
Hooks + components you can rely on (Expo SDK)
Expo-specific hooks
useSSO()— OAuth + enterprise SSO flows viastartSSOFlow().useSignInWithApple()— native Sign in with Apple on iOS (native build).useLocalCredentials()— store password creds locally and use biometrics to sign in later.
Common hooks inherited from Clerk React SDK
These work in Expo because the Expo SDK is built on top of Clerk’s React SDK:
useAuth()— auth state +getToken()for calling your backenduseUser()— current user object (profile data, unsafe metadata, etc)useClerk()— access Clerk methods likesignOut()useSignIn()— sign-in resource for custom sign-in flowsuseSignUp()— sign-up resource for custom sign-up flowsuseSession()/useSessionList()— session infouseOrganization()/useOrganizationList()— organisations (B2B)
Control components (native-safe)
Use these for gating render based on auth/load state:
<ClerkLoaded>/<ClerkLoading>— render based on SDK load state<SignedIn>/<SignedOut>— render based on auth state<Protect>— protect a subtree that requires auth
Note: Prebuilt auth UI components (e.g. “SignIn”, “SignUp” screens) are web-only. For native, build your own screens.
Offline support (experimental) for Expo Clerk SDK
This is based on Clerk’s experimental offline support for Expo.
What it does (conceptually)
- Lets the Clerk SDK bootstrap offline using cached resources.
- Makes
isLoadedresolve more reliably when offline (one fetch attempt, then fallback). - Allows
useAuth().getToken()to return cached tokens when offline. - Surfaces network errors (so you can handle them in custom flows).
Enable: tokenCache + __experimental_resourceCache
import { ClerkProvider } from '@clerk/clerk-expo'
import { tokenCache } from '@clerk/clerk-expo/token-cache'
import { resourceCache } from '@clerk/clerk-expo/resource-cache'
import { Slot } from 'expo-router'
export default function RootLayout() {
return (
<ClerkProvider
tokenCache={tokenCache}
__experimental_resourceCache={resourceCache}
>
<Slot />
</ClerkProvider>
)
}Notes:
- Requires
expo-secure-store. - Treat as experimental: test thoroughly and watch Clerk changelogs.
Handle network errors in custom flows
When offline, calls like signIn.create() can throw a Clerk runtime error with code === 'network_error'.
import { useSignIn, isClerkRuntimeError } from '@clerk/clerk-expo'
try {
await signIn.create({ identifier, password })
} catch (err) {
if (isClerkRuntimeError(err) && err.code === 'network_error') {
// Show “You appear to be offline” UI
}
// Log / show other errors
}Passkeys (high-level notes)
Clerk supports passkeys (built on the WebAuthn standard). Passkeys are intended as a more secure, user-friendly alternative to passwords.
When to consider passkeys in an Expo app
- You want phishing-resistant authentication
- You already support “email + password” and want a stronger/less-friction option
- You need to reduce password resets and improve conversion
Implementation approach
1) Enable/configure passkeys in Clerk (dashboard + instance settings). 2) Update your custom sign-in/sign-up flow to offer passkey registration and sign-in where supported. 3) Treat passkeys as a factor that may interact with other requirements (e.g. MFA/session tasks). 4) Ensure your testing matrix covers:
- iOS vs Android devices
- Expo Go vs native builds (some auth mechanisms require native builds)
- Offline/poor connectivity behaviour
Because passkey support details can evolve quickly, always follow the latest “Configure passkeys” page in Clerk docs when implementing.
Expo + Clerk quickstart (condensed)
This is a condensed checklist based on Clerk’s Expo quickstart.
Checklist
- [ ] In Clerk Dashboard, enable Native API (required for native integration).
- [ ] Install
@clerk/clerk-expo - [ ] Add
.envwith:EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=... - [ ] Add
<ClerkProvider>to the root of the app - [ ] Add secure token caching via
expo-secure-store+tokenCache - [ ] Create custom auth screens (native uses custom flows)
- [ ] Add sign-out (
useClerk().signOut()) - [ ] (Recommended) enable OTA updates so Clerk fixes can ship without a store resubmission
Root layout (Expo Router)
import { ClerkProvider } from '@clerk/clerk-expo'
import { tokenCache } from '@clerk/clerk-expo/token-cache'
import { Stack } from 'expo-router'
export default function RootLayout() {
return (
<ClerkProvider tokenCache={tokenCache}>
<Stack>
{/* route groups like (home), (auth), etc */}
</Stack>
</ClerkProvider>
)
}Auth route group layout guard
import { Redirect, Stack } from 'expo-router'
import { useAuth } from '@clerk/clerk-expo'
export default function AuthRoutesLayout() {
const { isSignedIn } = useAuth()
if (isSignedIn) return <Redirect href="/" />
return <Stack />
}Sign-up outline (email + password + email code)
High-level flow:
1) signUp.create({ emailAddress, password }) 2) signUp.prepareEmailAddressVerification({ strategy: 'email_code' }) 3) signUp.attemptEmailAddressVerification({ code }) 4) When status === 'complete', call setActive({ session: createdSessionId })
Sign-in outline (identifier + password)
High-level flow:
1) signIn.create({ identifier, password }) 2) If status === 'complete', call setActive({ session: createdSessionId }) 3) If additional steps are required (e.g. MFA), follow the object’s status / supportedFirstFactors.
Sign out
import { useClerk } from '@clerk/clerk-expo'
export function SignOutButton() {
const { signOut } = useClerk()
return <Button title="Sign out" onPress={() => signOut()} />
}OTA updates (recommended)
Clerk recommends implementing over-the-air updates (Expo Updates) so security patches/feature updates can ship without resubmitting the app to marketplaces.
Routing + protection patterns (Expo Router and React Navigation)
Terminology
- Control components:
<SignedIn>,<SignedOut>,<Protect>,<ClerkLoaded>,<ClerkLoading> - Imperative guard: a layout/screen that checks
useAuth()and redirects
Keep the app consistent: pick one main pattern and apply it everywhere.
---
Expo Router patterns
Pattern 1 — Gate a whole route group with a layout redirect
Example: protect everything under app/(home)/...:
app/(home)/_layout.tsx
import { Redirect, Stack } from 'expo-router'
import { useAuth } from '@clerk/clerk-expo'
export default function HomeLayout() {
const { isLoaded, isSignedIn } = useAuth()
if (!isLoaded) return null // or a splash component
if (!isSignedIn) return <Redirect href="/(auth)/sign-in" />
return <Stack />
}Pros: simple mental model; works well for “signed-in areas”.
Pattern 2 — Declarative rendering with <SignedIn> / <SignedOut>
Example: a screen that shows different content based on auth:
import { SignedIn, SignedOut } from '@clerk/clerk-expo'
export default function Index() {
return (
<>
<SignedIn>{/* signed-in UI */}</SignedIn>
<SignedOut>{/* signed-out UI */}</SignedOut>
</>
)
}Pros: great for landing screens that can be “both”.
Pattern 3 — Protect a component subtree with <Protect>
import { Protect } from '@clerk/clerk-expo'
export function BillingSection() {
return (
<Protect>
{/* content that should only render for signed-in users */}
</Protect>
)
}Pros: convenient for “small guarded areas”.
---
React Navigation patterns (no Expo Router)
Pattern — AuthStack vs AppStack
import { ClerkProvider, useAuth } from '@clerk/clerk-expo'
import { tokenCache } from '@clerk/clerk-expo/token-cache'
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
const Stack = createNativeStackNavigator()
function RootNavigator() {
const { isLoaded, isSignedIn } = useAuth()
if (!isLoaded) return null // splash/loading
return (
<Stack.Navigator>
{isSignedIn ? (
<>
<Stack.Screen name="Home" component={HomeScreen} />
{/* other signed-in screens */}
</>
) : (
<>
<Stack.Screen name="SignIn" component={SignInScreen} />
<Stack.Screen name="SignUp" component={SignUpScreen} />
</>
)}
</Stack.Navigator>
)
}
export default function App() {
return (
<ClerkProvider tokenCache={tokenCache}>
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
</ClerkProvider>
)
}Notes:
- Always wait for
isLoadedbefore rendering logic that depends on Clerk. - Keep sign-in and sign-up screens “dumb” UI; centralise navigation logic in one place.
OAuth + Enterprise SSO in Expo (useSSO)
Prefer useSSO (useOAuth is deprecated)
If you see older code using useOAuth(), migrate to useSSO().
Required Expo packages (typical)
expo-auth-sessionexpo-web-browser
Universal setup: complete pending sessions and warm up Android browser
Put this in a module that is imported by the sign-in screen, or in the screen file itself:
import { Platform } from 'react-native'
import * as WebBrowser from 'expo-web-browser'
import { useEffect } from 'react'
// Handle any pending authentication sessions
WebBrowser.maybeCompleteAuthSession()
export const useWarmUpBrowser = () => {
useEffect(() => {
if (Platform.OS !== 'android') return
void WebBrowser.warmUpAsync()
return () => {
void WebBrowser.coolDownAsync()
}
}, [])
}OAuth connection example (e.g. Google)
import { useSSO } from '@clerk/clerk-expo'
import * as AuthSession from 'expo-auth-session'
import { useRouter } from 'expo-router'
import { useCallback } from 'react'
export function GoogleButton() {
const router = useRouter()
const { startSSOFlow } = useSSO()
const onPress = useCallback(async () => {
const { createdSessionId, setActive, signIn, signUp } = await startSSOFlow({
strategy: 'oauth_google',
// On native you must pass a redirectUrl (scheme-based).
redirectUrl: AuthSession.makeRedirectUri(),
})
if (createdSessionId) {
await setActive?.({
session: createdSessionId,
navigate: async ({ session }) => {
if (session?.currentTask) {
router.push('/sign-in/tasks')
return
}
router.push('/')
},
})
} else {
// Missing requirements (e.g. MFA) — use signIn/signUp returned from startSSOFlow
// and branch on their status.
}
}, [])
return <Pressable onPress={onPress}><Text>Continue with Google</Text></Pressable>
}Notes:
startSSOFlow()can returnsignInorsignUpobjects when requirements remain (MFA, missing fields).- Centralise this into a reusable helper if you support multiple providers.
Enterprise SSO example
Same pattern, but pass:
strategy: 'enterprise_sso'identifier: userEmail(the email the user typed)
Production allowlisting + redirect URLs
In production, Clerk requires allowlisted redirect URLs for mobile SSO completion.
- Acquire a domain for the production Clerk instance (required even for Expo apps).
- Allowlist your mobile SSO redirect URLs in Clerk Dashboard (Native applications).
- Clerk’s default redirect URL is typically
{bundleIdentifier}://callback.
See: references/DEPLOYMENT.md.
#!/usr/bin/env python3
"""verify_expo_clerk_setup.py
Static checks for integrating Clerk into a React Native Expo project.
Usage:
python scripts/verify_expo_clerk_setup.py <project_root>
Exit codes:
0 All required checks passed (or only minor warnings).
1 One or more required checks failed.
2 Invalid arguments.
3 Project files not found (e.g. package.json missing).
"""
from __future__ import annotations
import json
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, List, Optional, Tuple
EXCLUDE_DIRS = {
"node_modules",
".git",
".expo",
"dist",
"build",
"ios",
"android",
".turbo",
".next",
".cache",
".yarn",
}
@dataclass
class CheckResult:
name: str
ok: bool
details: str
required: bool = True
def _read_text(path: Path) -> str:
return path.read_text(encoding="utf-8", errors="ignore")
def _load_package_json(project_root: Path) -> Tuple[Optional[dict], Optional[str]]:
pkg_path = project_root / "package.json"
if not pkg_path.exists():
return None, f"Missing {pkg_path}"
try:
return json.loads(_read_text(pkg_path)), None
except Exception as e:
return None, f"Failed to parse package.json: {e}"
def _deps(pkg: dict) -> dict:
deps: dict = {}
for key in ("dependencies", "devDependencies", "peerDependencies"):
val = pkg.get(key, {})
if isinstance(val, dict):
deps.update(val)
return deps
def _walk_source_files(project_root: Path) -> Iterable[Path]:
for root, dirs, files in os.walk(project_root):
# prune dirs
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS and not d.startswith(".")]
for fn in files:
if fn.endswith((".ts", ".tsx", ".js", ".jsx")):
yield Path(root) / fn
def _find_in_files(project_root: Path, pattern: re.Pattern, max_hits: int = 20) -> List[Tuple[Path, int, str]]:
hits: List[Tuple[Path, int, str]] = []
for p in _walk_source_files(project_root):
try:
text = _read_text(p)
except Exception:
continue
for idx, line in enumerate(text.splitlines(), start=1):
if pattern.search(line):
hits.append((p, idx, line.strip()))
if len(hits) >= max_hits:
return hits
return hits
def _env_has_publishable_key(project_root: Path) -> Tuple[bool, str]:
env_path = project_root / ".env"
if not env_path.exists():
return False, "No .env file found"
text = _read_text(env_path)
# Match key=value and allow whitespace
if re.search(r"^\s*EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY\s*=\s*\S+", text, flags=re.M):
return True, "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY found"
return False, "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY not found in .env"
def main(argv: List[str]) -> int:
if len(argv) != 2:
print(__doc__.strip())
return 2
project_root = Path(argv[1]).resolve()
if not project_root.exists():
print(f"Project root does not exist: {project_root}")
return 3
pkg, err = _load_package_json(project_root)
if err:
print(err)
return 3
deps = _deps(pkg)
uses_expo_router = "expo-router" in deps
results: List[CheckResult] = []
# Dependency checks
results.append(
CheckResult(
name="Dependency: @clerk/clerk-expo",
ok="@clerk/clerk-expo" in deps,
details=deps.get("@clerk/clerk-expo", "missing"),
required=True,
)
)
results.append(
CheckResult(
name="Dependency: expo-secure-store",
ok="expo-secure-store" in deps,
details=deps.get("expo-secure-store", "missing"),
required=True,
)
)
# .env
has_env_key, env_details = _env_has_publishable_key(project_root)
results.append(
CheckResult(
name="Env: EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY",
ok=has_env_key,
details=env_details,
required=True,
)
)
# File layout hints (warning only)
if uses_expo_router:
layout_path = project_root / "app" / "_layout.tsx"
results.append(
CheckResult(
name="Expo Router: app/_layout.tsx exists",
ok=layout_path.exists(),
details=str(layout_path) if layout_path.exists() else f"missing {layout_path}",
required=False,
)
)
else:
app_tsx = project_root / "App.tsx"
results.append(
CheckResult(
name="React Navigation: App.tsx exists",
ok=app_tsx.exists(),
details=str(app_tsx) if app_tsx.exists() else f"missing {app_tsx}",
required=False,
)
)
# Search for ClerkProvider usage (required)
clerk_provider_hits = _find_in_files(project_root, re.compile(r"\bClerkProvider\b"))
has_clerk_provider = len(clerk_provider_hits) > 0
results.append(
CheckResult(
name="Code: ClerkProvider referenced",
ok=has_clerk_provider,
details=f"{len(clerk_provider_hits)} hits" if has_clerk_provider else "not found in .ts/.tsx/.js/.jsx",
required=True,
)
)
# Search for tokenCache usage (required for recommended native persistence)
token_cache_hits = _find_in_files(project_root, re.compile(r"@clerk/clerk-expo/token-cache|\btokenCache\b"))
has_token_cache = any("@clerk/clerk-expo/token-cache" in h[2] or "tokenCache" in h[2] for h in token_cache_hits)
results.append(
CheckResult(
name="Code: tokenCache referenced",
ok=has_token_cache,
details=f"{len(token_cache_hits)} hits" if has_token_cache else "not found",
required=True,
)
)
# Print summary
print(f"Project: {project_root}")
print(f"Detected router: {'expo-router' if uses_expo_router else 'react-navigation / unknown'}")
print()
failed_required = False
for r in results:
status = "✓" if r.ok else ("✗" if r.required else "⚠")
req = "required" if r.required else "optional"
print(f"{status} {r.name} ({req}) — {r.details}")
if r.required and not r.ok:
failed_required = True
# Helpful context for debugging
if clerk_provider_hits:
print("\nClerkProvider hits (first few):")
for p, line_no, line in clerk_provider_hits[:5]:
rel = p.relative_to(project_root)
print(f" - {rel}:{line_no}: {line}")
if token_cache_hits:
print("\nToken cache hits (first few):")
for p, line_no, line in token_cache_hits[:5]:
rel = p.relative_to(project_root)
print(f" - {rel}:{line_no}: {line}")
if failed_required:
print(
"""\nOne or more REQUIRED checks failed.
Common fixes:
- Install missing deps:
npm i @clerk/clerk-expo expo-secure-store
- Add EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY to .env
- Wrap your root in <ClerkProvider tokenCache={tokenCache}> (Expo Router: app/_layout.tsx)
"""
)
return 1
print("\nAll required checks passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))