Passkeys on React Native
The useRegisterPasskey and useLoginPasskey hooks work identically to the web on React Native, but they require a native passkey stamper and some platform setup.
Passkeys (WebAuthn) require the OS to verify that your app and your rpId domain belong together. On Android, the installed APK's signing-cert SHA-256 must match what the domain publishes in /.well-known/assetlinks.json; on iOS, the app's Team ID + bundle identifier must appear in the domain's /.well-known/apple-app-site-association and the app must carry the matching webcredentials: entitlement. Both are covered by the Domain Association setup.
Prerequisites
- Complete the Domain Association setup:
RP_IDpoints at your domain, the verification files (assetlinks.jsonandapple-app-site-association) are served from it, and the domain is on the Dashboard's Origin allowlist if you use one. On iOS this includes the Associated Domains entitlement, which needs a paid Apple Developer membership. - Use an Expo development build — see the quickstart.
1. Add the passkey stamper
Install the peer deps (native module — rebuild afterwards):
npx expo install @turnkey/react-native-passkey-stamper uuidWire it into wagmi.config.ts (see Configuration for all options):
import { createReactNativePasskeyStamper } from "@zerodev/wallet-core/react-native/stampers/passkey";
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 ?? "";
export const RP_ID = "example.com";
export const RP_ID = "<your domain>";
const chains = [sepolia, arbitrumSepolia] as const;
export const wagmiConfig = createConfig({
chains,
connectors: [
zeroDevWallet({
projectId: ZERODEV_PROJECT_ID,
chains,
rpId: RP_ID,
apiKeyStamper: createSecureStoreStamper(),
passkeyStamper: createReactNativePasskeyStamper({ rpId: RP_ID }),
sessionStorage: asyncStorageAdapter,
persistStorage: asyncStorageAdapter,
}),
],
transports: {
[sepolia.id]: http(),
[arbitrumSepolia.id]: http(),
},
storage: createStorage({ storage: asyncStorageAdapter }),
multiInjectedProviderDiscovery: false,
});2. Add register / login
import { useLoginPasskey, useRegisterPasskey } from "@zerodev/wallet-react";
import { Button, Text, View } from "react-native";
import { useAccount } from "wagmi";
/** Renders nothing once connected. The same component can be reused on web. */
export function PasskeyFlow() {
const { status } = useAccount();
const register = useRegisterPasskey();
const login = useLoginPasskey();
if (status === "connected") return null;
return (
<View style={{ gap: 8, padding: 16, borderWidth: 1, borderRadius: 8 }}>
<Text style={{ fontWeight: "600" }}>Sign in (Passkey)</Text>
<Button
title={register.isPending ? "Creating…" : "Create passkey wallet"}
disabled={register.isPending}
onPress={() => register.mutate(undefined)}
/>
<Button
title={login.isPending ? "Signing in…" : "Sign in with passkey"}
disabled={login.isPending}
onPress={() => login.mutate(undefined)}
/>
{register.error ? (
<Text style={{ color: "red" }}>{register.error.message}</Text>
) : null}
{login.error ? (
<Text style={{ color: "red" }}>{login.error.message}</Text>
) : null}
</View>
);
}The OS handles the passkey UI (biometric/PIN); the hooks auto-connect the wallet on success.
- Android needs Google Play services and an enrolled screen lock.
- iOS needs a device passcode (plus Face ID / Touch ID where available) — test on a physical device. An opaque "The operation couldn't be completed" error almost always means the AASA file and the app's entitlement don't match: re-run the verification checks in Domain Association, and remember that entitlement changes only land after
npx expo prebuild --cleanand a fresh build.
If the same Expo app also runs on web, keep this component shared. The React Native Web guide uses the same PasskeyFlow and lets the web build auto-default its WebAuthn stamper, so you do not need a separate .web variant for the passkey UI.