React Native
For the complete documentation index, see llms.txt. This page is also available as markdown.

React Native Quickstart

Get an Embedded Wallet running in Expo

ZeroDev Wallet runs on React Native. The same Wagmi-based API you use on the web works in native apps — the main difference is that on React Native the storages and stampers must be configured explicitly, since there are no platform defaults.

This guide walks you through setting up the SDK in an Expo app and signing in with email OTP. Once connected, sending transactions and signing messages is plain Wagmi.

Prerequisites

  1. Create a new project on the ZeroDev Dashboard.
  2. Enable Sepolia and Arbitrum Sepolia.

Set up the project

Create a new Expo app:

npx create-expo-app@latest <app_name>
yarn create expo-app <app_name>
pnpm create expo-app@latest <app_name>
bun create expo-app@latest <app_name>

Copy the project ID from the prerequisites into a .env file in the project root:

EXPO_PUBLIC_ZERODEV_PROJECT_ID=<your project id>

Install the SDK:

npm i @zerodev/[email protected] @zerodev/[email protected]
yarn add @zerodev/[email protected] @zerodev/[email protected]
pnpm add @zerodev/[email protected] @zerodev/[email protected]
bun add @zerodev/[email protected] @zerodev/[email protected]

Install Wagmi (per the Wagmi getting started guide):

npm i wagmi [email protected] @tanstack/react-query
yarn add wagmi [email protected] @tanstack/react-query
pnpm add wagmi [email protected] @tanstack/react-query
bun add wagmi [email protected] @tanstack/react-query

ZeroDev Wallet works with both Wagmi v2 and v3. Wagmi v3 still requires [email protected], so the pinned viem version above applies to either.

