# How to use SwiftUI components in a React Expo and Clerk app

Clerk's [`@clerk/expo`](https://github.com/clerk/javascript/tree/main/packages/expo) package ships a `/native` subpath that exports three React components, `AuthView`, `UserButton`, and `UserProfileView`, which render as real SwiftUI views on iOS. On the JavaScript side you write one React Native component tree; on the device, `UIHostingController` embeds the native SwiftUI view inside React Native's Fabric hierarchy. Apple Sign-In uses the OS-level `ASAuthorizationController` credential sheet. Google Sign-In on iOS uses `ASWebAuthenticationSession`, the same RFC 8252 compliant Safari sheet that Google's own iOS SDK uses internally, because iOS does not expose a system-level Google credential picker.

On Android, the same `<AuthView />` renders as Jetpack Compose, and Google Sign-In goes through the true native Credential Manager bottom sheet. One TypeScript file builds both platforms. This tutorial focuses on iOS and SwiftUI because that's where the bridging work is most visible; Android-specific setup is called out inline where it applies.

> Clerk's Expo native components are in public beta as of 2026-04-16. The API surface is stable enough to build with, but expect minor breaking changes before general availability. Pin `@clerk/expo@^3.2.0` if you want a reproducible build.

## What you'll build

By the end of this tutorial you'll have an Expo app that:

