# Migrating from @clerk/clerk-expo to @clerk/expo — Breaking Changes, Native Components, and the Complete Upgrade Path - Part 2

> Part 2 of 2. Start with [Migrating from @clerk/clerk-expo to @clerk/expo — Breaking Changes, Native Components, and the Complete Upgrade Path](https://clerk.com/articles/migrating-from-clerk-clerk-expo-to-clerk-expo-breaking-changes-native-components.md).

This is Part 2 of the migration guide for `@clerk/clerk-expo` to `@clerk/expo` (Core 3). Part 1 covered the core API upgrades and hook changes. This part explores adopting the new beta Native Components, platform-native authentication, passkeys, offline support, and multi-tenant authorization.

## Step 8: Adopting Native Components (Beta)

Native components are the biggest addition in `@clerk/expo` 3.1. They render authentication UI using SwiftUI on iOS and Jetpack Compose on Android ([Expo Native Components, 2026-03-09](https://clerk.com/changelog/2026-03-09-expo-native-components.md)).

> Native components are in beta. They require a development build and Expo SDK 53+.

### AuthView: Native Authentication Interface

`AuthView` renders a complete sign-in/sign-up flow using platform-native UI. It handles email/password, social login, passkeys, and MFA automatically.

filename: app/(auth)/sign-in.tsx
```tsx
import { AuthView } from '@clerk/expo/native'
import { useAuth } from '@clerk/expo'
import { useRouter } from 'expo-router'
import { useEffect } from 'react'

export default function SignInScreen() {
  const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false })
  const router = useRouter()

  useEffect(() => {
    if (isSignedIn) {
      router.replace('/(home)')
    }
  }, [isSignedIn])

  return <AuthView mode="signInOrUp" />
}
```

Props:

| Prop            | Type                                   | Default        | Description                                                                                                |
| --------------- | -------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------- |
| `mode`          | `'signIn' | 'signUp' | 'signInOrUp'` | `'signInOrUp'` | Controls which flow to display                                                                             |
| `isDismissible` | `boolean`                              | `true`         | When `true`, shows a dismiss button; when `false`, the user must complete authentication to close the view |
| `onDismiss`     | `() => void`                           | `undefined`    | Called when the view requests dismissal (the user dismisses it, or the native flow finishes)               |

> `isDismissible` defaults to `true`, which shows AuthView's own dismiss button. When you present `AuthView` inside a React Native `<Modal>`, set `isDismissible={false}` so the modal owns dismissal and you avoid two competing dismiss controls.

AuthView handles social sign-in flows automatically. You don't need `useSignInWithGoogle` or `useSignInWithApple` hooks (or their peer dependencies like `expo-crypto`) when using AuthView.

### UserButton: Native Profile Avatar

`UserButton` displays the user's avatar as a tappable circle. Tapping opens a native profile modal.

filename: app/(home)/\_layout.tsx
```tsx
import { Stack } from 'expo-router'
import { UserButton } from '@clerk/expo/native'
import { View } from 'react-native'

export default function HomeLayout() {
  return (
    <Stack
      screenOptions={{
        headerRight: () => (
          <View style={{ width: 36, height: 36, borderRadius: 18, overflow: 'hidden' }}>
            <UserButton />
          </View>
        ),
      }}
    >
      <Stack.Screen name="index" options={{ title: 'Home' }} />
    </Stack>
  )
}
```

`UserButton` has no props. The parent container controls its size and shape. Sign-out is handled automatically and synced with the JS SDK.

### UserProfileView: Full Profile Management

`UserProfileView` renders Clerk's full profile and account management UI natively. Render it inline, either in its own route or inside a React Native `<Modal>`:

filename: app/(home)/profile.tsx
```tsx
import { UserProfileView } from '@clerk/expo/native'
import { useAuth } from '@clerk/expo'

export default function ProfileScreen() {
  const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false })

  if (!isSignedIn) return null

  return <UserProfileView style={{ flex: 1 }} />
}
```

`UserProfileView` provides personal info, security settings, account switching, MFA, passkeys, connected accounts, and sign-out. Like `AuthView`, it accepts `isDismissible` (default `true`); set `isDismissible={false}` when you present it inside your own `<Modal>` so the modal owns dismissal.

### Session Synchronization

Native components run through a separate native SDK. `ClerkProvider` keeps the native and JavaScript sessions in sync automatically, so you don't add any sync component yourself:

1. Native auth completes and creates a session
2. The native session's token syncs to the JS SDK's token cache
3. The JS SDK picks up the session
4. React hooks reflect the new auth state

Use `useEffect` to react to auth state changes. Don't use imperative callbacks. Always set `treatPendingAsSignedOut` to `false` with native components to avoid a flash of signed-out content during sync.

### Web Compatibility

For Expo web projects, use `@clerk/expo/web` which provides prebuilt web components (`SignIn`, `SignUp`, `UserButton`, etc.). These throw on native. Keep native and web paths separate with platform checks.

## Step 9: Native Authentication Hooks

### Google Sign-In Without a WebView

`useSignInWithGoogle` uses platform-native APIs: ASAuthorization on iOS, Credential Manager on Android. No browser redirect.

Install the required peer dependency:

```bash
npx expo install expo-crypto
```

Configure 3 OAuth client IDs in the Google Cloud Console (iOS, Android, Web) and set them as environment variables ([Google Sign-In guide](https://clerk.com/docs/expo/guides/configure/auth-strategies/sign-in-with-google.md)).

filename: components/GoogleSignIn.tsx
```tsx
import { useSignInWithGoogle } from '@clerk/expo/google'
import { useRouter } from 'expo-router'
import { Alert, Platform, Pressable, Text } from 'react-native'

export function GoogleSignInButton() {
  const { startGoogleAuthenticationFlow } = useSignInWithGoogle()
  const router = useRouter()

  if (Platform.OS !== 'ios' && Platform.OS !== 'android') return null

  const onPress = async () => {
    try {
      const { createdSessionId, setActive } = await startGoogleAuthenticationFlow()

      if (createdSessionId && setActive) {
        await setActive({ session: createdSessionId })
        router.replace('/')
      }
    } catch (err: any) {
      if (err.code === 'SIGN_IN_CANCELLED' || err.code === '-5') return
      Alert.alert('Error', err.message || 'Google sign-in failed')
    }
  }

  return (
    <Pressable onPress={onPress}>
      <Text>Sign in with Google</Text>
    </Pressable>
  )
}
```

### Apple Sign-In (iOS Only)

Install the required peer dependencies:

```bash
npx expo install expo-apple-authentication expo-crypto
```

Register in the Clerk Dashboard with your Team ID + Bundle ID ([Apple Sign-In guide](https://clerk.com/docs/expo/guides/configure/auth-strategies/sign-in-with-apple.md)).

filename: components/AppleSignIn.tsx
```tsx
import { useSignInWithApple } from '@clerk/expo/apple'
import { useRouter } from 'expo-router'
import { Alert, Platform, Pressable, 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) {
      if (err.code === 'ERR_REQUEST_CANCELED') return
      Alert.alert('Error', err.message || 'Apple sign-in failed')
    }
  }

  return (
    <Pressable onPress={onPress}>
      <Text>Sign in with Apple</Text>
    </Pressable>
  )
}
```

### Biometric Authentication with Local Credentials

`useLocalCredentials` enables biometric authentication (Face ID, fingerprint) for password-based sign-in. It stores encrypted credentials on-device after the first password sign-in.

Install the required peer dependencies:

```bash
npx expo install expo-local-authentication expo-secure-store
```

Properties: `hasCredentials`, `userOwnsCredentials`, `biometricType` (`'face-recognition'` | `'fingerprint'` | `null`). Methods: `setCredentials()`, `clearCredentials()`, `authenticate()`.

Workflow:

1. User signs in with password
2. Call `setCredentials()` to store credentials
3. On future launches, call `authenticate()` for biometric sign-in

filename: app/(auth)/sign-in.tsx
```tsx
import { useSignIn, useClerk } from '@clerk/expo'
import { useLocalCredentials } from '@clerk/expo/local-credentials'
import { useState } from 'react'
import { Text, TextInput, Pressable, View } from 'react-native'
import { useRouter, type Href } from 'expo-router'

export default function SignInScreen() {
  const { signIn, errors, fetchStatus } = useSignIn()
  const { setActive } = useClerk()
  const { hasCredentials, setCredentials, authenticate, biometricType } = useLocalCredentials()
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const router = useRouter()

  // Biometric sign-in for returning users.
  // authenticate() returns a SignInResource (Core 2 type), so use
  // setActive() from useClerk() instead of signIn.finalize().
  const onBiometricSignIn = async () => {
    const result = await authenticate()
    if (result.status === 'complete') {
      await setActive({ session: result.createdSessionId })
      router.replace('/')
    }
  }

  // Password sign-in with credential storage (Core 3 authentication hook API)
  const onPasswordSignIn = async () => {
    await signIn.password({ emailAddress: email, password })

    if (signIn.status === 'complete') {
      // Store credentials for future biometric sign-in
      await setCredentials({ identifier: email, password })
      await signIn.finalize({
        navigate: ({ session, decorateUrl }) => {
          if (session?.currentTask) return
          router.push(decorateUrl('/') as Href)
        },
      })
    }
  }

  return (
    <View>
      {hasCredentials && biometricType ? (
        <Pressable onPress={onBiometricSignIn} disabled={fetchStatus === 'fetching'}>
          <Text>
            {biometricType === 'face-recognition'
              ? 'Sign in with Face ID'
              : 'Sign in with Fingerprint'}
          </Text>
        </Pressable>
      ) : null}

      <TextInput value={email} onChangeText={setEmail} placeholder="Email" />
      <TextInput value={password} onChangeText={setPassword} secureTextEntry />
      {errors?.fields?.identifier ? <Text>{errors.fields.identifier.message}</Text> : null}
      <Pressable onPress={onPasswordSignIn} disabled={fetchStatus === 'fetching'}>
        <Text>Sign In</Text>
      </Pressable>
    </View>
  )
}
```

> Local credentials only work for password-based sign-in on native platforms (not web). See the [Local Credentials guide](https://clerk.com/docs/guides/development/local-credentials.md).

## Step 10: Passkeys Configuration

Passkeys provide passwordless authentication using WebAuthn. This feature is experimental in `@clerk/expo`.

### Installation

```bash
npx expo install @clerk/expo-passkeys
npx expo prebuild
```

Enable passkeys in your Clerk Dashboard's authentication settings. Then configure `ClerkProvider`:

filename: app/\_layout.tsx
```tsx
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { passkeys } from '@clerk/expo/passkeys'
import { Slot } from 'expo-router'

export default function RootLayout() {
  return (
    <ClerkProvider
      publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!}
      tokenCache={tokenCache}
      __experimental_passkeys={passkeys}
    >
      <Slot />
    </ClerkProvider>
  )
}
```

> `@clerk/expo-passkeys` has a peer dependency of `expo >=53 <55`, which is narrower than `@clerk/expo`'s range of `>=53 <56`. If you're on Expo SDK 55, check for an updated version of `@clerk/expo-passkeys` before installing.

### iOS Requirements

- iOS 16+ required for passkeys (Apple added passkey support in iOS 16). The `@clerk/expo` config plugin raises the iOS deployment target to 17.0 automatically, which already clears this minimum, so no manual `expo-build-properties` step is needed.
- Register your app in Clerk Dashboard with App ID Prefix + Bundle ID (from Apple Developer portal's Identifiers page)
- Configure associated domains in `app.json`:

filename: app.json
```json
{
  "expo": {
    "ios": {
      "associatedDomains": [
        "applinks:<YOUR_FRONTEND_API_URL>",
        "webcredentials:<YOUR_FRONTEND_API_URL>"
      ]
    }
  }
}
```

### Android Requirements

- Android 9+ required
- **Physical device only.** Emulators don't support passkeys.
- Register in Clerk Dashboard with your package name and SHA256 certificate fingerprints
- Configure intent filters:

filename: app.json
```json
{
  "expo": {
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [{ "scheme": "https", "host": "<YOUR_FRONTEND_API_URL>" }],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}
```

Verify setup with Google's Statement List Generator tool.

### Passkey Methods (Core 3)

Create a passkey:

filename: components/CreatePasskey.tsx
```tsx
import { useUser } from '@clerk/expo'

function CreatePasskeyButton() {
  const { user } = useUser()

  const onCreate = async () => {
    await user?.createPasskey()
  }

  // render button...
}
```

Sign in with a passkey:

filename: components/PasskeySignIn.tsx
```tsx
import { useSignIn } from '@clerk/expo'
import { useRouter, type Href } from 'expo-router'

function PasskeySignIn() {
  const { signIn } = useSignIn()
  const router = useRouter()

  const onSignIn = async () => {
    await signIn.passkey({ flow: 'discoverable' })

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: ({ session, decorateUrl }) => {
          if (session?.currentTask) return
          router.push(decorateUrl('/') as Href)
        },
      })
    }
  }

  // render button...
}
```

Flow options: `'discoverable'` (requires user interaction) or `'autofill'` (prompts before interaction).

## Step 11: Offline Support and ClerkOfflineError

### Breaking Change: getToken() Behavior

In Core 2, `getToken()` returned `null` when offline. This was ambiguous: it could mean signed out or offline. Core 3 throws `ClerkOfflineError` after a \~15 second retry period, making the distinction explicit.

Before (Core 2):

filename: utils/api.tsx
```tsx
import { useAuth } from '@clerk/clerk-expo'

function useApiClient() {
  const { getToken } = useAuth()

  const fetchData = async () => {
    const token = await getToken()
    if (!token) {
      // Could be signed out OR offline. No way to tell.
      return null
    }
    // make API call with token
  }
}
```

After (Core 3, `@clerk/expo` `>=3.0.0`):

filename: utils/api.tsx
```tsx
import { useAuth } from '@clerk/expo'
import { ClerkOfflineError } from '@clerk/react/errors'

function useApiClient() {
  const { getToken } = useAuth()

  const fetchData = async () => {
    try {
      const token = await getToken()
      if (!token) {
        // Definitively signed out
        return null
      }
      // make API call with token
    } catch (error) {
      if (ClerkOfflineError.is(error)) {
        // Definitively offline. Show cached data or retry UI.
        return null
      }
      throw error
    }
  }
}
```

Expo's custom `useAuth` override adds JWT caching: if a network error occurs, it returns the cached token instead of throwing. This makes offline transitions smoother.

> `ClerkOfflineError.is()` is for `getToken()` calls specifically. For custom sign-in/sign-up flows, use `isClerkRuntimeError` from `@clerk/expo` with the `network_error` code instead:
>
> ```tsx
> import { isClerkRuntimeError } from '@clerk/expo'
>
> try {
>   await signIn.password({ emailAddress: email, password })
> } catch (err) {
>   if (isClerkRuntimeError(err) && err.code === 'network_error') {
>     // Handle offline scenario in custom flows
>   }
> }
> ```
>
> See the [Offline Support guide](https://clerk.com/docs/guides/development/offline-support.md) for details.

### Experimental Offline Support

For full offline resilience, pass `resourceCache` to `ClerkProvider`. It caches authentication state, environment data, and session JWTs using `expo-secure-store`.

filename: app/\_layout.tsx
```tsx
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>
  )
}
```

The resource cache stores authentication state using `expo-secure-store` for encrypted persistent storage ([Offline Support, 2024-12-12](https://clerk.com/changelog/2024-12-12-expo-offline-support.md)).

### Token Refresh Strategy

Clerk uses a hybrid auth model: client tokens (long-lived, on the FAPI domain) and session tokens (60-second expiry, on the app domain). The SDK handles token refresh automatically in the background, so sessions stay valid without manual intervention ([How Clerk Works](https://clerk.com/docs/guides/how-clerk-works/overview.md)). No code changes required.

## Step 12: Expo Router Protected Routes

### Layout-Based Route Protection

Use route groups with `_layout.tsx` files for authentication-based routing:

filename: app/(home)/\_layout.tsx
```tsx
import { useAuth } from '@clerk/expo'
import { Redirect, Stack } from 'expo-router'

export default function HomeLayout() {
  const { isSignedIn, isLoaded } = useAuth()

  if (!isLoaded) return null

  if (!isSignedIn) {
    return <Redirect href="/(auth)/sign-in" />
  }

  return <Stack />
}
```

The auth route layout redirects signed-in users away:

filename: app/(auth)/\_layout.tsx
```tsx
import { useAuth } from '@clerk/expo'
import { Redirect, Stack } from 'expo-router'

export default function AuthLayout() {
  const { isSignedIn, isLoaded } = useAuth()

  if (!isLoaded) return null

  if (isSignedIn) {
    return <Redirect href="/(home)" />
  }

  return <Stack />
}
```

### Authorization-Based Route Protection

Protect admin routes using `<Show>` with organization roles:

filename: app/(home)/admin/\_layout.tsx
```tsx
import { Show } from '@clerk/expo'
import { Stack } from 'expo-router'
import { Text } from 'react-native'

export default function AdminLayout() {
  return (
    <Show when={{ role: 'org:admin' }} fallback={<Text>Not authorized</Text>}>
      <Stack />
    </Show>
  )
}
```

> `useAuth()` and `useUser()` work with any navigation library (React Navigation, etc.), not only Expo Router. The auth state hooks are navigation-agnostic.

## Organizations and Multi-Tenant Authorization

Organizations in Core 3 use the same `<Show>` component for [multi-tenant](https://clerk.com/glossary/multi-tenancy.md) authorization checks.

### Organization Authorization Patterns

filename: app/(home)/dashboard.tsx
```tsx
import { Show } from '@clerk/expo'
import { Text, View } from 'react-native'

export default function Dashboard() {
  return (
    <View>
      <Show when={{ role: 'org:admin' }}>
        <Text>Admin panel: manage members and settings</Text>
      </Show>

      <Show when={{ permission: 'org:invoices:create' }}>
        <Text>Create and manage invoices</Text>
      </Show>

      <Show when={{ feature: 'premium_access' }}>
        <Text>Premium content for subscribers</Text>
      </Show>

      <Show when={{ plan: 'enterprise' }}>
        <Text>Enterprise features: SSO, audit logs</Text>
      </Show>

      <Show
        when={(has) => has({ role: 'org:admin' }) || has({ permission: 'org:billing:manage' })}
        fallback={<Text>Access restricted</Text>}
      >
        <Text>Billing management</Text>
      </Show>
    </View>
  )
}
```

### User Management

`UserProfileView` from `@clerk/expo/native` provides self-service user management including personal info, security settings, and account switching. Render it inline in its own route or inside a React Native `<Modal>`, as shown in Step 8.

For session management, the native SDK handles session lifecycle, switching, and sign-out automatically when using native components.

## Testing and Validation

### Migration Checklist

Run through this checklist after completing all migration steps:

- [ ] Ran `npx @clerk/upgrade` CLI
- [ ] Package renamed from `@clerk/clerk-expo` to `@clerk/expo`
- [ ] All import paths updated (see the import reference table in Part 1)
- [ ] `publishableKey` explicitly passed to `ClerkProvider`
- [ ] `tokenCache` from `@clerk/expo/token-cache` configured
- [ ] `app.json` plugins updated (`@clerk/expo`, `expo-secure-store`)
- [ ] `SignedIn`/`SignedOut`/`Protect` replaced with `<Show>`
- [ ] Hook API calls updated to Core 3 authentication API
- [ ] Environment variables updated to `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY`
- [ ] Redirect props renamed (`afterSignInUrl` to `signInFallbackRedirectUrl`)
- [ ] `useOAuth` replaced with `useSSO` if applicable
- [ ] `@clerk/types` imports moved to `@clerk/shared/types`

### Testing Authentication Flows

| Flow                   | What to Test                                                           |
| ---------------------- | ---------------------------------------------------------------------- |
| Email/password sign-in | `signIn.password()` completes, `signIn.finalize()` navigates correctly |
| Email/password sign-up | `signUp.password()`, email verification, `signUp.finalize()`           |
| OAuth (native)         | Google and Apple native flows on device                                |
| OAuth (browser)        | `useSSO` flows with browser redirect                                   |
| MFA                    | `needs_second_factor` status, `signIn.mfa.verifyEmailCode()`           |
| Client Trust           | `needs_client_trust` on new device with password                       |
| Sign-out               | Session cleanup, UI updates                                            |

### Testing Authorization

- Verify `<Show>` with role-based conditions shows/hides correctly
- Verify `<Show>` with permission-based conditions
- Verify fallback content renders for unauthorized users
- Test organization switching and role changes in real-time

### Testing Native Components

- AuthView renders and completes auth flow on iOS and Android
- UserButton displays avatar, opens profile modal
- `treatPendingAsSignedOut: false` is set on `useAuth()` and `<Show>`
- Session sync completes within \~3 seconds of native auth

### Testing Offline and Error Handling

- Disable network, verify `ClerkOfflineError` is caught (not null)
- Test biometric auth if using `useLocalCredentials`
- Test passkeys on physical devices (not emulators)

### Development vs. Production

| Environment       | How to Test                                                                                                                  |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Development build | `npx expo run:ios` / `npx expo run:android`                                                                                  |
| Production-like   | EAS Build                                                                                                                    |
| API keys          | Switch from `pk_test_` to `pk_live_` ([Expo deployment guide](https://clerk.com/docs/guides/development/deployment/expo.md)) |
| Native features   | Verify in production builds via EAS                                                                                          |

## Troubleshooting Common Migration Issues

### Breaking Changes Quick Reference

| Change                 | Before (Core 2)                      | After (Core 3)                            |
| ---------------------- | ------------------------------------ | ----------------------------------------- |
| Package name           | `@clerk/clerk-expo`                  | `@clerk/expo`                             |
| Control components     | `SignedIn` / `SignedOut` / `Protect` | `Show`                                    |
| Sign-in API            | `signIn.create()` + `setActive()`    | `signIn.password()` + `signIn.finalize()` |
| Sign-up API            | `signUp.create()` + `setActive()`    | `signUp.password()` + `signUp.finalize()` |
| Environment variable   | `CLERK_FRONTEND_API`                 | `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY`       |
| Token offline behavior | Returns `null`                       | Throws `ClerkOfflineError`                |
| Expo SDK minimum       | 50.0.0+                              | 53.0.0+                                   |
| Node.js minimum        | 18.17.0+                             | 20.9.0+                                   |
| OAuth hooks            | `useOAuth()`                         | `useSSO()`                                |
| Native OAuth imports   | `@clerk/clerk-expo`                  | `@clerk/expo/apple`, `@clerk/expo/google` |
| Appearance config      | `appearance.layout`                  | `appearance.options`                      |
| Redirect props         | `afterSignInUrl`                     | `signInFallbackRedirectUrl`               |
| SAML strategy          | `strategy: 'saml'`                   | `strategy: 'enterprise_sso'`              |
| Error kind             | `'ClerkApiError'`                    | `'ClerkAPIError'`                         |
| Active sessions        | `client.activeSessions`              | `client.sessions`                         |
| Clerk export           | `import { Clerk }`                   | `useClerk()` / `getClerkInstance()`       |
| setActive callback     | `beforeEmit`                         | `navigate`                                |
| Passkey sign-in        | `signIn.authenticateWithPasskey()`   | `signIn.passkey()`                        |

### Common Errors and Fixes

| Error                                  | Cause                       | Fix                                                                        |
| -------------------------------------- | --------------------------- | -------------------------------------------------------------------------- |
| `Cannot find module @clerk/clerk-expo` | Package not renamed         | `npx expo install @clerk/expo`                                             |
| `publishableKey is required`           | Not passed explicitly       | Add `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY` to `.env`, pass to `ClerkProvider` |
| Native components don't render         | Using Expo Go               | Run `npx expo run:ios` or `npx expo run:android`                           |
| Tokens lost on restart                 | `expo-secure-store` missing | `npx expo install expo-secure-store`, add `tokenCache`                     |
| OAuth fails                            | Native API not enabled      | Enable at Dashboard's Native Applications page                             |
| Passkeys fail on emulator              | Not supported               | Use a physical device                                                      |
| `ClerkOfflineError` not caught         | Using null-check pattern    | Switch to try/catch with `ClerkOfflineError.is(error)`                     |
| App crashes in production              | `publishableKey` missing    | Env vars aren't inlined in RN builds; pass explicitly                      |

## Conclusion

Upgrading to `@clerk/expo` modernizes your authentication flow and unlocks native components, passkeys, and offline resilience. Once you've completed these steps, validate every flow against the Migration Checklist and testing tables above before shipping to production.

## FAQ

### Do native components work in Expo Go?

No, native features like `AuthView`, `UserButton`, and platform-native OAuth require a development build. They cannot be run inside Expo Go.

### Why does `getToken()` throw an error when offline?

In Core 3, `getToken()` throws a `ClerkOfflineError` after retrying, making it explicitly clear that the network is unavailable, whereas Core 2 returned `null` which was ambiguous with being signed out.

## In this series

1. [Migrating from @clerk/clerk-expo to @clerk/expo — Breaking Changes, Native Components, and the Complete Upgrade Path](https://clerk.com/articles/migrating-from-clerk-clerk-expo-to-clerk-expo-breaking-changes-native-components.md)
2. **Migrating from @clerk/clerk-expo to @clerk/expo — Breaking Changes, Native Components, and the Complete Upgrade Path - Part 2** (you are here)
