Skip to content

Passkeys on React Native

Native WebAuthn on Android & iOS

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_ID points at your domain, the verification files (assetlinks.json and apple-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):

npm
npx expo install @turnkey/react-native-passkey-stamper uuid

Wire 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 --clean and 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.