- Renders `<AuthView />` as a SwiftUI view with native Google Sign-In and native Apple Sign-In on iOS.
- Displays a `<UserButton />` in the Expo Router header that opens the native profile modal on tap.
- Uses `<UserProfileView />` for a dedicated profile screen (personal info, email, [MFA](https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication.md), [passkeys](https://clerk.com/docs/guides/configure/auth-strategies/sign-up-sign-in-options.md#passkeys), connected accounts, active sessions, sign-out).
- Gates the home stack behind `Stack.Protected` so only signed-in users can reach it.
- Runs the same code on Android as Jetpack Compose, with Google Sign-In routed through the Android Credential Manager bottom sheet.

Testing the Google Sign-In flow works on the iOS Simulator. Testing Apple Sign-In reliably requires a physical iPhone. A full end-to-end iOS build takes about 30 minutes from a clean machine.

## Why native authentication beats WebView OAuth

Native credential pickers close a specific attack surface that embedded WebView browsers cannot. Beyond the UX benefits over WebView-based [authentication](https://clerk.com/docs/guides/how-clerk-works/overview.md), that's the security reason standards bodies, Apple, and Google all moved away from WebView OAuth.

### The WebView OAuth problem

Google announced the embedded-WebView OAuth block in August 2016 ([Google Developers Blog, Aug 2016](https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html)). New OAuth clients were blocked starting 2016-10-20; existing clients started receiving the `disallowed_useragent` error for WebView traffic on 2017-04-20. The reason is simple: a WebView is part of the host app's process, so the app can inject JavaScript, read the DOM, and capture keystrokes during the OAuth flow. RFC 8252 (BCP 212) codified this in October 2017: "this best current practice requires that native apps MUST NOT use embedded user-agents to perform authorization requests" ([RFC 8252 Section 8.12](https://datatracker.ietf.org/doc/html/rfc8252#section-8.12)). The 2025 OAuth 2.0 Security Best Current Practice (RFC 9700) reaffirmed the external-agent requirement ([RFC 9700, Jan 2025](https://datatracker.ietf.org/doc/rfc9700/)).

Real-world evidence backs this up. Felix Krause demonstrated in August 2022 that Instagram and Facebook's in-app browsers inject tracking JavaScript into every third-party page, including sign-in forms ([Krause, Aug 2022](https://krausefx.com/blog/ios-privacy-instagram-and-facebook-can-track-anything-you-do-on-any-website-in-their-in-app-browser)). That's ambient surveillance; a hostile host app could just as easily capture passwords.

### The compliant-browser alternative

`ASWebAuthenticationSession` on iOS and Chrome Custom Tabs on Android are the standards-compliant alternatives. They run outside the host app's process, share cookies with Safari or Chrome (so users already signed into Google can continue with one tap), and prevent the app from observing keystrokes. This is the baseline every modern [OAuth](https://clerk.com/docs/guides/configure/auth-strategies/social-connections/overview.md) provider hits on iOS.

These sheets still show browser chrome and context-switch the user out of your app's visual flow. That's the UX trade-off: they're secure, but they're not borderless.

### True native credential pickers and where they exist

An OS-level credential sheet, no browser, no URL bar, biometric confirmation, is the best experience available today. But they only exist for specific providers on specific platforms:

- **Apple Sign-In on iOS:** `ASAuthorizationController` / `ASAuthorizationAppleIDProvider` (iOS 13+). No browser.
- **Google Sign-In on Android:** Jetpack Credential Manager (`androidx.credentials.CredentialManager`). A system bottom sheet that surfaces signed-in Google accounts and passkeys.
- **[Passkeys](https://clerk.com/docs/guides/configure/auth-strategies/sign-up-sign-in-options.md#passkeys) on iOS and Android:** `ASAuthorizationPlatformPublicKeyCredential*` APIs on iOS and Credential Manager on Android.

There is one important asymmetry: iOS has no OS-level Google credential picker. Clerk's `<AuthView />` on iOS routes Google Sign-In through `ASWebAuthenticationSession`, the same path Google's own `GIDSignIn` iOS SDK uses under the hood. The Safari sheet opens, the user taps "Continue with Google," and cookies are shared so most sign-ins are a single tap. But it is still a browser sheet, not a browserless OS picker.

On Android, the same `<AuthView />` invokes Credential Manager and users see the real native bottom sheet. The Android path is the closest modern experience to Apple's on-device credential sheet.

### App Store Guideline 4.8

Apple's App Store Review Guidelines require apps that offer any third-party [social login](https://clerk.com/docs/guides/configure/auth-strategies/social-connections/overview.md) to also offer an equivalent privacy-preserving option (Sign in with Apple qualifies) ([Apple Developer, updated 2026-02-06](https://developer.apple.com/app-store/review/guidelines/#login-services)). Enforcement began on 2020-06-30 ([Apple Developer, 2020](https://developer.apple.com/news/upcoming-requirements/?id=06302020a)). Practically, this means if you ship Google Sign-In, you also ship Apple Sign-In, and the path of least friction is the native `ASAuthorizationAppleIDButton` and credential sheet.

A related requirement, Guideline 5.1.1(v), mandates in-app account deletion since 2022-06-30 ([Apple Developer, 2022](https://developer.apple.com/support/offering-account-deletion-in-your-app/)). Clerk's `<UserProfileView />` surfaces account deletion as part of the native profile flow, so this ships for free.

### The numbers

Measurable conversion gains for native and passkey flows are well-documented:

- **Pinterest:** 126% Android sign-up increase with Google One Tap ([Google Identity, Pinterest case study](https://developers.google.com/identity/sign-in/case-studies/pinterest)).
- **FIDO Passkey Index (Oct 2025):** 93% sign-in success with passkeys vs 63% with other authentication methods (social login, MFA, OTPs) ([FIDO Alliance, Oct 2025](https://fidoalliance.org/wp-content/uploads/2025/10/FIDO-Passkey-Index-October-2025.pdf)).
- **Dashlane:** 92% conversion rate on passkey sign-in opportunities vs 54% on automatic password opportunities, a 70% conversion lift ([Google Developers Blog, Dashlane](https://developers.googleblog.com/password-manager-dashlane-sees-70-increase-in-conversion-rate-for-signing-in-with-passkeys-compared-to-passwords/)).
- **Microsoft:** \~98% success for passkeys vs \~32% for passwords; phishing attacks using adversary-in-the-middle techniques up 146% year-over-year ([Microsoft Security, Dec 2024](https://www.microsoft.com/en-us/security/blog/2024/12/12/convincing-a-billion-users-to-love-passkeys-ux-design-insights-from-microsoft-to-boost-adoption-and-security/)).

The pattern is consistent: when sign-in drops below a browser hop and uses on-device credentials, success rates climb and friction collapses.

## How Clerk's native components render SwiftUI

`@clerk/expo/native` exports three React components. Each one is backed by a real SwiftUI view on iOS, hosted inside React Native's Fabric hierarchy via `UIHostingController` ([Apple Developer](https://developer.apple.com/documentation/swiftui/uihostingcontroller)). Props flow React → C++ Shadow Node → SwiftUI view; events flow back through Fabric's `EventEmitter`. The TypeScript surface hides every bit of that plumbing.

The end-to-end bridge looks like this:

```text
React / TypeScript
   <AuthView mode="signInOrUp" />
              │
              ▼
   Fabric C++ Shadow Node  (props down, events up)
              │
   ┌──────────┴──────────┐
   ▼                     ▼
iOS                   Android
UIHostingController   ComposeView
   │                     │
   ▼                     ▼
SwiftUI view          Jetpack Compose view
(clerk-ios)           (clerk-android)
   │                     │
   ▼                     ▼
ASAuthorizationController,   Credential Manager,
ASWebAuthenticationSession   Chrome Custom Tabs
   │                     │
   └──────────┬──────────┘
              ▼
    native Clerk session
              │
              ▼
    JS Clerk instance → useAuth(), useUser(), useSession()
```

The actual UI lives in the [`clerk-ios`](https://github.com/clerk/clerk-ios) native SDK, which ships as a native dependency of `@clerk/expo`. When a user signs in via the SwiftUI `AuthView`, the native SDK creates a [session](https://clerk.com/docs/guides/sessions/session-tokens.md) and hands it back through the Fabric event emitter. Since `@clerk/expo` 3.1.6, the JavaScript `Clerk` instance syncs bidirectionally with this native session, so `useAuth()`, `useUser()`, and `useSession()` reflect the new state without a page reload ([@clerk/expo CHANGELOG](https://github.com/clerk/javascript/blob/main/packages/expo/CHANGELOG.md)).

Compare that to rolling your own SwiftUI integration. Callstack's "Exposing SwiftUI views to React Native" guide ([Callstack, 2024](https://www.callstack.com/blog/exposing-swiftui-views-to-react-native-an-integration-guide)) lays out the three-layer architecture you'd own: a SwiftUI view, an Objective-C++ wrapper inheriting from `RCTViewComponentView`, and an `ObservableObject` for prop passing. Add a Codegen spec, a `Podspec` for the native dependency, and lifecycle handling for the hosting controller. Cross-platform parity to Jetpack Compose is a second, separate effort.

### Android parity

The same React API renders as Jetpack Compose on Android via Clerk's [`clerk-android`](https://github.com/clerk/clerk-android) SDK. You write one `<AuthView />` import and get SwiftUI on iOS and Compose on Android, with per-platform Google, Apple, and passkey flows. The rest of this tutorial concentrates on iOS; Android-specific bits (Google SHA-1 fingerprint, Credential Manager behaviour) are flagged inline when they apply.

## Comparing native React Native authentication approaches

Four broad options exist when you want real native auth on React Native:

1. **Build your own bridge.** Obj-C++ wrapper around `RCTViewComponentView`, `UIHostingController` hosting a SwiftUI view, repeat for Compose on Android. High effort, high maintenance ([Callstack integration guide](https://www.callstack.com/blog/exposing-swiftui-views-to-react-native-an-integration-guide)).
2. **Redirect-based OAuth libraries.** `react-native-app-auth`, Auth0 React Native, Amplify social login, the Supabase default path, Okta. All route through `ASWebAuthenticationSession` on iOS and Chrome Custom Tabs on Android. Compliant with RFC 8252 and fine for security; still shows browser chrome.
3. **Native credential-picker wrappers.** `expo-apple-authentication`, `@react-native-google-signin/google-signin`, `invertase/react-native-apple-authentication`, and similar packages. They expose the native picker, but you still design every pixel of your sign-in, sign-up, and profile screens.
4. **Pre-built native RN UI.** Clerk's `@clerk/expo/native` is currently the only option that gives you the full set of auth screens, social + email + MFA + profile, as drop-in React components.

| Provider      | Pre-built RN UI | Native Google picker | Native Apple picker | Profile UI | Official RN SDK |
| ------------- | :-------------: | :------------------: | :-----------------: | :--------: | :-------------: |
| Clerk         |       Yes       |          Yes         |         Yes         |     Yes    |       Yes       |
| Firebase Auth |        No       |       3rd party      |      3rd party      |     No     |       Yes       |
| Auth0         |        No       |          No          |          No         |     No     |       Yes       |
| Supabase Auth |        No       |       3rd party      |      3rd party      |     No     |      JS SDK     |
| AWS Amplify   |  Authenticator  |          No          |          No         |     No     |       Yes       |
| Stytch        |     StytchUI    |          No          |          No         |     No     |       Yes       |
| Descope       |     FlowView    |      via option      |      via option     |     No     |       Yes       |
| WorkOS        |        No       |          No          |          No         |     No     |        No       |

A few notes the table flattens:

- **Firebase** does not ship pre-built React Native UI. Its documented path is to combine `@react-native-firebase/auth` with `@react-native-google-signin/google-signin` and `invertase/react-native-apple-authentication`, wiring the tokens back into Firebase ([rnfirebase.io, Social Auth](https://rnfirebase.io/auth/social-auth)).
- **Auth0** relies on Universal Login, an `ASWebAuthenticationSession` redirect. There is no native-form option ([Auth0 React Native Quickstart](https://auth0.com/docs/quickstart/native/react-native/interactive)).
- **Supabase** does not expose native React Native UI. Their Apple Sign-In guide walks you through `expo-apple-authentication`, and their Google Sign-In guide uses `@react-native-google-signin/google-signin` ([Supabase Apple](https://supabase.com/docs/guides/auth/social-login/auth-apple); [Supabase Google](https://supabase.com/docs/guides/auth/social-login/auth-google)).
- **Amplify Authenticator** exists for React Native but routes social providers through a browser-based flow ([Amplify Authenticator](https://ui.docs.amplify.aws/react-native/connected-components/authenticator); [Amplify social providers](https://docs.amplify.aws/gen1/react-native/build-a-backend/auth/add-social-provider/)).
- **Stytch** ships a React Native UI configuration that renders a form, but their native social pickers are wrapper-level rather than system-level ([Stytch React Native UI](https://stytch.com/docs/mobile-sdks/react-native-sdk/ui-configuration)).
- **Descope** offers a native-vs-browser toggle per flow; it's flexible, but there is no drop-in profile UI ([Descope native vs browser flows](https://docs.descope.com/mobile-sdk/native-vs-browser-flows)).
- **WorkOS** does not publish a React Native SDK ([WorkOS SDKs list](https://workos.com/docs/sdks)).

If you need full native UI with the least custom code, Clerk is the shortest path today. The rest of this tutorial walks through the implementation.

## Prerequisites

Before you start, make sure you have:

- **Node.js 20.9.0+** (`@clerk/expo` minimum per [its README](https://github.com/clerk/javascript/blob/main/packages/expo/README.md)). React Native 0.81 raises that floor in practice to 20.19.4+, so install the latest LTS Node if you're on an older machine.
- **Expo SDK 54+** (SDK 55 also works). This tutorial pins 54 to match Clerk's [NativeComponentQuickstart](https://github.com/clerk/clerk-expo-quickstart/tree/main/NativeComponentQuickstart) repo.
- **Xcode 16.1+** ([required by React Native 0.81 which ships with SDK 54](https://reactnative.dev/blog/2025/08/12/react-native-0.81)).
- **A Clerk account.** The free Hobby tier includes 50,000 [monthly retained users](https://clerk.com/pricing) (MRUs: users who visit your app on a day after sign-up).
- **Google Cloud Console access** for creating OAuth 2.0 client IDs. Free.
- **iOS device and Apple ID setup (read carefully):**
  - A physical iPhone is strongly recommended for the Apple Sign-In section. The iOS Simulator can complete the initial Sign in with Apple authorization flow when a real Apple ID is signed into Settings, but `getCredentialStateAsync` always throws on the Simulator, so any app logic that checks credential state on subsequent launches won't work there ([expo-apple-authentication docs](https://docs.expo.dev/versions/latest/sdk/apple-authentication/)). Google Sign-In via `<AuthView />` works reliably on the Simulator.
  - A free Apple ID builds to the Simulator. Deploying to a physical device and enabling the Sign in with Apple capability for App Store distribution both require an Apple Developer Program membership ($99/year) ([Apple Developer Program](https://developer.apple.com/programs/whats-included/)).
- **Android parity (optional):** Android Studio + Android SDK 24+ if you also want `npx expo run:android`. The Jetpack `androidx.credentials` Credential Manager library supports Android 6.0 (API 23) and higher ([Android Credential Manager](https://developer.android.com/identity/sign-in/credential-manager)).

A quick version check:

```bash
node --version     # v20.9.0 or newer
xcodebuild -version # Xcode 16.1 or newer
```

If either is too old, upgrade before proceeding.

## Why development builds, not Expo Go

Expo Go bundles a fixed set of native modules. `@clerk/expo/native` ships custom native code (the SwiftUI `ClerkAuthView`, the Jetpack Compose equivalent, and the `clerk-ios` / `clerk-android` native SDK dependencies). Expo Go can't load custom native modules, so it can't render these components ([Expo development builds introduction](https://docs.expo.dev/develop/development-builds/introduction/)).

`expo-dev-client` replaces Expo Go. It's the same Debug build of your app, with the Expo Dev Menu and fast refresh wired in ([expo-dev-client](https://docs.expo.dev/versions/latest/sdk/dev-client/)). You install the package once; each `npx expo run:ios` compiles a fresh dev build for your device or simulator.

Two paths to a dev build:

1. **Local.** `npx expo run:ios` / `npx expo run:android`. Zero cloud setup, requires Xcode or Android Studio locally.
2. **EAS.** `eas build --profile development`. Cloud builder, no local Xcode needed. Requires an Expo account and, for iOS device builds, a paid Apple Developer Program membership ([EAS development build](https://docs.expo.dev/develop/development-builds/create-a-build/)).

This tutorial uses local builds for zero-setup reproducibility. EAS is a drop-in swap if that fits your team better.

## Step 1: Create the Expo project

Scaffold a fresh project:

```bash
npx create-expo-app@latest clerk-native-app --template default
cd clerk-native-app
```

Then install Clerk and the two required Expo packages:

```bash
npx expo install @clerk/expo expo-secure-store expo-dev-client
```

Three notes:

- `@clerk/expo` is the Clerk SDK. The native components live under the `@clerk/expo/native` subpath.
- `expo-secure-store` is how the Clerk token cache persists the session JWT securely on device ([Expo SecureStore](https://docs.expo.dev/versions/latest/sdk/securestore/)).
- `expo-dev-client` turns your build into a dev client (replacing Expo Go).

Do **not** install `expo-apple-authentication` or `expo-crypto` yet. `<AuthView />` handles Apple Sign-In internally without them. You'll only need those packages if you later swap to the custom-UI path with `useSignInWithApple()`, covered in Step 6. This matches the [NativeComponentQuickstart `package.json`](https://github.com/clerk/clerk-expo-quickstart/blob/main/NativeComponentQuickstart/package.json).

## Step 2: Configure Clerk

There are four pieces of configuration: the Dashboard application, the Native API, environment variables, and the Clerk plugin in `app.json`.

### Create a Clerk application

[Sign up for a Clerk account](https://dashboard.clerk.com/sign-up) and create a new application. Pick a name, leave the defaults, and copy the [Publishable Key](https://clerk.com/docs/guides/development/clerk-environment-variables.md) from the API keys page.

### Enable the Native API

In the Dashboard, navigate to **Native applications** and click **Enable**. Register your iOS and Android apps ([Clerk iOS production docs](https://clerk.com/docs/ios/reference/native-mobile/production.md); [Clerk Android production docs](https://clerk.com/docs/android/reference/native-mobile/production.md)):

- **iOS:** Apple Team ID + Bundle ID. Team ID is on the [Apple Developer account membership page](https://developer.apple.com/account/#/membership/). Bundle ID matches the `ios.bundleIdentifier` value in your `app.json`, for example `com.yourname.clerknativeapp`.
- **Android:** Package name + SHA-256 debug fingerprint. Get the fingerprint with:

```bash
cd android && ./gradlew signingReport
```

Use the `SHA-256` line under `Variant: debug`. Production apps need the release-signing SHA-256 registered separately.

### Configure environment variables

Create a `.env.local` file at the project root:

```bash
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
```

The `EXPO_PUBLIC_` prefix is required for Expo to inline the value into the client bundle.

### Configure the Clerk plugin in `app.json`

Add the `@clerk/expo` plugin alongside `expo-router` and `expo-secure-store`:

```json
{
  "expo": {
    "name": "clerk-native-app",
    "slug": "clerk-native-app",
    "ios": { "bundleIdentifier": "com.yourname.clerknativeapp" },
    "android": { "package": "com.yourname.clerknativeapp" },
    "plugins": ["expo-router", "expo-secure-store", ["@clerk/expo", {}]]
  }
}
```

The plugin accepts two options, verified against `withClerkExpo.ts` in `@clerk/expo` 3.2.0 ([plugin source](https://github.com/clerk/javascript/blob/main/packages/expo/src/plugin/withClerkExpo.ts)):

- `appleSignIn: true` (default). The plugin writes the `com.apple.developer.applesignin` entitlement at prebuild time so Sign in with Apple works out of the box. Set it to `false` only if you're certain you don't need Apple Sign-In.
- `theme: './clerk-theme.json'` (see the Theming section later).

The plugin also reads `EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME` from the environment if you opt into the native `GIDSignIn` path via `useSignInWithGoogle()`. That's a more advanced use case; `<AuthView />` doesn't need it.

> The published plugin source does not expose a `keychainService` / App Group option. Keychain sharing between app and extensions lives on the clerk-ios Clerk.Options API and requires a custom config plugin from Expo. See the FAQ for details.

### Wrap the app with `<ClerkProvider>`

Open `app/_layout.tsx` and wrap the root `<Stack>` with [`<ClerkProvider>`](https://clerk.com/docs/expo/reference/components/clerk-provider.md). Use the `tokenCache` export from `@clerk/expo/token-cache` so sessions persist across restarts:

```tsx
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { Stack } from 'expo-router'

const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!

export default function RootLayout() {
  return (
    <ClerkProvider publishableKey={publishableKey} tokenCache={tokenCache}>
      <Stack />
    </ClerkProvider>
  )
}
```

The token cache stores the Clerk session [JWT](https://clerk.com/docs/guides/sessions/jwt-templates.md) in `expo-secure-store`, which maps to iOS Keychain and Android EncryptedSharedPreferences ([`tokenCache` source](https://github.com/clerk/javascript/blob/main/packages/expo/src/token-cache/index.ts)). Restart the app and the previous session rehydrates.

## Step 3: Create the development build

From the project root:

```bash
npx expo run:ios
```

The first build compiles React Native and takes several minutes. Subsequent builds cache and finish in tens of seconds. Expo launches the iOS Simulator and runs your app once the build completes. Add `--device` to deploy to a connected iPhone:

```bash
npx expo run:ios --device
```

Success criteria: the app launches with the default Expo welcome screen and no red or yellow boxes. If you see "Native module cannot be null" or similar, you're running in Expo Go or against a stale dev build. Re-run `npx expo run:ios`.

For Android parity, start an emulator and run:

```bash
npx expo run:android
```

See the [Expo iOS development build tutorial](https://docs.expo.dev/tutorial/eas/ios-development-build-for-devices/) if you run into device provisioning issues.

## Step 4: Add Google Sign-In with `<AuthView />`

Google Sign-In needs two places to know about each other: Google Cloud Console (which issues the client IDs) and the Clerk Dashboard (which uses them).

### Google Cloud Console setup (iOS primary)

In the [Google Cloud Console](https://console.cloud.google.com/apis/credentials):

1. Create an OAuth consent screen (External, fill the minimum fields). This is mandatory before you can create OAuth client IDs.
2. Create the OAuth 2.0 client IDs you need:
   - **iOS client ID** (required): use the Bundle ID from your `app.json`.
   - **Web client ID** (required): Clerk uses this for server-side token verification.
   - **Android client ID** (only if building Android): package name plus the SHA-1 debug fingerprint from `./gradlew signingReport`. Production builds need a separate client ID registered with the release SHA-1.

Google's native-apps guide has the canonical walkthrough ([Google Identity, OAuth for Native Apps](https://developers.google.com/identity/protocols/oauth2/native-app)).

### Paste credentials into Clerk Dashboard

In the Dashboard, go to **Social Connections → Google** and paste:

- iOS Client ID.
- Web Client ID.
- Web Client Secret.
- Android Client ID (if applicable).

That's it for configuration. Clerk's config plugin handles the iOS URL scheme; you do not edit `Info.plist` by hand.

### Render `<AuthView />`

Create `app/sign-in.tsx`:

```tsx
import { AuthView } from '@clerk/expo/native'

export default function SignIn() {
  return <AuthView mode="signInOrUp" />
}
```

The `mode` prop accepts three values, verified in [AuthView.types.ts](https://github.com/clerk/javascript/blob/main/packages/expo/src/native/AuthView.types.ts):

- `'signIn'`: sign-in only.
- `'signUp'`: sign-up only.
- `'signInOrUp'` (default): combined flow. The native UI decides based on whether the identifier exists.

The only other prop is `isDismissible: boolean` (default `false`) for presentations where you want the user to be able to close the sheet. There is no `style` or `appearance` prop; theming lives in `clerk-theme.json` (see the Theming section).

### React to auth state changes

Pair `<AuthView />` with `useAuth()` to navigate after sign-in completes. On a fresh native sign-in, there's a short window while the native session syncs into the JavaScript `Clerk` instance; during that window the JS layer sees a `pending` state. Tell `useAuth` to treat `pending` as signed-in so your `useEffect` doesn't misfire:

```tsx
import { AuthView } from '@clerk/expo/native'
import { useAuth } from '@clerk/expo'
import { useRouter } from 'expo-router'
import { useEffect } from 'react'

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

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

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

The `treatPendingAsSignedOut: false` flag deserves a proper explanation. Since [`@clerk/clerk-react` 5.26.0 (April 2025, PR #5507)](https://github.com/clerk/javascript/pull/5507), which `@clerk/expo` depends on, `useAuth()` defaults `treatPendingAsSignedOut` to `true`. "Pending" is Clerk's generic state for "a session exists but requires an additional step" (for example, [MFA](https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication.md) not yet completed). In the native-components case, pending shows up during the native-session → JS-Clerk sync window: `clerk-ios` has already created a session, and `@clerk/expo` is still syncing it into the React tree. A `useEffect` watching `isSignedIn` with the default `true` would briefly see signed-out and fire, causing a redirect flicker. Setting `treatPendingAsSignedOut: false` bridges the gap for screens that react to native authentication.

### Test the flow

Run the dev build and tap the Google button inside `<AuthView />`:

```bash
npx expo run:ios
```

iOS has no OS-level Google credential picker. `<AuthView />` opens an `ASWebAuthenticationSession` Safari sheet, the same sheet Google's own `GIDSignIn` iOS SDK uses. The user sees a system-managed Safari interface asking permission to sign in; cookies shared with Safari mean most users continue with one tap. This is the RFC 8252 compliant flow, with browser chrome visible rather than a browserless OS picker.

On Android, the same `<AuthView />` opens the Credential Manager bottom sheet ([Android Credential Manager for Sign in with Google](https://developer.android.com/identity/sign-in/credential-manager-siwg)), the fully native experience. The contrast is deliberate: iOS doesn't expose an equivalent OS API.

You can cross-reference the full sample code in Clerk's [NativeComponentQuickstart `app/index.tsx`](https://github.com/clerk/clerk-expo-quickstart/blob/main/NativeComponentQuickstart/app/index.tsx).

## Step 5: User management with `<UserButton />` and `useUserProfileModal()`

Post-auth UX usually needs two things: a visible affordance for the user to manage their account, and a place to do it. `<UserButton />` is the affordance; `<UserProfileView />` is the place.

### Place `<UserButton />` in the header

`UserButton` has no props. Size is controlled by the parent `<View>` using `width`, `height`, `borderRadius`, and `overflow: 'hidden'` ([UserButton.tsx source](https://github.com/clerk/javascript/blob/main/packages/expo/src/native/UserButton.tsx)). Put it in the Expo Router header's `headerRight`:

```tsx
import { UserButton } from '@clerk/expo/native'
import { Stack } from 'expo-router'
import { View } from 'react-native'

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

Tapping the button opens the native profile modal, a real SwiftUI sheet on iOS, not a React Native `<Modal />`.

### Use `useUserProfileModal()` to open the profile imperatively

If you want a button in another screen (say, a settings row) to open the profile, use the `useUserProfileModal()` hook ([`useUserProfileModal.ts`](https://github.com/clerk/javascript/blob/main/packages/expo/src/hooks/useUserProfileModal.ts)):

```tsx
import { useUserProfileModal } from '@clerk/expo'
import { Button } from 'react-native'

export function AccountRow() {
  const { presentUserProfile, isAvailable } = useUserProfileModal()
  if (!isAvailable) return null
  return <Button title="Account" onPress={presentUserProfile} />
}
```

`isAvailable` is `false` on platforms where the native modal isn't supported (for example, web), so gate on it.

### Inline `<UserProfileView />` for dedicated profile screens

If you prefer a full-screen profile route, render `<UserProfileView />` inline:

```tsx
import { UserProfileView } from '@clerk/expo/native'

export default function ProfileScreen() {
  return <UserProfileView style={{ flex: 1 }} />
}
```

Of the three native components, `UserProfileView` is the only one that accepts a `style` prop ([UserProfileView.tsx source](https://github.com/clerk/javascript/blob/main/packages/expo/src/native/UserProfileView.tsx)). Use it to size and position the inline view; visual theming is still controlled by `clerk-theme.json`.

### Sign-out flow

`<UserProfileView />`'s built-in Sign Out button emits a signed-out event. Thanks to two-way session sync (introduced in `@clerk/expo` 3.1.6), the JavaScript `Clerk` instance updates automatically, so `useAuth().isSignedIn` flips to `false` and any `Stack.Protected` gate you've set up re-renders. If you want to react explicitly (log analytics, clear app state), use `useNativeAuthEvents()`:

```tsx
import { useNativeAuthEvents } from '@clerk/expo'
import { useRouter } from 'expo-router'

export function useSignOutRedirect() {
  const router = useRouter()
  useNativeAuthEvents({
    onSignedOut: () => router.replace('/sign-in'),
  })
}
```

The hook emits `signedIn` and `signedOut` events from the native SDKs via `NativeEventEmitter` ([`useNativeAuthEvents.ts`](https://github.com/clerk/javascript/blob/main/packages/expo/src/hooks/useNativeAuthEvents.ts)).

## Step 6: Add Apple Sign-In

Apple Sign-In is the only provider on iOS that renders a true native credential sheet via `ASAuthorizationController`. On Android, Apple falls back to an OAuth browser flow that `<AuthView />` handles transparently. The `@clerk/expo` config plugin writes the necessary iOS entitlement at prebuild time, so the wiring is minimal.

### Enable Apple in the Dashboard

In the Clerk Dashboard, go to **Social Connections → Apple** and enable it. For development, that's enough; you can test on a physical iPhone signed into a real Apple ID.

For production you'll also supply the Apple-side credentials ([Clerk Apple social connection setup](https://clerk.com/docs/guides/configure/auth-strategies/social-connections/apple.md)):

- Apple Team ID.
- Services ID (created in the [Apple Developer portal](https://developer.apple.com/account/resources/identifiers/list/serviceId)).
- Key ID and `.p8` private key file (one-time download; back it up immediately per [Apple's private-key guide](https://developer.apple.com/help/account/capabilities/create-a-sign-in-with-apple-private-key)).

### Rebuild the dev build

The `@clerk/expo` plugin adds the `com.apple.developer.applesignin` entitlement at prebuild time, which is a native change. Rebuild:

```bash
npx expo run:ios --device
```

A simulator rebuild works for most of the flow, but physical-device testing is required for reliable end-to-end Apple Sign-In (see prerequisites).

### Testing without a physical iPhone

If you don't have a device on hand, you can still make progress. The Sign in with Apple sheet will render on the Simulator when an Apple ID is signed into **Settings → [your name] → Media & Purchases**, and the initial authorization sometimes completes if that Apple ID has 2FA enabled ([Apple Developer Forums discussion](https://developer.apple.com/forums/thread/121940)). `getCredentialStateAsync` always throws on the Simulator, though, so any logic that checks credential state on subsequent launches will fail there.

Two pragmatic fallbacks:

- **Mock the native modules for unit tests and CI.** Expo's [official mocking guide](https://docs.expo.dev/modules/mocking/) covers the `jest-expo` preset and module mocks so your Apple Sign-In tests run green on a laptop with no device attached.
- **Borrow, ad-hoc, or rent a real device before release.** A personal device, an internal-distribution EAS build on a teammate's phone, or a cloud device farm (AWS Device Farm, BrowserStack, Firebase Test Lab) all work for pre-release verification. Apple's App Review requires a working Sign in with Apple implementation ([App Store Review Guideline 4.8](https://developer.apple.com/app-store/review/guidelines/#login-services)), so at least one physical-device pass is mandatory before shipping to the App Store.

### Test Apple Sign-In with `<AuthView />`

`<AuthView />` detects that Apple is enabled for your Clerk instance and renders the `ASAuthorizationAppleIDButton` automatically ([Apple Developer](https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidbutton)). Tap it and the OS presents the credential sheet, biometric prompt, email relay option, all rendered by `ASAuthorizationController`. Clerk handles the sign-in → sign-up transfer flow internally, so a returning user with a Clerk account is signed in; a new user triggers a sign-up with the name and email that Apple provides on first authorization.

One OS constraint to plan for: Apple only returns the user's `fullName` and `email` on the first authorization. Subsequent sign-ins omit them. Clerk stores them automatically on first auth, so you don't need to handle this in your app, but it's worth knowing if you ever see a user with no name on re-sign-in during testing.

### Optional: custom Apple Sign-In UI with `useSignInWithApple()`

If you need a branded Apple button that lives outside `<AuthView />` (custom placement, haptics, extra `unsafeMetadata`, or interleaving with non-Clerk screens), use the `useSignInWithApple()` hook from the `/apple` subpath. [`@clerk/expo` 3.0.0](https://github.com/clerk/javascript/blob/main/packages/expo/CHANGELOG.md) moved the hook to a dedicated `/apple` entry point, so projects that don't need Apple Sign-In don't bundle `expo-apple-authentication` and `expo-crypto`.

Install the two additional packages and register `expo-apple-authentication` as an Expo config plugin so its entitlements and native dependencies are applied at prebuild ([Clerk Sign in with Apple (Expo)](https://clerk.com/docs/expo/guides/configure/auth-strategies/sign-in-with-apple.md); [expo-apple-authentication config plugin](https://docs.expo.dev/versions/latest/sdk/apple-authentication/)):

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

Register the plugin in `app.json`:

```json
{
  "expo": {
    "plugins": [
      "expo-router",
      "expo-secure-store",
      "expo-apple-authentication",
      ["@clerk/expo", {}]
    ]
  }
}
```

Then build the custom button. Import the hook from `@clerk/expo/apple` and gate rendering on `Platform.OS === 'ios'` because the hook is iOS-only ([`useSignInWithApple.ios.ts`](https://github.com/clerk/javascript/blob/main/packages/expo/src/hooks/useSignInWithApple.ios.ts)):

```tsx
import { useSignInWithApple } from '@clerk/expo/apple'
import { Platform, Pressable, Text } from 'react-native'
import { useRouter } from 'expo-router'

export function AppleButton() {
  const { startAppleAuthenticationFlow } = useSignInWithApple()
  const router = useRouter()
  if (Platform.OS !== 'ios') return null

  async function onPress() {
    try {
      const { createdSessionId, setActive } = await startAppleAuthenticationFlow()
      if (createdSessionId && setActive) {
        await setActive({ session: createdSessionId })
        router.replace('/')
      }
    } catch (err: any) {
      if (err.code !== 'ERR_REQUEST_CANCELED') console.error(err)
    }
  }

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

The hook auto-generates a cryptographic nonce via `expo-crypto`, requests `FULL_NAME` + `EMAIL` scopes, and transparently transfers a sign-in into a sign-up when the Apple ID is new to Clerk. User cancellation is usually swallowed inside the hook and resolves with `createdSessionId: null` (no throw), but the `ERR_REQUEST_CANCELED` guard handles edge paths where it surfaces ([Clerk `useSignInWithApple` reference](https://clerk.com/docs/reference/expo/native-hooks/use-sign-in-with-apple.md)).

For Android, Apple authentication goes through `useSSO({ strategy: 'oauth_apple' })` instead, which uses a browser flow under the hood ([`useSSO()` reference](https://clerk.com/docs/reference/expo/native-hooks/use-sso.md)).

### When to pick `useSignInWithApple` vs `<AuthView />`

`<AuthView />` is the default path: drop-in native UI for Apple + Google + email/password + MFA + recovery, no manual dependency wiring, automatic entitlement management. Pick `useSignInWithApple()` only when you need a fully custom button UI that composes into an otherwise non-Clerk screen. Mixing the two inside a single sign-in flow is possible but usually unnecessary.

Apple first shipped the `useSignInWithApple()` hook for Expo on 2025-11-13 ([Clerk changelog, 2025-11-13](https://clerk.com/changelog/2025-11-13-native-sign-in-with-apple-expo.md)).

## Step 7: Protect routes with Expo Router

Only signed-in users should reach the home stack. The cleanest pattern on Expo Router v5+ is `Stack.Protected` ([Expo Router authentication guide](https://docs.expo.dev/router/advanced/authentication/); [Stack.Protected reference](https://docs.expo.dev/router/advanced/protected/)):

```tsx
import { Stack } from 'expo-router'
import { useAuth } from '@clerk/expo'

export default function RootLayout() {
  const { isSignedIn, isLoaded } = useAuth()
  if (!isLoaded) return null

  return (
    <Stack>
      <Stack.Protected guard={isSignedIn}>
        <Stack.Screen name="(home)" />
      </Stack.Protected>
      <Stack.Protected guard={!isSignedIn}>
        <Stack.Screen name="sign-in" />
      </Stack.Protected>
    </Stack>
  )
}
```

The `isLoaded` check prevents a flash of the wrong route while Clerk's hydration completes. On Expo SDK 52 and earlier, use the legacy pattern with `useEffect` and `router.replace()`. `Stack.Protected` shipped with [Expo Router v5](https://expo.dev/blog/expo-router-v5).

If you prefer render-level gating instead of navigation-level gating, Clerk ships a `<Show>` component that replaces the legacy `<SignedIn>` / `<SignedOut>` / `<Protect>` triplet ([Clerk `<Show>` reference](https://clerk.com/docs/expo/reference/components/control/show.md)). Both approaches work; `Stack.Protected` is usually cleaner for mobile flows.

## Under the hood: the Core 3 Signal API

> **Skip this section if you're only using `<AuthView />`, `<UserButton />`, and `<UserProfileView />`.** The native components render directly through `clerk-ios` (SwiftUI) and `clerk-android` (Jetpack Compose) and do not call the JavaScript Signal API. `@clerk/expo` syncs the resulting session into `useAuth()`, `useUser()`, and `useSession()` automatically, so you get reactive React state without writing any sign-in orchestration code.
>
> Come back here the first time you need to render a custom sign-in, sign-up, or MFA screen yourself. The Signal API is the supported JavaScript path for that; the native components are the default path when the pre-built UI works.

Clerk Core 3 shipped on 2026-03-03 ([Clerk changelog, 2026-03-03](https://clerk.com/changelog/2026-03-03-core-3.md)), and `@clerk/expo` 3.1 brought it to Expo alongside the native components on 2026-03-09 ([Clerk changelog, 2026-03-09](https://clerk.com/changelog/2026-03-09-expo-native-components.md)). The Signal API is a redesign of `useSignIn`, `useSignUp`, `useCheckout`, and `useWaitlist`. Each hook returns a reactive `*Future` object (`SignInFuture`, `SignUpFuture`) that triggers re-renders automatically and exposes three structured fields:

- `fetchStatus` (`'idle' | 'fetching'`) replaces manual `setIsLoading(true)` booleans.
- `status` (`'needs_first_factor' | 'needs_second_factor' | 'complete'` and so on) tells you where you are in the flow.
- `errors.fields` gives you parsed, field-scoped errors, no try/catch parsing of `ClerkAPIError[]`.

The canonical sign-in shape is:

```ts
signIn.create({ identifier }) // initialize with email, phone, or username
signIn.password({ password }) // authenticate with password
signIn.finalize({ navigate }) // completes the flow (replaces setActive())
```

Other first-factor methods follow the same pattern: `signIn.emailCode.sendCode()` / `.verifyCode({ code })`, `signIn.passkey()`, `signIn.mfa.verifyTOTP({ code })`, and so on ([Clerk MFA custom flow guide](https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication.md)). The `navigate` callback passed to `finalize()` receives `{ session, decorateUrl }` so you can branch on `session.currentTask` (forced MFA setup, org selection, and similar "requires additional step" branches).

A minimal custom email/password screen:

```tsx
import { useSignIn } from '@clerk/expo'
import { useRouter } from 'expo-router'
import { useState } from 'react'
import { Text, TextInput, Button, View } from 'react-native'

export default function CustomSignIn() {
  const { signIn, errors, fetchStatus } = useSignIn()
  const router = useRouter()
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  async function onSubmit() {
    await signIn.create({ identifier: email })
    await signIn.password({ password })
    if (signIn.status === 'complete') {
      await signIn.finalize({ navigate: () => router.replace('/') })
    }
  }

  return (
    <View>
      <TextInput value={email} onChangeText={setEmail} autoCapitalize="none" />
      {errors.fields.identifier && <Text>{errors.fields.identifier.message}</Text>}
      <TextInput value={password} onChangeText={setPassword} secureTextEntry />
      <Button title={fetchStatus === 'fetching' ? 'Signing in...' : 'Sign in'} onPress={onSubmit} />
    </View>
  )
}
```

Legacy Core 2 required wrapping each call in `try { ... } catch (err) { /* parse ClerkAPIError[] */ }`. Core 3 exposes parsed errors on the hook itself (`errors.fields.identifier?.message`, `errors.fields.password?.message`, plus `errors.global` and raw `errors.raw`), so the component code stays linear.

`<AuthView />` does not call the JavaScript Signal API. It renders through the native Clerk SDKs (`clerk-ios` SwiftUI, `clerk-android` Compose), which implement the equivalent flow natively. When the native SDK completes authentication, `@clerk/expo` syncs the session into the JS `Clerk` instance, so `useAuth()`, `useUser()`, and `useSession()` all reflect the new state. The Signal API hooks are the custom-UI escape hatch when you need to render the sign-in screen yourself.

One stability caveat worth flagging: the `SignInFuture` instance does not have a stable identity across flow steps. If you reference `signIn` inside `useEffect`, `useCallback`, or `useMemo`, include it in the dependency array explicitly rather than relying on React identity stability ([`SignInFuture` reference](https://clerk.com/docs/reference/objects/sign-in-future.md)).

## Theming the native components

`@clerk/expo` 3.2.0 (shipped 2026-04-16) added a `theme` plugin option that points at a JSON file. Tokens are embedded at prebuild time, so the customization lives at the native layer rather than JS runtime.

Create `clerk-theme.json` at the project root:

```json
{
  "colors": {
    "primary": "#6C47FF",
    "background": "#FFFFFF",
    "foreground": "#0A0A0A"
  },
  "darkColors": {
    "background": "#121212",
    "foreground": "#F5F5F5"
  },
  "design": {
    "borderRadius": 12
  }
}
```

Reference it in the plugin config:

```json
["@clerk/expo", { "theme": "./clerk-theme.json" }]
```

Fifteen color tokens are available in the light `colors` block: `primary`, `background`, `input`, `danger`, `success`, `warning`, `foreground`, `mutedForeground`, `primaryForeground`, `inputForeground`, `neutral`, `border`, `ring`, `muted`, and `shadow`. The `darkColors` block accepts the same tokens (override any you want), plus `design.borderRadius` and an iOS-only `design.fontFamily`. Rebuild after changing the file because values are baked in at prebuild time ([NativeComponentQuickstart `clerk-theme.json`](https://github.com/clerk/clerk-expo-quickstart/blob/main/NativeComponentQuickstart/clerk-theme.json)).

## Offline session rehydration

Mobile apps live with flaky networks. `@clerk/expo` ships an experimental resource cache that persists environment, client state, and the last session JWT to `expo-secure-store`, so the app boots into an authenticated state on cold start without a network roundtrip. Opt in by passing `resourceCache` as the `__experimental_resourceCache` prop on `<ClerkProvider>` ([Clerk offline support](https://clerk.com/docs/guides/development/offline-support.md)):

```tsx
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { resourceCache } from '@clerk/expo/resource-cache'
import { Stack } from 'expo-router'

const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!

export default function RootLayout() {
  return (
    <ClerkProvider
      publishableKey={publishableKey}
      tokenCache={tokenCache}
      __experimental_resourceCache={resourceCache}
    >
      <Stack />
    </ClerkProvider>
  )
}
```

Three things are cached: the Clerk environment (enabled auth strategies, organization settings, feature flags), client state (active sessions, the user object), and the session JWT. Behind the scenes, `resourceCache` chunks values across `expo-secure-store` slots with `keychainAccessible: AFTER_FIRST_UNLOCK`, so the cache is readable only after the user has unlocked the device at least once per boot ([`resource-cache.ts` source](https://github.com/clerk/javascript/blob/main/packages/expo/src/resource-cache/resource-cache.ts)).

What it does and does not do:

- **Rehydration, yes.** On a cold start with no network, `useAuth()` resolves from cache and `getToken()` returns the last cached JWT. Your gated routes render immediately.
- **Offline sign-in, no.** `signIn.create()`, `signIn.password()`, `<AuthView />`, and the native social flows all still require network. The resource cache accelerates the "already signed in" path, not the first authentication.
- **Staleness applies.** Cached tokens can be stale until the next refresh, so always treat server-side verification as the source of truth when gating sensitive data.

The `__experimental_` prefix reflects that the shape may change. Clerk's docs warn: "It is subject to change in future updates, so use it cautiously in production environments" ([Clerk offline support](https://clerk.com/docs/guides/development/offline-support.md)). The older `@clerk/expo/secure-store` export covered similar ground and is now deprecated in favor of `resourceCache`.

## Troubleshooting

Most native-component issues fall into a handful of buckets. Here's what to check first.

**"Native module not found" or `TurboModuleRegistry` errors.** You're running in Expo Go or an old dev build. Rebuild with `npx expo run:ios` or `npx expo run:android`.

**Google Sign-In `error code: 10` on Android.** The SHA-1 fingerprint you registered in the Google Cloud Console doesn't match the signing key of the build currently on the device. Re-run `./gradlew signingReport`, copy the debug SHA-1, and update the Android OAuth client ID. Release builds use a different fingerprint ([React Native Google Sign-In, Setting Up](https://react-native-google-signin.github.io/docs/setting-up/get-config-file)).

**Apple Sign-In "hangs" on the simulator.** You're hitting the Simulator's `getCredentialStateAsync` gap. Move to a physical iPhone signed into a real Apple ID ([expo-apple-authentication docs](https://docs.expo.dev/versions/latest/sdk/apple-authentication/)).

**Kotlin metadata version mismatch on Android.** Fixed in `@clerk/expo` 3.1.5. The config plugin now adds the `-Xskip-metadata-version-check` Kotlin compiler flag at prebuild time, so builds against Expo SDK 54/55 stop failing with mismatched metadata errors. Upgrade to 3.1.5 or newer ([@clerk/expo CHANGELOG](https://github.com/clerk/javascript/blob/main/packages/expo/CHANGELOG.md)).

**OAuth redirect URI mismatch.** Clerk's default mobile SSO redirect is `{bundleIdentifier}://callback`. Verify the Dashboard's "Allowlist for mobile SSO redirect" matches your app's Bundle ID ([Clerk iOS production docs](https://clerk.com/docs/ios/reference/native-mobile/production.md)).

**White flash on AuthView mount (iOS).** Fixed in `@clerk/expo` 3.1.10.

**`useAuth()` reports signed-out briefly after a successful native sign-in.** This is the native-session → JS-Clerk sync window. `clerk-ios` has already created the session (as `pending`); `@clerk/expo` is still syncing it into the React tree. Since `useAuth()` defaults `treatPendingAsSignedOut: true`, the hook reads pending as signed-out. Set `useAuth({ treatPendingAsSignedOut: false })` anywhere you watch `isSignedIn` after a native flow to bridge the gap.

## Frequently asked questions

## FAQ

### Can I use Clerk native components in Expo Go?

No. The native components render real SwiftUI views on iOS and Jetpack Compose views on Android, which means they ship custom native code that Expo Go cannot load. You need a development build created with `npx expo run:ios`, `npx expo run:android`, or `eas build --profile development`. See [Expo development builds](https://docs.expo.dev/develop/development-builds/introduction/) for the full setup.

### Do I write separate TypeScript files for iOS SwiftUI and Android Compose?

No. One TypeScript file handles both. You import `AuthView`, `UserButton`, and `UserProfileView` from `@clerk/expo/native` and the SDK renders SwiftUI on iOS and Jetpack Compose on Android automatically. The same applies to the hooks; only platform-specific paths like `useSignInWithApple()` are iOS-only, and you gate those with `Platform.OS === "ios"`.

### How do native components handle multi-factor authentication?

`<AuthView />` handles [MFA](https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication.md) automatically. When you enable MFA in the Clerk Dashboard, the native UI prompts for the second factor (TOTP, SMS, backup codes) as part of the sign-in flow without any additional code. The same is true for the `<UserProfileView />` MFA management screen.

### Can I customize the appearance of <AuthView />?

You customize tokens (colors, border radius, font family) via `clerk-theme.json` in the `@clerk/expo` plugin config, new in `@clerk/expo` 3.2.0. The component itself does not accept `style` or `appearance` props; only `<UserProfileView />` takes a `style` prop for outer sizing. For fully custom UI, use the Core 3 Signal API hooks (`useSignIn`, `useSignUp`, `useSignInWithApple`) to build your own screens.

### How do I add email and password sign-in alongside Google and Apple?

Enable email/password in the Clerk Dashboard under **User & Authentication → Email, Phone, Username**. `<AuthView />` detects the enabled strategies and renders the appropriate inputs alongside the social buttons. No additional code is needed.

### Does Apple Sign-In work on the iOS Simulator?

The initial authorization flow can complete on the Simulator when a real Apple ID is signed into Settings, but `getCredentialStateAsync` always throws on the Simulator, so any logic that checks credential state on subsequent launches will fail ([expo-apple-authentication docs](https://docs.expo.dev/versions/latest/sdk/apple-authentication/)). Use a physical iPhone for reliable end-to-end Apple Sign-In testing.

### What happens if the user has no internet connection?

Native components require network connectivity. Without it, the authentication request fails with a network error. The recommended pattern is to detect connectivity with [`@react-native-community/netinfo`](https://github.com/react-native-netinfo/react-native-netinfo) and render a fallback UI rather than mounting `<AuthView />` over a dead network. Clerk also ships an experimental offline resource cache (`@clerk/expo/resource-cache`) for session rehydration without network ([Clerk offline support](https://clerk.com/docs/guides/development/offline-support.md)).

### Can I use native components with Expo Router?

Yes. This tutorial uses Expo Router throughout. Render `<AuthView />` inside a sign-in screen, `<UserButton />` in a `headerRight`, and gate routes with `Stack.Protected` or the legacy `useAuth + <Redirect>` pattern. Both approaches work well.

### How do I share authentication sessions between my app and app extensions?

The `@clerk/expo` config plugin does not expose a keychain-sharing option as of version 3.2.0. Keychain configuration lives on the native iOS SDK (`clerk-ios`) via `Clerk.Options(keychainConfig: .init(service:, accessGroup:))`, with a matching `keychain-access-groups` entitlement on every target. From an Expo project, that currently requires a custom config plugin to write the entitlement and init-time access to the underlying `Clerk` instance, which pushes you into bare-workflow territory. See [`Clerk.Options` documentation](https://clerk.com/docs/reference/native-mobile/configuration.md) and [Apple keychain access groups](https://developer.apple.com/documentation/security/sharing-access-to-keychain-items-among-a-collection-of-apps).

### What is the Core 3 Signal API and do I need to use it?

Core 3 Signal API is the reactive, hook-based pattern in `@clerk/expo` 3.x that replaces legacy `setActive()` with a Future object exposing `status`, `fetchStatus`, and `errors.fields`. `<AuthView />` uses the native equivalent under the hood, so you do not need to use Signal API directly with native components. You would reach for it when building a fully custom email/password or MFA screen. See the "Under the hood" section above.

### Does this work with passkeys?

Yes. Install [`@clerk/expo-passkeys`](https://clerk.com/docs/reference/expo/passkeys.md) and pass `__experimental_passkeys` to `<ClerkProvider>`. The native components surface passkey enrollment and sign-in once configured. iOS 16+ and Android 9+ are required, and you need an Apple App Site Association file or Android asset link for domain binding.

### What is the minimum @clerk/expo version I need?

`@clerk/expo` 3.0.0 is required for the `/apple` subpath import. 3.1.0 adds the Core 3 Signal API and the native components beta. 3.1.6 adds two-way native-to-JS session sync. 3.2.0 adds the `clerk-theme.json` option and is the recommended minimum for new projects as of 2026-04-16.

## Next steps

- **Add [passkeys](https://clerk.com/docs/guides/configure/auth-strategies/sign-up-sign-in-options.md#passkeys).** Install `@clerk/expo-passkeys` and pass `__experimental_passkeys` to `<ClerkProvider>` to enable passkey sign-in and enrollment inside `<AuthView />` and `<UserProfileView />`. Works on iOS 16+ and Android 9+ ([Clerk Expo passkeys](https://clerk.com/docs/reference/expo/passkeys.md)).
- **Biometric sign-in.** Use `useLocalCredentials()` to authenticate with a stored password via Face ID or Touch ID on return visits. It's a password-strategy-only hook, so it complements `<AuthView />` rather than replacing it ([Clerk `useLocalCredentials`](https://clerk.com/docs/reference/expo/native-hooks/use-local-credentials.md)).
- **Authenticated backend calls.** Use `getToken()` from `useAuth()` to retrieve a short-lived session JWT and send it as an `Authorization: Bearer` header to your own backend ([Clerk session tokens](https://clerk.com/docs/guides/sessions/session-tokens.md)).
