
What Changed in Clerk Expo SDK 3.1
What changed in Clerk Expo SDK 3.1?
Clerk's Expo SDK 3.1 introduces native UI components (<AuthView />, <UserButton />, <UserProfileView />) powered by SwiftUI and Jetpack Compose, and platform-native Google Sign-In via system credential pickers without browser redirects. It builds on the Core 3 foundation from version 3.0, which includes the @clerk/expo package rename, the new signIn.finalize() custom flow API, the <Show> component for conditional rendering, and bundle size reductions. Read the sections below for details on implementing these native views, upgrading from @clerk/clerk-expo, and managing offline tokens.
What Changed: A Version Timeline
New in 3.1.0 (March 9, 2026)
- Native UI components:
<AuthView />,<UserButton />,<UserProfileView />(SwiftUI on iOS, Jetpack Compose on Android, beta) - Native Google Sign-In via ASAuthorization (iOS) and Credential Manager (Android)
useUserProfileModal(),useNativeSession(), anduseNativeAuthEvents()hooks announced (not part of the shipped public API surface — see New Hooks and APIs)- Expo SDK 55 support added to the peer dependency range
Core 3 Platform Changes (3.0, March 3, 2026)
- Package rename:
@clerk/clerk-expoto@clerk/expo publishableKeyprop required inClerkProvider<Show>component replaces<SignedIn>,<SignedOut>,<Protect>- Core 3 custom flow API:
signIn.finalize()replacessetActive()for custom flows (OAuth hooks still usesetActive()) getToken()throwsClerkOfflineErrorwhen offline instead of returningnullClerkexport removed: usegetClerkInstance()oruseClerk()@clerk/typesdeprecated: import types from the consolidated@clerk/shared/types(see Core 3 Upgrade Guide)- ~50KB gzipped bundle size reduction
- Expo SDK 53+ required, Node.js 20.9.0+
Older Capabilities Still Relevant When Evaluating 3.1
- Native Apple Sign-In (November 2025, predates Core 3; import path changed)
useLocalCredentials()for biometric authentication password storage (August 2024)useSSO()replacing the deprecateduseOAuth()for browser-based OAuth@clerk/expo-passkeysfor FIDO2/WebAuthn passkeys (separate package, experimental)
Core 3 Foundation
Expo SDK 3.1 is built on Clerk's Core 3 platform release, which modernizes APIs, improves React compatibility, and delivers performance improvements across all Clerk SDKs. Every Expo developer upgrading to 3.x encounters these changes.
Package Rename
The package has been renamed from @clerk/clerk-expo to @clerk/expo, aligning with the @clerk/<framework> naming convention used across all Clerk SDKs (@clerk/nextjs, @clerk/react, @clerk/tanstack-start).
// Before (Core 2)
import { ClerkProvider } from '@clerk/clerk-expo'
// After (Core 3)
import { ClerkProvider } from '@clerk/expo'The legacy @clerk/clerk-expo package is deprecated as of the Core 3 launch. The npx @clerk/upgrade CLI handles this rename automatically.
The Core 3 Custom Flow API
Core 3 introduces a redesigned custom flow API (referred to as the "Signal API" in the March 9, 2026 changelog) that replaces the legacy setActive() pattern for custom flows built with useSignIn() and useSignUp(). The new API uses step methods like signIn.password() and signIn.emailCode.sendCode() instead of signIn.attemptFirstFactor(), and signIn.finalize() instead of setActive().
Legacy Pattern vs. Core 3 Custom Flow API
The following example demonstrates the Core 3 custom flow pattern for email and password sign-in:
import { useState } from 'react'
import { View, TextInput, Text, Button } from 'react-native'
import { useSignIn } from '@clerk/expo'
import { useRouter } from 'expo-router'
function SignInScreen() {
const { signIn, errors, fetchStatus } = useSignIn()
const router = useRouter()
const [identifier, setIdentifier] = useState('')
const [password, setPassword] = useState('')
const handleSignIn = async () => {
await signIn.create({ identifier })
await signIn.password({ password })
if (signIn.status === 'complete') {
await signIn.finalize({
navigate: ({ session }) => router.replace('/(home)'),
})
}
}
return (
<View>
<TextInput value={identifier} onChangeText={setIdentifier} />
{errors?.fields.identifier && <Text>{errors.fields.identifier.message}</Text>}
<TextInput value={password} onChangeText={setPassword} secureTextEntry />
<Button
title={fetchStatus === 'fetching' ? 'Signing in...' : 'Sign in'}
onPress={handleSignIn}
disabled={fetchStatus === 'fetching'}
/>
</View>
)
}For a complete @clerk/expo walkthrough of this custom flow, see the custom email and password sign-in guide. The SignInFuture API reference documents every method and property used above — create(), password(), emailCode.*, mfa.*, and finalize().
The <Show> Component
Core 3 consolidates <SignedIn>, <SignedOut>, and <Protect> into a single <Show> component with a when prop.
Before (Core 2):
import { SignedIn, SignedOut, Protect } from '@clerk/clerk-expo'
function AuthLayout() {
return (
<>
<SignedIn>
<HomeScreen />
</SignedIn>
<SignedOut>
<SignInScreen />
</SignedOut>
<Protect role="admin" fallback={<Text>Not authorized</Text>}>
<AdminPanel />
</Protect>
</>
)
}After (Core 3):
import { Show } from '@clerk/expo'
function AuthLayout() {
return (
<>
<Show when="signed-in">
<HomeScreen />
</Show>
<Show when="signed-out">
<SignInScreen />
</Show>
<Show when={{ role: 'admin' }} fallback={<Text>Not authorized</Text>}>
<AdminPanel />
</Show>
</>
)
}The when prop accepts 'signed-in', 'signed-out', { role: '...' }, { permission: '...' }, { feature: '...' }, { plan: '...' }, or a callback (has) => boolean. See the Show component reference for the full API.
Performance Improvements
Core 3 delivers a ~50KB gzipped bundle size reduction by sharing React internals across Clerk packages instead of duplicating them. Token refresh is now proactive: session tokens (60-second JWTs) are refreshed in the background approximately every 50 seconds, preventing mid-request delays that occurred when tokens expired during API calls.
Native UI Components
Version 3.1 introduces three prebuilt native components available from @clerk/expo/native. These components render with SwiftUI on iOS and Jetpack Compose on Android. These are truly native views, not WebView wrappers. They automatically synchronize authentication state with the JavaScript SDK, so a sign-in completed in native UI is immediately reflected in React hooks like useAuth().
All three components are currently in beta. They are powered by the clerk-ios and clerk-android native SDKs, which are added to your project automatically by the @clerk/expo Expo config plugin.
<AuthView />
<AuthView /> renders a complete native authentication interface. It handles all auth flows configured in the Clerk Dashboard: email, phone, OAuth, passkeys, multi-factor authentication (MFA), and password recovery.
<AuthView /> handles Google and Apple sign-in automatically if enabled in the Dashboard—no useSignInWithGoogle() or expo-crypto required.
import { AuthView } from '@clerk/expo/native'
import { useAuth } from '@clerk/expo'
import { useEffect } from 'react'
import { useRouter } from 'expo-router'
export default function SignInScreen() {
const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false })
const router = useRouter()
useEffect(() => {
if (isSignedIn) {
router.replace('/(home)')
}
}, [isSignedIn])
return <AuthView mode="signInOrUp" />
}See the AuthView reference for the full API.
<UserButton />
<UserButton /> displays the signed-in user's avatar (image or initials fallback). Tapping it opens the native profile management modal. The component accepts no props; the parent container controls its size and shape.
import { UserButton } from '@clerk/expo/native'
import { View } from 'react-native'
function Header() {
return (
<View style={{ width: 36, height: 36, borderRadius: 18, overflow: 'hidden' }}>
<UserButton />
</View>
)
}Sign-out actions in the profile modal are automatically synchronized with the JavaScript SDK. See the UserButton reference.
<UserProfileView />
<UserProfileView /> renders the complete user profile interface inline. It manages personal information, email addresses, phone numbers, MFA settings, passkeys, connected accounts, active sessions, and sign-out.
There are two ways to surface it. The simplest is automatic: tapping the <UserButton /> shown above opens <UserProfileView /> as a native modal with no extra code. To embed the profile yourself, render it inline on its own screen:
import { UserProfileView } from '@clerk/expo/native'
import { View } from 'react-native'
function ProfileScreen() {
return (
<View style={{ flex: 1 }}>
<UserProfileView style={{ flex: 1 }} />
</View>
)
}To present it modally on demand, wrap <UserProfileView isDismissible /> in a React Native <Modal> and toggle its visibility from your own state.
See the UserProfileView reference.
State Management with Hooks
Native components require passing { treatPendingAsSignedOut: false } to useAuth(). Otherwise, the asynchronous "pending" phase during native-to-JavaScript session synchronization triggers premature signed-out evaluations and incorrect redirects.
import { useAuth } from '@clerk/expo'
import { useEffect } from 'react'
import { useRouter } from 'expo-router'
function AuthGate({ children }: { children: React.ReactNode }) {
const { isSignedIn, isLoaded } = useAuth({ treatPendingAsSignedOut: false })
const router = useRouter()
useEffect(() => {
if (isLoaded && !isSignedIn) {
router.replace('/sign-in')
}
}, [isLoaded, isSignedIn])
if (!isLoaded) return null
return <>{children}</>
}Web Fallback
Native components are iOS and Android only. For web builds in cross-platform Expo apps, use @clerk/expo/web which provides standard Clerk UI components (<SignIn />, <SignUp />, <UserButton />). Use React Native platform-specific file extensions (.ios.tsx, .android.tsx, .web.tsx) to separate native and web auth code. See the web support guide.
Native Sign-In
Native sign-in uses platform-native APIs (ASAuthorization on iOS, Credential Manager on Android) for social authentication instead of browser redirects, keeping users inside the app. This aligns with RFC 8252 requirements for system-level authentication.
Native Google Sign-In
Native Google Sign-In (new in 3.1) uses ASAuthorization on iOS and Credential Manager on Android. It runs via the NativeClerkGoogleSignIn TurboModule bundled in the @clerk/expo config plugin.
For custom UI implementations, use useSignInWithGoogle() from @clerk/expo/google:
import { useSignInWithGoogle } from '@clerk/expo/google'
import { Button, Alert } from 'react-native'
function GoogleSignInButton() {
const { startGoogleAuthenticationFlow } = useSignInWithGoogle()
const handleGoogleSignIn = async () => {
try {
const { createdSessionId, setActive } = await startGoogleAuthenticationFlow()
if (createdSessionId) {
await setActive({ session: createdSessionId })
}
} catch (error) {
if (error.code === 'SIGN_IN_CANCELLED' || error.code === '-5') {
return // User cancelled
}
Alert.alert('Error', 'Google sign-in failed. Please try again.')
}
}
return <Button title="Sign in with Google" onPress={handleGoogleSignIn} />
}Requirements for custom hook usage:
- Peer dependency:
expo-crypto - Three OAuth client IDs configured in the Clerk Dashboard: iOS, Android, and Web (the Web client ID is required for token verification even in native-only apps)
- Environment variables:
EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID,EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID,EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME,EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID - Development build required (not Expo Go)
See the useSignInWithGoogle reference and the Google Sign-In setup guide.
Native Apple Sign-In
Native Apple Sign-In (introduced November 2025) uses ASAuthorization and is iOS only. Its import path changed in Core 3.
import { useSignInWithApple } from '@clerk/expo/apple'
import { Button, Alert, Platform } from 'react-native'
function AppleSignInButton() {
const { startAppleAuthenticationFlow } = useSignInWithApple()
if (Platform.OS !== 'ios') return null
const handleAppleSignIn = async () => {
try {
const { createdSessionId, setActive } = await startAppleAuthenticationFlow()
if (createdSessionId) {
await setActive({ session: createdSessionId })
}
} catch (error) {
if (error.code === 'ERR_REQUEST_CANCELED') {
return // User cancelled
}
Alert.alert('Error', 'Apple sign-in failed.')
}
}
return <Button title="Sign in with Apple" onPress={handleAppleSignIn} />
}Requirements:
- Peer dependencies:
expo-apple-authentication+expo-crypto - Expo config plugin option
appleSignIndefaults totrue - Development build required
- Works on iOS Simulator with limitations (no biometric); test on physical device for production flows
See the useSignInWithApple reference and the Apple Sign-In setup guide.
Import Path Changes
Both native sign-in hooks moved to dedicated entry points in Core 3 to avoid bundling optional native dependencies when they are not used:
// Before (Core 2)
import { useSignInWithApple, useSignInWithGoogle } from '@clerk/expo'
// After (Core 3)
import { useSignInWithApple } from '@clerk/expo/apple'
import { useSignInWithGoogle } from '@clerk/expo/google'The npx @clerk/upgrade CLI detects and fixes these imports automatically.
Browser-Based OAuth via useSSO()
For OAuth providers without native hooks (GitHub, Discord, LinkedIn, etc.) or enterprise SSO, useSSO() replaces the deprecated useOAuth(). The key difference: useOAuth() required the strategy at hook instantiation, while useSSO() accepts it at flow invocation via startSSOFlow({ strategy: 'oauth_github' }). This makes useSSO() a single hook for all browser-based OAuth and enterprise SSO providers. See the useSSO reference.
New Hooks and APIs
useUserProfileModal()
The 3.1 changelog also announced useUserProfileModal() for presenting the native profile modal imperatively. In the shipped @clerk/expo public API, profile presentation is handled by the patterns shown earlier under Native UI Components — tapping <UserButton /> opens the profile modal automatically, and <UserProfileView /> can be rendered inline — so no separate hook import is required. Check the Expo SDK reference for the current hook surface before relying on an imperative API.
useNativeSession() and useNativeAuthEvents()
Clerk's 3.1 changelog announced two additional native hooks. As described there:
useNativeSession(): access to native SDK session management state (isSignedIn,sessionId,user,refresh()).useNativeAuthEvents(): listens for authentication state changes (signedIn,signedOut) emitted by native components.
In practice, most apps need neither hook: native components sync their authentication state to the JavaScript SDK automatically, so the fully documented useAuth() and useSession() remain the recommended way to read session state in React. Neither hook has a dedicated reference page yet — check the Expo SDK reference for the current, supported hook surface before relying on them.
Existing Features Worth Knowing
Not everything important in @clerk/expo 3.x is new. Several capabilities predate 3.1 but remain central when building or upgrading a mobile app — the most significant for returning users is biometric sign-in.
useLocalCredentials()
useLocalCredentials() stores passwords securely on-device for biometric sign-in (Face ID/Touch ID). It is distinct from passkeys, which use the experimental @clerk/expo-passkeys package.
The hook returns:
- Name
hasCredentials- Type
boolean- Description
Whether credentials are stored on device
- Name
userOwnsCredentials- Type
boolean- Description
Whether stored credentials belong to the signed-in user
- Name
biometricType- Type
'face-recognition' | 'fingerprint' | null- Description
Available biometric type
- Name
setCredentials()- Type
(opts) => Promise- Description
Store credentials after successful sign-in
- Name
clearCredentials()- Type
() => Promise- Description
Remove stored credentials
- Name
authenticate()- Type
() => Promise<SignInResource>- Description
Trigger biometric prompt and sign in
import { useLocalCredentials, useSignIn } from '@clerk/expo'
import { Button, Text, View } from 'react-native'
function BiometricSignIn() {
const { hasCredentials, biometricType, authenticate, setCredentials } = useLocalCredentials()
const { signIn } = useSignIn()
if (hasCredentials && biometricType) {
return (
<View>
<Text>Sign in with {biometricType === 'face-recognition' ? 'Face ID' : 'Touch ID'}</Text>
<Button
title="Use biometrics"
onPress={async () => {
const result = await authenticate()
if (result.status === 'complete') {
// Session is active
}
}}
/>
</View>
)
}
// Fall back to password sign-in, then call setCredentials() on success
return <Text>No stored credentials — use password sign-in</Text>
}Requirements: expo-local-authentication + expo-secure-store. Device must have an enrolled biometric and passcode. Works only with password-based sign-in. Not supported on web. See the local credentials guide.
Choosing an Authentication Approach
Expo developers now have three authentication paths:
- JavaScript-Only: Build custom React Native UI components with browser-based OAuth (
useSSO()). Works with Expo Go. Best for maximum customization. - JavaScript + Native Sign-In: Custom UI but with platform-native Google/Apple sign-in buttons. Requires a development build. Best for custom apps needing native social provider experiences.
- Full Native Components: Prebuilt SwiftUI and Jetpack Compose components (
<AuthView />,<UserButton />). Fastest integration path, handles Dashboard-configured auth flows automatically. Requires a development build.
Offline Support and Token Management
Token Caching with expo-secure-store
By default, Clerk stores session tokens in memory (lost on restart). For production, use the built-in tokenCache from @clerk/expo/token-cache. Backed by expo-secure-store, it requires no custom code:
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { Slot } from 'expo-router'
export default function RootLayout() {
return (
<ClerkProvider
publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!}
tokenCache={tokenCache}
>
<Slot />
</ClerkProvider>
)
}Session tokens are 60-second JSON Web Tokens that are proactively refreshed every ~50 seconds in the background. See How Clerk Works for the full token lifecycle.
ClerkOfflineError
In Core 3, getToken() throws ClerkOfflineError when the device is offline instead of returning null. This is a breaking change that resolves a long-standing ambiguity: previously, null could mean either "the user is signed out" or "the device is offline and token refresh failed." Now, null unambiguously means signed out, and ClerkOfflineError means offline.
import { useAuth } from '@clerk/expo'
import { ClerkOfflineError } from '@clerk/react/errors'
function ApiClient() {
const { getToken } = useAuth()
const fetchData = async () => {
try {
const token = await getToken()
if (!token) {
// User is signed out
return
}
// Make authenticated request with token
} catch (error) {
if (ClerkOfflineError.is(error)) {
// Device is offline — show cached data or retry later
return
}
throw error
}
}
}Experimental Offline Mode
The __experimental_resourceCache option enables resilient initialization and cached token fallback during network outages:
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { resourceCache } from '@clerk/expo/resource-cache'
import { Slot } from 'expo-router'
export default function RootLayout() {
return (
<ClerkProvider
publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!}
tokenCache={tokenCache}
__experimental_resourceCache={resourceCache}
>
<Slot />
</ClerkProvider>
)
}This caches environment config, client state, and session JWTs, enabling offline rendering of user info, role checks, and authenticated API calls with cached tokens. Write operations (sign-in, sign-up) still require network connectivity. This feature is experimental and not recommended as a production dependency. See the offline support guide.
Breaking Changes and Migration
This section covers the key breaking changes for Expo developers upgrading from @clerk/clerk-expo (Core 2) to @clerk/expo 3.x. For the full step-by-step migration walkthrough with code examples, see the Core 3 Upgrade Guide.
Using the Upgrade CLI
The fastest path to migration is the automated upgrade tool:
npx @clerk/upgradeThis CLI scans your codebase and applies AST-level transformations: it catches re-exports, aliased imports, and files across monorepo workspaces. It handles the package rename, import path updates, and component replacements automatically.
Client Trust
Client Trust (credential stuffing protection) predates Core 3, but developers upgrading custom password flows may encounter the needs_client_trust status if their app opted in or was created after November 2025.
Client Trust triggers when all three conditions are met: valid password entered, no MFA configured, and a new or unrecognized device. In the Core 3 custom flow API, handle it like this:
await signIn.password({ password })
if (signIn.status === 'needs_client_trust') {
// Check supported second factors for email code strategy
const emailCodeFactor = signIn.supportedSecondFactors?.find(
(factor) => factor.strategy === 'email_code',
)
if (emailCodeFactor) {
await signIn.mfa.sendEmailCode()
// After user enters the code:
await signIn.mfa.verifyEmailCode({ code: userEnteredCode })
}
}
if (signIn.status === 'complete') {
await signIn.finalize({ navigate: ({ session }) => router.replace('/(home)') })
}Client Trust is enabled by default for apps created after November 14, 2025. Existing apps must opt in via the Dashboard. See the Client Trust guide.
Migration Checklist
- Run
npx @clerk/upgrade(handles most codemods automatically) - Update Expo SDK to 53–55
- Verify package name updated:
@clerk/clerk-expo→@clerk/expo - Confirm
publishableKeyis explicit inClerkProvider - Update native sign-in hook import paths (
@clerk/expo/apple,@clerk/expo/google) - Replace
<SignedIn>/<SignedOut>/<Protect>with<Show> - Replace
Clerkexport withgetClerkInstance()oruseClerk() - Add
ClerkOfflineErrorhandling aroundgetToken()calls - Replace
setActive()withfinalize()in custom flows (not for OAuth hooks) - Handle
needs_client_trustin custom password sign-in flows - Test all authentication flows end-to-end
For the full migration walkthrough: Core 3 Upgrade Guide.
Implementation Notes
Plugin and Development Build
The @clerk/expo config plugin automatically adds the clerk-ios and clerk-android native SDKs to your project. Add it to app.json:
{
"expo": {
"plugins": [
[
"@clerk/expo",
{
"appleSignIn": true,
"keychainService": "my-app-keychain",
"theme": "./clerk-theme.json"
}
]
]
}
}The plugin accepts the following options:
Native components and native sign-in hooks require a development build. They can't run in Expo Go. Build with npx expo run:ios or npx expo run:android. Once built, JavaScript changes still hot-reload instantly. The JavaScript-only authentication approach works in Expo Go without a development build.
The optional theme JSON supports colors (14 hex tokens), darkColors, design.borderRadius, and design.fontFamily (iOS only). Changes to the theme file require npx expo prebuild --clean. See the theming reference.
Quick Start Example
This example demonstrates native components. For full setup (providers, env vars, Dashboard), see the Expo Quickstart.
Prerequisites: Clerk account with Native API enabled, native app registered in Dashboard, Expo SDK 53–55, development build.
Install:
npx expo install @clerk/expo expo-secure-storeHome screen with signed-in state check and native <UserButton />:
import { UserButton } from '@clerk/expo/native'
import { Show, useAuth } from '@clerk/expo'
import { useEffect } from 'react'
import { useRouter } from 'expo-router'
import { View } from 'react-native'
export default function HomeScreen() {
const { isSignedIn, isLoaded } = useAuth({ treatPendingAsSignedOut: false })
const router = useRouter()
useEffect(() => {
if (isLoaded && !isSignedIn) {
router.replace('/sign-in')
}
}, [isLoaded, isSignedIn])
return (
<Show when="signed-in">
<View style={{ flex: 1, alignItems: 'center', paddingTop: 60 }}>
<View style={{ width: 48, height: 48, borderRadius: 24, overflow: 'hidden' }}>
<UserButton />
</View>
</View>
</Show>
)
}Sign-in screen using the native <AuthView /> component:
import { AuthView } from '@clerk/expo/native'
export default function SignInScreen() {
return <AuthView mode="signInOrUp" />
}