> ## 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 Quickstart

> Let users withdraw stablecoins to their bank account from your React Native app

In this guide, you will add a withdrawal flow to an Expo app with the `CrossmintEmbeddedOfframp` component and run a withdrawal in the Staging environment. You will:

* Create an offramp session on your server for the user and the wallet they withdraw from
* Render `CrossmintEmbeddedOfframp` with the session and the user's Crossmint wallet
* Handle the component's events
* Test a withdrawal on staging

The component runs the whole withdrawal inside your app: the user picks, adds, renames or removes a bank account, enters an amount, verifies their identity if the order needs it, reviews the quote and approves the deposit from their wallet. Your server only creates the session.

<Snippet file="offramp-embedded-markets-note.mdx" />

## Prerequisites

* **Expo**: Expo SDK 58 (React Native 0.88), on the iOS and Android versions it [supports](https://docs.expo.dev/versions/latest/#support-for-android-and-ios-versions). A bare React Native app needs [Expo modules installed](https://docs.expo.dev/bare/installing-expo-modules/).
* **API keys**: a server-side API key with the `orders.create` and `payment-methods.create` scopes, and a [client-side API key](https://docs.crossmint.com/introduction/platform/api-keys/client-side.md) for the component, both from the [staging console](https://staging.crossmint.com/console).
* **Project setup**: KYC configured for your project and, for production, Offramp, bank account payment methods and the embedded offramp enabled on it. Crossmint sets these up per project; [contact Crossmint](https://www.crossmint.com/contact/sales) to enable them. You can test the embedded offramp on staging before they are enabled for production.
* **Crossmint wallet**: an EVM or Solana wallet owned by a user of your project with a `userId:` locator. The session refuses email and phone locators. With [your own authentication](https://docs.crossmint.com/wallets/guides/bring-your-own-auth.md), Crossmint sets the wallet's owner to the user ID in your JWT. Set up the providers as in the [Wallets React Native quickstart](https://docs.crossmint.com/wallets/quickstarts/react-native.md), with `base-sepolia` as the chain.
* **USDC**: testnet USDC in the wallet, from the [Circle faucet](https://faucet.circle.com/).
* **Camera access**: identity verification captures documents with the camera. Declare `android.permission.CAMERA` in `AndroidManifest.xml` and set `NSCameraUsageDescription` in `Info.plist`; neither is added for you. In an Expo app, set them in `app.json` under `android.permissions` and `ios.infoPlist`.

Install the Crossmint client SDK, version 2.1.1 or later:

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

The example App component also uses `react-native-safe-area-context`:

```bash theme={null}
npx expo install react-native-safe-area-context
```

Add your client-side API key to `.env.local`. The providers from the Wallets quickstart read it:

```bash .env.local theme={null}
EXPO_PUBLIC_CROSSMINT_API_KEY="_YOUR_CLIENT_API_KEY_"
```

Your backend reads the server-side API key from `CROSSMINT_SERVER_SIDE_API_KEY`.

### 1. Create the session on your server

A session lets one user make one withdrawal from one wallet. Create it with [Create Offramp Session](https://docs.crossmint.com/offramp/api-reference/create-offramp-session.md) on your backend, so your server API key never reaches the app. This example uses Express, but you can use any backend framework:

```typescript server/offrampSession.ts theme={null}
import express from "express";

const app = express();
app.use(express.json());

app.post("/api/offramp-session", async (req, res) => {
    const { userId, receiptEmail, walletAddress, chain } = req.body;
    const serverApiKey = process.env.CROSSMINT_SERVER_SIDE_API_KEY;

    if (serverApiKey == null) {
        return res.status(500).json({ error: "Server API key not configured" });
    }

    const response = await fetch("https://staging.crossmint.com/api/unstable/offramp-sessions", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "X-API-KEY": serverApiKey,
        },
        body: JSON.stringify({
            userLocator: `userId:${userId}`,
            payer: { address: walletAddress, chain },
            receiptEmail,
        }),
    });

    const data = await response.json();
    if (!response.ok) {
        return res.status(response.status).json(data);
    }

    res.json({ sessionId: data.sessionId, clientSecret: data.clientSecret });
});

app.listen(3000);
```

<Snippet file="offramp-embedded-session-fields.mdx" />

> [!WARNING]
>
> This example takes the user from the request body to stay short. In your backend, read the signed-in user's ID and email from your own authenticated session, so no one can open a session for another user.

### 2. Render the offramp

Create a screen that creates the session and renders `CrossmintEmbeddedOfframp`. `createCrossmintWalletSigner` turns the user's Crossmint wallet into the `payer` that signs and sends the deposit. With `fill`, the offramp fills the screen and scrolls inside itself:

```tsx screens/WithdrawScreen.tsx theme={null}
import { useState } from "react";
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from "react-native";
import {
    CrossmintEmbeddedOfframp,
    createCrossmintWalletSigner,
    useWallet,
    type EmbeddedOfframpCredentials,
} from "@crossmint/client-sdk-react-native-ui";

// Replace with your backend URL. A device or emulator cannot reach localhost: use your computer's LAN IP, or 10.0.2.2 on the Android emulator.
const API_URL = "http://192.168.1.10:3000";

export default function WithdrawScreen({ userId, email }: { userId: string; email: string }) {
    const { wallet } = useWallet();
    const [credentials, setCredentials] = useState<EmbeddedOfframpCredentials | null>(null);
    const [isLoading, setIsLoading] = useState(false);

    if (wallet == null) {
        return <Text>Sign in to load your wallet.</Text>;
    }

    const startWithdrawal = async () => {
        setIsLoading(true);
        try {
            const response = await fetch(`${API_URL}/api/offramp-session`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    userId,
                    receiptEmail: email,
                    walletAddress: wallet.address,
                    chain: wallet.chain,
                }),
            });
            if (!response.ok) {
                throw new Error("Failed to create the offramp session");
            }
            setCredentials(await response.json());
        } catch (error) {
            console.error("Failed to create the offramp session:", error);
        } finally {
            setIsLoading(false);
        }
    };

    if (credentials == null) {
        return (
            <TouchableOpacity style={styles.button} onPress={startWithdrawal} disabled={isLoading}>
                {isLoading ? (
                    <ActivityIndicator color="#fff" />
                ) : (
                    <Text style={styles.buttonText}>Withdraw to bank</Text>
                )}
            </TouchableOpacity>
        );
    }

    const close = () => setCredentials(null);

    return (
        <View style={styles.offramp}>
            <TouchableOpacity style={styles.closeButton} onPress={close}>
                <Text>Close</Text>
            </TouchableOpacity>
            <CrossmintEmbeddedOfframp
                credentials={credentials}
                payer={createCrossmintWalletSigner(wallet)}
                fill
                onStatusChange={({ status, orderId }) => console.log("Offramp status:", status, orderId)}
                onComplete={({ orderId, txId }) => console.log("Withdrawal sent:", orderId, txId)}
                onCancel={close}
                onError={({ reason, retriable, message }) => console.error("Offramp error:", reason, retriable, message)}
            />
        </View>
    );
}

const styles = StyleSheet.create({
    offramp: {
        flex: 1,
    },
    closeButton: {
        alignSelf: "flex-end",
        padding: 16,
    },
    button: {
        backgroundColor: "#000",
        padding: 16,
        borderRadius: 8,
        alignItems: "center",
        margin: 24,
    },
    buttonText: {
        color: "#fff",
        fontSize: 16,
        fontWeight: "500",
    },
});
```

Render the screen inside the Crossmint providers, with the signed-in user's ID and email:

```tsx App.tsx theme={null}
import { StyleSheet } from "react-native";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import CrossmintProviders from "./providers";
import WithdrawScreen from "./screens/WithdrawScreen";

export default function App() {
    return (
        <SafeAreaProvider>
            <CrossmintProviders>
                <SafeAreaView style={styles.container}>
                    <WithdrawScreen userId="<USER_ID>" email="alice@example.com" />
                </SafeAreaView>
            </CrossmintProviders>
        </SafeAreaProvider>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: "#fff",
    },
});
```

Without `fill`, the component is at least 650px tall plus padding, and grows when a screen needs more. See [Sizing](https://docs.crossmint.com/offramp/guides/embedded-offramp-reference.md#sizing).

### 3. Handle the events

<Snippet file="offramp-embedded-handle-events.mdx" />

### 4. Test on staging

1. Fund the wallet with testnet USDC from the [Circle faucet](https://faucet.circle.com/).
2. Tap **Withdraw to bank**, add a US bank account, and enter an amount. Use a bank account number that is unique to your integration.
3. If the user is not verified yet, verify their identity when the component asks.
4. Review the quote and confirm with **Hold to confirm**. The wallet signs and sends the deposit, and the status moves through `confirming-deposit` to `completed`, when `onComplete` fires.

On staging every payout is simulated and succeeds. To see a failed payout, add the US test bank account from [Test a Failed Payout](https://docs.crossmint.com/offramp/guides/test-payout-failures.md) in the component. The SEPA one appears only when SEPA is offered (see [Supported Markets](https://docs.crossmint.com/offramp/guides/embedded-offramp-reference.md#supported-markets)).

## Use an External Wallet

<Snippet file="offramp-embedded-external-wallet-payer.mdx" />

```typescript payer.ts theme={null}
import { OfframpSignatureRejectedError, type EmbeddedOfframpPayer } from "@crossmint/client-sdk-react-native-ui";

// Your wallet code: sign and broadcast the transaction, and resolve with its transaction ID.
declare function sendWithYourWallet(tx: { serializedTransaction: string; chain: string; payerAddress: string }): Promise<string>;
// Your wallet code: true when the user declined the request in their wallet.
declare function isUserRejection(error: unknown): boolean;

export const payer: EmbeddedOfframpPayer = {
    async signAndSendTransaction(tx) {
        try {
            return await sendWithYourWallet(tx);
        } catch (error) {
            if (isUserRejection(error)) {
                throw new OfframpSignatureRejectedError("The user declined the transaction");
            }
            throw error;
        }
    },
};
```

<Snippet file="offramp-embedded-external-wallet-proof.mdx" />

## Troubleshooting

<Snippet file="offramp-embedded-troubleshooting.mdx" />

> [accordion] The offramp does not load
>
> If the offramp page fails to load in the WebView, `onError` reports `provider-error` (retriable) for a connection failure or an HTTP 5xx, and `invalid-configuration` (not retriable) for an HTTP 4xx. The `message` carries the cause. The WebView then shows an error page with no retry or **Close** button, and `onReady` does not fire. To retry a connection failure or an HTTP 5xx, remount the component, for example by changing its `key`. For an HTTP 4xx, fix the integration.

## Launching in Production

<Snippet file="offramp-embedded-launch.mdx" />

## Learn More

> [card] [Embedded Offramp Reference](https://docs.crossmint.com/offramp/guides/embedded-offramp-reference.md)
>
> Props, events, statuses and errors

> [card] [Manage Orders](https://docs.crossmint.com/offramp/guides/manage-orders.md)
>
> Status, webhooks, and failures
