---
title: Expo and React Native
description: "Native sign-in for iOS and Android with Expo: the provider, hooks, screens, deep links, Sign in with ZevID, theming and wording."
---

```bash
npx expo install @zevauth/expo expo-secure-store expo-web-browser expo-crypto
```

Works with Expo SDK 56, 57 and 58. We support the current SDK and the two
before it, and add each new SDK as Expo releases it. `npx expo install`
picks the module versions that match your SDK.

`@zevauth/expo` gives a native app what `@zevauth/react` gives a website: the
same hooks, ready-made screens, and a session that survives restarts. It's built
on the web package's DOM-free half, so `useUser()` behaves identically in your
web app and your mobile app.

## The provider

Wrap your app once, usually in the root layout:

```tsx
// app/_layout.tsx (Expo Router)
import { Slot } from 'expo-router';
import { ZevAuthProvider } from '@zevauth/expo';

export default function RootLayout() {
  return (
    <ZevAuthProvider
      publishableKey={process.env.EXPO_PUBLIC_ZEVAUTH_KEY!}
      redirectUrl="https://acme.example/auth/callback"
    >
      <Slot />
    </ZevAuthProvider>
  );
}
```

| Prop | Meaning |
| --- | --- |
| `publishableKey` | `pk_test_…` or `pk_live_…`. Required unless you pass `client`. |
| `redirectUrl` | Where sign-in links and ZevID return to your app. See [Deep links](#deep-links). Required. |
| `apiUrl` | Where the API lives. Defaults to `https://api.zevauth.net`. |
| `appearance` | How the screens look. See [Customising](#customising). |
| `localization` | The words on the screens. See [Wording](#wording). |
| `storage`, `crypto`, `browser` | Replacements for the Expo modules, for bare React Native or tests. |
| `client` | An existing client from `createZevAuth`, instead of a key. |

The provider:

- **Stores the session securely.** The refresh token goes in the Keychain (iOS)
  or Keystore (Android), on this device only, so it isn't restored from a
  backup onto another phone. The access token stays in memory.
- **Finishes links automatically.** When a link opens the app, whether a sign-in
  link, an email confirmation or a ZevID return, the sign-in completes without
  any code of yours.
- **Re-checks on return.** Each time the app comes back to the foreground, the
  session is checked. A session ended elsewhere signs out here too, for
  example after "sign out everywhere" or a password change.

## Hooks and control components

These are `@zevauth/react`'s own, re-exported:

```tsx
import { useUser, useAuth, SignedIn, SignedOut, Protect } from '@zevauth/expo';

const { isLoaded, isSignedIn, user } = useUser();
const { userId, organizationId, getToken, signOut } = useAuth();
```

`getToken()` returns the access token for your own API, refreshing it first if
it's about to expire. Verify it on your server with
[`@zevauth/backend`](/sdk/backend/), exactly as for a web app.

## Screens

```tsx
import { SignIn, SignUp, UserButton, UserProfile } from '@zevauth/expo';

<SignIn onSignedIn={() => router.replace('/')} />
<SignUp onSignedUp={() => router.replace('/')} />
<UserButton onManageAccount={() => router.push('/account')} />
<UserProfile showSessions />
```

| Screen | What it does |
| --- | --- |
| `<SignIn>` | Whichever of password, email code, sign-in link and ZevID your environment enables. Then the second step for anyone with two-step verification, and setup when your app requires it. |
| `<SignUp>` | Creates the account and confirms the email with the emailed code. |
| `<UserButton>` | An avatar that opens a sheet with the person's name and Sign out. `onManageAccount` adds "Manage account". |
| `<UserAvatar>` | The picture, or initials. |
| `<UserProfile>` | Name, password, two-step verification, and with `showSessions` the person's signed-in devices. |
| `<TaskSetupMfa>` | Two-step setup on its own, for an app that routes signed-in people elsewhere. |

The screens never navigate. Each takes a callback such as `onSignedIn`, so your
router stays in charge.

Two-step setup on a phone has no QR code, because the person is holding the
device it would be scanned with. "Open authenticator app" hands the setup link
to whichever authenticator is installed, and the key is shown for typing in by
hand.

## Deep links

Sign-in links, email confirmations and Sign in with ZevID all come back to your
app through `redirectUrl`.

**In development**, use your app's custom scheme, such as
`acme://auth/callback`. Set `scheme` in `app.json`, then add the callback under
**Project settings → Domains & callbacks** in the console. Custom schemes work
in development environments only, because any app on a phone can claim a
scheme.

**In production**, use a universal link (iOS) or app link (Android) on your
verified domain, such as `https://acme.example/auth/callback`:

1. **Register the apps.** In the console under **Project settings → Mobile
   apps**, add each app: on iOS the bundle ID and Apple team ID, on Android the
   package name and SHA-256 signing certificate fingerprint.
2. **Serve the association files.** Host the two files the console generates
   for your domain: `/.well-known/apple-app-site-association` and
   `/.well-known/assetlinks.json`. Then press **Check again** until both apps
   show as verified.
3. **Declare the domain in your app.**

```json
{
  "expo": {
    "scheme": "acme",
    "ios": { "associatedDomains": ["applinks:acme.example"] },
    "android": {
      "intentFilters": [{
        "action": "VIEW",
        "autoVerify": true,
        "data": [{ "scheme": "https", "host": "acme.example", "pathPrefix": "/auth/callback" }],
        "category": ["BROWSABLE", "DEFAULT"]
      }]
    }
  }
}
```

4. **Register the callback.** Add `https://acme.example/auth/callback` under
   **Domains & callbacks**.

See [Mobile apps](/concepts/native-apps/) for how verification works and what
people see.

## Sign in with ZevID

The ZevID button opens the system authentication session:
`ASWebAuthenticationSession` on iOS, Custom Tabs on Android. It never uses an
embedded web view. The system browser shares the person's existing ZevID
session and password manager, and your app can't read what they type.

- **No second sign-in.** Someone already signed in to ZevID in Safari or Chrome
  continues without signing in again.
- **Tied to your app.** Each sign-in uses a PKCE challenge and a `state` value.
  The code that comes back can only be exchanged by your app, for the exact
  `redirectUrl` it was sent to.
- **Closing is safe.** If the person closes the sheet, the form stays as it was.
- **Your name on the consent screen.** With your apps registered and verified,
  ZevID's consent screen shows your app's name with the device, for example
  "Acme on iPhone".

## Customising

`appearance` uses the same ideas as the web, in native units:

```tsx
<ZevAuthProvider
  publishableKey="pk_live_..."
  redirectUrl="https://acme.example/auth/callback"
  appearance={{
    theme: 'soft',
    colorScheme: 'system',
    variables: {
      colorPrimary: { light: '#4f46e5', dark: '#a5b4fc' },
      fontFamily: 'Inter',
      fontSize: 16,
      borderRadius: 10,
    },
    elements: {
      buttonPrimary: { paddingVertical: 14 },
      title: { letterSpacing: -0.3 },
    },
  }}
>
```

- **`theme`** takes the same five names as the web: `default`, `soft`,
  `minimal`, `sharp` and `contrast`.
- **`variables`** take the web's colour names, each a value or a
  `{ light, dark }` pair. `fontSize` and `borderRadius` are numbers, in points.
- **`elements`** are React Native style objects, keyed by the web's element
  names (`card`, `title`, `input`, `buttonPrimary`, `zevIdButton` and so on).
  One design spec covers both apps. Native adds `buttonText` and
  `codeInput`.
- **The console's settings still apply.** Your accent, logo, radius and
  wording come from the console unless you override them here.

## Wording

`localization` takes exactly the web's shape, so a translation written for
your web app works unchanged. A `native` group covers the few phone-only
phrases:

```tsx
localization={{
  common: { email: 'E-mail', password: 'Mot de passe' },
  signIn: { submit: 'Se connecter' },
  native: { openAuthenticator: "Ouvrir l'application d'authentification" },
}}
```

## Bare React Native

The package uses three Expo modules: `expo-secure-store`, `expo-web-browser` and
`expo-crypto`. In a bare React Native app, add them with
`npx install-expo-modules`, or pass your own `storage`, `crypto` and `browser`
adapters to the provider.

> **Note:**
> Run your app as a development build (`npx expo run:ios` or `run:android`), not
> in Expo Go. Custom schemes, universal links and the keychain behave as they
> will in production only in your own build.

## A working example

[`zevauth-examples/expo`](https://github.com/zevop/zevauth-examples/tree/main/expo)
is a minimal Expo Router app with sign-in, the account button and the profile
with devices.
