> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crossmint.com/llms.txt
> Use this file to discover all available pages before exploring further.

# React Native

> Authenticate users in your Expo app in under 5 minutes

<Snippet file="auth-staging-note.mdx" />

<Note>
  Crossmint Auth on React Native is **headless** — there is no pre-built login modal.
  You build your own UI and call the SDK hooks directly. This guide walks through both
  **email OTP** and **OAuth** login flows.
</Note>

<CardGroup cols={2}>
  <Snippet file="before-you-start.mdx" />

  <Card title="Expo Wallets Quickstart" icon="github" iconType="duotone" href="https://github.com/Crossmint/wallets-expo-quickstart">
    See a full working example with auth and wallets.
  </Card>
</CardGroup>

<Steps>
  <Step title="Install the SDK">
    Install the Crossmint React Native SDK and its required Expo peer dependencies:

    <Snippet file="client-sdk-react-native-ui-installation-cmd.mdx" />

    The SDK requires the following Expo packages:

    | Package             | Purpose                                                        |
    | ------------------- | -------------------------------------------------------------- |
    | `expo-secure-store` | Encrypted token storage (Keychain on iOS, Keystore on Android) |
    | `expo-web-browser`  | Opens the OAuth consent screen in a system browser             |
    | `expo-device`       | Detects device capabilities for secure storage availability    |
    | `expo-linking`      | Listens for OAuth deep-link redirects back to your app         |
  </Step>

  <Step title="Configure your app scheme">
    OAuth redirects require a **deep link scheme** so the browser can return to your app after login.

    Add a `scheme` to your `app.json` (or `app.config.ts`):

    ```json app.json theme={null}
    {
        "expo": {
            "scheme": "myapp"
        }
    }
    ```

    <Warning>
      **Expo Go** uses a different scheme at runtime (`exp://127.0.0.1:8081`). The SDK
      detects Expo Go automatically and adjusts the redirect URI, so OAuth works during
      development without extra configuration. For production builds, the `scheme` value
      above is used.
    </Warning>
  </Step>

  <Step title="Add the Crossmint providers to your app">
    Wrap your application with `CrossmintProvider` and `CrossmintAuthProvider`.

    ```tsx providers.tsx theme={null}
    import {
        CrossmintProvider,
        CrossmintAuthProvider,
    } from "@crossmint/client-sdk-react-native-ui";

    type ProvidersProps = {
        children: React.ReactNode;
    };

    export default function Providers({ children }: ProvidersProps) {
        return (
            <CrossmintProvider
                apiKey={process.env.EXPO_PUBLIC_CROSSMINT_API_KEY!}
            >
                <CrossmintAuthProvider>
                    {children}
                </CrossmintAuthProvider>
            </CrossmintProvider>
        );
    }
    ```

    <Accordion title="CrossmintAuthProvider Props">
      | Prop              | Type                 | Description                                                                  |
      | ----------------- | -------------------- | ---------------------------------------------------------------------------- |
      | `appSchema`       | `string \| string[]` | Override the deep link scheme. Defaults to the `scheme` in your Expo config. |
      | `storageProvider` | `StorageProvider`    | Custom token storage implementation. Defaults to `expo-secure-store`.        |
      | `onLoginSuccess`  | `(user) => void`     | Callback fired after a successful login.                                     |
    </Accordion>
  </Step>

  <Step title="Build the login screen">
    Use the `useCrossmintAuth` hook to access auth functions. Unlike the React web SDK, there is no
    pre-built modal — you call `loginWithOAuth` and `crossmintAuth.sendEmailOtp` / `crossmintAuth.confirmEmailOtp` directly.

    ### Email OTP Login

    ```tsx LoginScreen.tsx theme={null}
    import React, { useState } from "react";
    import {
        View,
        Text,
        TextInput,
        TouchableOpacity,
        ActivityIndicator,
        Alert,
    } from "react-native";
    import { useCrossmintAuth } from "@crossmint/client-sdk-react-native-ui";

    export default function LoginScreen() {
        const { crossmintAuth, createAuthSession, status } =
            useCrossmintAuth();

        const [email, setEmail] = useState("");
        const [emailId, setEmailId] = useState("");
        const [otp, setOtp] = useState("");
        const [otpSent, setOtpSent] = useState(false);
        const [isPending, setIsPending] = useState(false);

        const sendOtp = async () => {
            if (!email.trim()) {
                Alert.alert("Error", "Enter a valid email address");
                return;
            }
            setIsPending(true);
            try {
                const res =
                    await crossmintAuth?.sendEmailOtp(email);
                if (res == null) {
                    throw new Error("Auth client not ready");
                }
                setEmailId(res.emailId);
                setOtpSent(true);
            } catch (error) {
                const message =
                    error instanceof Error
                        ? error.message
                        : "Failed to send OTP.";
                Alert.alert("Error", message);
            } finally {
                setIsPending(false);
            }
        };

        const verifyOtp = async () => {
            if (!otp.trim()) {
                Alert.alert("Error", "Enter the OTP code");
                return;
            }
            setIsPending(true);
            try {
                const oneTimeSecret =
                    await crossmintAuth?.confirmEmailOtp(
                        email,
                        emailId,
                        otp
                    );
                if (oneTimeSecret == null) {
                    throw new Error("Auth client not ready");
                }
                await createAuthSession(oneTimeSecret);
            } catch (error) {
                const message =
                    error instanceof Error
                        ? error.message
                        : "Invalid OTP. Try again.";
                Alert.alert("Error", message);
            } finally {
                setIsPending(false);
            }
        };

        if (status === "initializing") {
            return (
                <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
                    <ActivityIndicator size="large" />
                </View>
            );
        }

        return (
            <View style={{ flex: 1, padding: 20, justifyContent: "center" }}>
                <TextInput
                    style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
                    placeholder="Enter your email"
                    value={email}
                    onChangeText={setEmail}
                    editable={!otpSent}
                />

                {!otpSent ? (
                    <TouchableOpacity
                        style={{ backgroundColor: "#05b959", padding: 15, alignItems: "center" }}
                        onPress={sendOtp}
                        disabled={isPending}
                    >
                        {isPending ? (
                            <ActivityIndicator color="#fff" size="small" />
                        ) : (
                            <Text style={{ color: "#fff" }}>Send OTP</Text>
                        )}
                    </TouchableOpacity>
                ) : (
                    <>
                        <TextInput
                            style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
                            placeholder="Enter OTP code"
                            value={otp}
                            onChangeText={setOtp}
                        />
                        <TouchableOpacity
                            style={{ backgroundColor: "#05b959", padding: 15, alignItems: "center" }}
                            onPress={verifyOtp}
                            disabled={isPending}
                        >
                            {isPending ? (
                                <ActivityIndicator color="#fff" size="small" />
                            ) : (
                                <Text style={{ color: "#fff" }}>Verify OTP</Text>
                            )}
                        </TouchableOpacity>
                    </>
                )}
            </View>
        );
    }
    ```

    ### OAuth Login (Google, Twitter)

    ```tsx OAuthButton.tsx theme={null}
    import React, { useEffect } from "react";
    import { TouchableOpacity, Text } from "react-native";
    import { useCrossmintAuth } from "@crossmint/client-sdk-react-native-ui";
    import * as Linking from "expo-linking";

    export default function OAuthButton() {
        const { loginWithOAuth, createAuthSession } =
            useCrossmintAuth();

        const url = Linking.useURL();

        useEffect(() => {
            if (url != null) {
                createAuthSession(url);
            }
        }, [url, createAuthSession]);

        return (
            <TouchableOpacity
                style={{ backgroundColor: "#4285f4", padding: 15, alignItems: "center" }}
                onPress={() => loginWithOAuth("google")}
            >
                <Text style={{ color: "#fff" }}>
                    Sign in with Google
                </Text>
            </TouchableOpacity>
        );
    }
    ```

    <Accordion title="How OAuth Works on Android vs iOS">
      The SDK handles platform differences automatically:

      * **iOS**: Uses `WebBrowser.openAuthSessionAsync`, which opens an in-app authentication
        session (ASWebAuthenticationSession). The redirect URL is intercepted by the authentication
        session before it reaches your app's URL routing, so `Linking.useURL()` remains `null` after
        the OAuth flow. There is no double invocation — `createAuthSession` is called only once by
        the SDK internally.
      * **Android**: Uses `WebBrowser.openBrowserAsync`, which opens the system browser. The OAuth
        provider redirects back to your app via the deep link scheme. You must listen for the
        incoming URL with `Linking.useURL()` and pass it to `createAuthSession` yourself, as shown
        in the example above.

      The `Linking.useURL()` + `createAuthSession` pattern works on both platforms, so a single
      code path is sufficient.
    </Accordion>
  </Step>

  <Step title="Add logout and display user info">
    ```tsx HomeScreen.tsx theme={null}
    import React from "react";
    import { View, Text, TouchableOpacity } from "react-native";
    import { useCrossmintAuth } from "@crossmint/client-sdk-react-native-ui";

    export default function HomeScreen() {
        const { user, jwt, logout, status } = useCrossmintAuth();

        return (
            <View style={{ flex: 1, padding: 20 }}>
                <View
                    style={{
                        flexDirection: "row",
                        justifyContent: "space-between",
                        marginBottom: 20,
                    }}
                >
                    <Text style={{ fontSize: 20 }}>Welcome!</Text>
                    <TouchableOpacity onPress={logout}>
                        <Text style={{ color: "red" }}>Logout</Text>
                    </TouchableOpacity>
                </View>

                <Text>Email: {user?.email}</Text>
                <Text>User ID: {user?.id}</Text>
                <Text>Status: {status}</Text>
                <Text>JWT: {jwt ? "Present" : "None"}</Text>
            </View>
        );
    }
    ```
  </Step>