Install the polyfills (events is a shim for a Node module that React Native doesn't ship):

npx expo install react-native-get-random-values events
yarn expo install react-native-get-random-values events
pnpm expo install react-native-get-random-values events
bunx expo install react-native-get-random-values events

Add the polyfill at the very top of your root _layout.tsx:

import "react-native-get-random-values"; 

import { DarkTheme, DefaultTheme, ThemeProvider } from "expo-router";
import { useColorScheme } from "react-native";

import { AnimatedSplashOverlay } from "@/components/animated-icon";
import AppTabs from "@/components/app-tabs";

export default function TabLayout() {
  const colorScheme = useColorScheme();
  return (
    <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
      <AnimatedSplashOverlay />
      <AppTabs />
    </ThemeProvider>
  );
}

Configure the SDK

Install the SDK's remaining peer deps — the storage adapter and the secure store backing the API-key stamper:

npx expo install @react-native-async-storage/async-storage expo-secure-store
yarn expo install @react-native-async-storage/async-storage expo-secure-store
pnpm expo install @react-native-async-storage/async-storage expo-secure-store
bunx expo install @react-native-async-storage/async-storage expo-secure-store

Create wagmi.config.ts with the zeroDevWallet connector. This replaces the plain config from Wagmi's getting-started guide:

import { createSecureStoreStamper } from "@zerodev/wallet-core/react-native/stampers/secure-store";
import { asyncStorageAdapter } from "@zerodev/wallet-core/react-native/storage/async-storage";
import { zeroDevWallet } from "@zerodev/wallet-react";
import { createConfig, createStorage, http } from "wagmi";
import { arbitrumSepolia, sepolia } from "wagmi/chains";

const ZERODEV_PROJECT_ID = process.env.EXPO_PUBLIC_ZERODEV_PROJECT_ID ?? "";
// A domain — see the note below.
export const RP_ID = "example.com";

const chains = [sepolia, arbitrumSepolia] as const;

export const wagmiConfig = createConfig({
  chains,
  connectors: [
    zeroDevWallet({
      projectId: ZERODEV_PROJECT_ID,
      chains,
      rpId: RP_ID,
      apiKeyStamper: createSecureStoreStamper(),
      sessionStorage: asyncStorageAdapter,
      persistStorage: asyncStorageAdapter,
    }),
  ],
  transports: {
    [sepolia.id]: http(),
    [arbitrumSepolia.id]: http(),
  },
  storage: createStorage({ storage: asyncStorageAdapter }),
  multiInjectedProviderDiscovery: false,
});

declare module "wagmi" {
  interface Register {
    config: typeof wagmiConfig;
  }
}

About RP_ID

RP_ID is a domain, as defined by the WebAuthn standard. As long as you don't use passkeys or App Links for OAuth / magic-link redirects, it doesn't have to be a valid domain you own. If you do use those features, the domain association must be set up and served from this same domain. Also, if you specify an Access Control List of whitelisted Origins on the ZeroDev Dashboard, the same domain needs to be on the allowlist as https://${RP_ID}/.

See Configuration for what each option does and which ones are required on React Native.

Then wrap the app in the Wagmi and React Query providers in _layout.tsx:

import "react-native-get-random-values";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 
import { DarkTheme, DefaultTheme, ThemeProvider } from "expo-router";
import { useColorScheme } from "react-native";
import { WagmiProvider } from "wagmi"; 

import { AnimatedSplashOverlay } from "@/components/animated-icon";
import AppTabs from "@/components/app-tabs";
import { wagmiConfig } from "@/wagmi.config"; 

const queryClient = new QueryClient(); 

export default function TabLayout() {
  const colorScheme = useColorScheme();
  return (
    <WagmiProvider config={wagmiConfig}> {}
      <QueryClientProvider client={queryClient}> {}
        <ThemeProvider
          value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
        >
          <AnimatedSplashOverlay />
          <AppTabs />
        </ThemeProvider>
      </QueryClientProvider> {}
    </WagmiProvider> 
  );
}

Add OTP email auth

Email OTP is a two-step flow: send a code, then verify it. Verification connects the wallet (Wagmi's useAccount().status flips to "connected").

import { useSendOTP, useVerifyOTP } from "@zerodev/wallet-react"; 
import { useState } from "react";
import { Button, Text, TextInput } from "react-native";

const input = { borderWidth: 1, padding: 8, borderRadius: 4 };

export function OtpEmailFlow() {
  const [email, setEmail] = useState("");
  const [code, setCode] = useState("");
  const [otp, setOtp] = useState<{
    otpId: string;
    otpEncryptionTargetBundle: string;
  } | null>(null);

  const sendOTP = useSendOTP(); 
  const verifyOTP = useVerifyOTP(); 

  // Step 1: email -> send a code
  if (otp === null) {
    return (
      <>
        <TextInput
          value={email}
          onChangeText={setEmail}
          placeholder="[email protected]"
          autoCapitalize="none"
          keyboardType="email-address"
          style={input}
        />
        <Button
          title={sendOTP.isPending ? "Sending…" : "Send code"}
          disabled={sendOTP.isPending || !email}
          onPress={() => sendOTP.mutate({ email }, { onSuccess: setOtp })} 
        />
        {sendOTP.error ? (
          <Text style={{ color: "red" }}>{sendOTP.error.message}</Text>
        ) : null}
      </>
    );
  }

  // Step 2: code -> verify and connect the wallet
  return (
    <>
      <TextInput
        value={code}
        onChangeText={setCode}
        placeholder="123456"
        keyboardType="number-pad"
        style={input}
      />
      <Button
        title={verifyOTP.isPending ? "Verifying…" : "Verify"}
        disabled={verifyOTP.isPending || !code}
        onPress={() => verifyOTP.mutate({ code, ...otp })} 
      />
      {verifyOTP.error ? (
        <Text style={{ color: "red" }}>{verifyOTP.error.message}</Text>
      ) : null}
    </>
  );
}

The useSendOTP and useVerifyOTP hooks work identically to the web — see Email OTP for the full flow.

Send a transaction

Sending a transaction is plain Wagmi (useSendTransaction, useBalance, useAccount, …) — no ZeroDev-specific code needed:

import { parseEther } from "viem";
import { Button, Text } from "react-native";
import { useAccount, useSendTransaction } from "wagmi";

export function SendTransaction() {
  const { status } = useAccount();
  const { sendTransaction, isPending, data: hash, error } = useSendTransaction();

  if (status !== "connected") return null;

  return (
    <>
      <Button
        title={isPending ? "Sending…" : "Send 0.001 ETH"}
        disabled={isPending}
        onPress={() =>
          sendTransaction({
            to: "0xd2135CfB216b74109775236E36d4b433F1DF507B",
            value: parseEther("0.001"),
          })
        }
      />
      {hash ? <Text>Transaction hash: {hash}</Text> : null}
      {error ? <Text style={{ color: "red" }}>{error.message}</Text> : null}
    </>
  );
}

Run the app — you now have a fully working wallet app: sign in with the email OTP flow to connect the wallet, then send a transaction.

Switch to an Expo development build

The features in the next guides (OAuth deep links, WebView export, passkeys) rely on native modules that Expo Go doesn't ship, so switch to a development build:

npx expo install expo-dev-client
yarn expo install expo-dev-client
pnpm expo install expo-dev-client
bunx expo install expo-dev-client

Build and run on Android (environment setup):

npx expo run:android
yarn expo run:android
pnpm expo run:android
bunx expo run:android

Or on iOS — requires a Mac with Xcode (environment setup):

npx expo run:ios
yarn expo run:ios
pnpm expo run:ios
bunx expo run:ios

The first run generates the native project (expo prebuild), builds it, installs the dev client on the emulator / Simulator, and starts the bundler. If app.json doesn't define android.package / ios.bundleIdentifier yet, the CLI prompts for one.

What runs where

Email OTP, Google OAuth, and wallet export work on both platforms with no further setup — on iOS they run in the Simulator with a free Apple account. Passkeys and verified https links (Magic Link) additionally require the domain association setup: free on Android, while the iOS half (the Associated Domains entitlement) needs a paid Apple Developer membership and is best tested on a physical device.

Next Steps

On this page