Skip to content

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.

1. Set up the project

Create a new Expo app:

npm
npx 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
npm i @zerodev/wallet-core @zerodev/wallet-react

Install Wagmi (per the Wagmi getting started guide):

npm
npm i wagmi viem@2.x @tanstack/react-query

Install the crypto polyfill:

npm
npx expo install react-native-get-random-values

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>
  );
}

2. Configure the SDK

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

npm
npx 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;
  }
}

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> 
  );
}

3. 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="you@example.com"
          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.

4. 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.

5. 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:

npm
npx expo install expo-dev-client

Build and run on Android (environment setup):

npm
npx expo run:android

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

npm
npx 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.

Next Steps