</Steps>

## Auth Status Values

The `status` field returned by `useCrossmintAuth` can be one of the following:

| Status           | Description                                                    |
| ---------------- | -------------------------------------------------------------- |
| `"initializing"` | The SDK is loading stored tokens and checking session validity |
| `"logged-out"`   | No active session. Show your login screen                      |
| `"in-progress"`  | An OAuth or OTP flow is currently in progress                  |
| `"logged-in"`    | The user is authenticated. `user` and `jwt` are available      |

## Token Storage

By default, the SDK stores authentication tokens using `expo-secure-store`, which encrypts data
at rest using the platform keychain (iOS) or keystore (Android). This means tokens persist
across app restarts and are protected by the device's hardware-backed security.

You can provide a custom storage implementation via the `storageProvider` prop on
`CrossmintAuthProvider` if you need a different storage backend.

## Differences from the React Web SDK

| Feature              | React (Web)                      | React Native                                           |
| -------------------- | -------------------------------- | ------------------------------------------------------ |
| **Login UI**         | Pre-built modal via `login()`    | Headless — build your own UI                           |
| **Auth hook**        | `useAuth()`                      | `useCrossmintAuth()`                                   |
| **OAuth flow**       | Popup / redirect in browser      | System browser via `expo-web-browser`                  |
| **OAuth callback**   | Handled automatically            | Listen with `Linking.useURL()` + `createAuthSession()` |
| **Token storage**    | Browser `localStorage` / cookies | `expo-secure-store` (encrypted)                        |
| **Package**          | `@crossmint/client-sdk-react-ui` | `@crossmint/client-sdk-react-native-ui`                |
| **Deep link scheme** | Not required                     | Required in `app.json` for OAuth                       |
| **Email OTP**        | Handled by modal                 | Call `sendEmailOtp` / `confirmEmailOtp` manually       |

