React Native Quickstart
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
- Create a new project on the ZeroDev Dashboard.
- Enable Sepolia and Arbitrum Sepolia.
1. Set up the project
Create a new Expo app:
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 i @zerodev/wallet-core @zerodev/wallet-reactInstall Wagmi (per the Wagmi getting started guide):
npm i wagmi viem@2.x @tanstack/react-queryInstall the crypto polyfill:
npx expo install react-native-get-random-valuesAdd 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:
npx expo install @react-native-async-storage/async-storage expo-secure-storeCreate 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:
npx expo install expo-dev-clientBuild and run on Android (environment setup):
npx expo run:androidOr on iOS — requires a Mac with Xcode (environment setup):
npx expo run:iosThe 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
- Configuration — Stampers, storage adapters, and connector options
- Google OAuth — Social login with Expo WebBrowser
- Domain Association — Link your app to a domain for passkeys and App Links
- Magic Link — Sign in with a link sent by email
- Passkeys — Native WebAuthn
- Export Wallet — Reveal the seed phrase or private key via WebView
- React Native Web — Run the same app on the web with react-native-web