Skip to main content
Crossmint Auth is designed for staging and moving fast. For production applications, Crossmint strongly recommends connecting your own authentication provider for full control over user management. See the Bring Your Own Auth guide.
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.

Before you start

Set up your project and get an API key.

Expo Wallets Quickstart

See a full working example with auth and wallets.
1

Install the SDK

Install the Crossmint React Native SDK and its required Expo peer dependencies:
npm i @crossmint/client-sdk-react-native-ui expo-secure-store expo-web-browser expo-device
The SDK requires the following Expo packages:
PackagePurpose
expo-secure-storeEncrypted token storage (Keychain on iOS, Keystore on Android)
expo-web-browserOpens the OAuth consent screen in a system browser
expo-deviceDetects device capabilities for secure storage availability
expo-linkingListens for OAuth deep-link redirects back to your app
2

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):
app.json
{
    "expo": {
        "scheme": "myapp"
    }
}
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.
3

Add the Crossmint providers to your app

Wrap your application with CrossmintProvider and CrossmintAuthProvider.
providers.tsx
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>
    );
}
PropTypeDescription
appSchemastring | string[]Override the deep link scheme. Defaults to the scheme in your Expo config.
storageProviderStorageProviderCustom token storage implementation. Defaults to expo-secure-store.
onLoginSuccess(user) => voidCallback fired after a successful login.
4

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

LoginScreen.tsx
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)

OAuthButton.tsx
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>
    );
}
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.
5

Add logout and display user info

HomeScreen.tsx
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>
    );
}

Auth Status Values

The status field returned by useCrossmintAuth can be one of the following:
StatusDescription
"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

FeatureReact (Web)React Native
Login UIPre-built modal via login()Headless — build your own UI
Auth hookuseAuth()useCrossmintAuth()
OAuth flowPopup / redirect in browserSystem browser via expo-web-browser
OAuth callbackHandled automaticallyListen with Linking.useURL() + createAuthSession()
Token storageBrowser localStorage / cookiesexpo-secure-store (encrypted)
Package@crossmint/client-sdk-react-ui@crossmint/client-sdk-react-native-ui
Deep link schemeNot requiredRequired in app.json for OAuth
Email OTPHandled by modalCall sendEmailOtp / confirmEmailOtp manually

Moving to Production

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.
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 to integrate it with Crossmint via JWT.
  2. Create a developer account on the production console.
  3. Create a production client API key on the API Keys page with the API scopes users.create, users.read, wallets.read.
  4. Configure JWT authentication for your auth provider in the Crossmint Console.

Next Steps