## Moving to Production

<Warning>
  **Crossmint Auth is designed for staging and getting started quickly.** For production applications,
  Crossmint strongly recommends migrating to your own authentication provider for full control
  over user management.
</Warning>

When you are ready to go to production, Crossmint recommends:

1. **Set up your own auth provider** (Auth0, Firebase, Supabase, Stytch, etc.) and follow the [Bring Your Own Auth guide](/wallets/guides/bring-your-own-auth) to integrate it with Crossmint via JWT.
2. Create a developer account on the <a href="https://www.crossmint.com/console" target="_blank">production console</a>.
3. Create a production client API key on the <a href="https://www.crossmint.com/console/projects/apiKeys" target="_blank">API Keys</a> page with the API scopes `users.create`, `users.read`, `wallets.read`.
4. [Configure JWT authentication](/introduction/platform/api-keys/jwt-authentication) for your auth provider in the Crossmint Console.

## Next Steps

* [Create Smart Wallets on sign up](/wallets/quickstarts/react-native) using the wallets React Native quickstart
* [Bring Your Own Auth](/wallets/guides/bring-your-own-auth), recommended for production
* Read and update [user information](/authentication/user-profile)
* Use [webhooks](/authentication/webhooks) to get notified when a user signs up
* [React Native SDK Reference](/sdk-reference/wallets/react-native/get-started) for the full API documentation
