# Passkeys on React Native (/wallets/react-native/passkeys)

> For the complete documentation index, see [llms.txt](/llms.txt)



The `useRegisterPasskey` and `useLoginPasskey` hooks work identically to [the web](/wallets/auth/passkeys) 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](/wallets/react-native/domain-association) setup.

## Prerequisites [#prerequisites]

* Complete the [Domain Association](/wallets/react-native/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](https://developer.apple.com/programs/) membership.
* Use an [Expo development build](https://docs.expo.dev/develop/development-builds/expo-go-to-dev-build/) — see the [quickstart](/wallets/react-native/quickstart#5-switch-to-an-expo-development-build).

<div className="fd-steps">
  <div className="fd-step">
    ## Add the passkey stamper [#add-the-passkey-stamper]

    Install the peer deps (native module — rebuild afterwards):

    <Tabs items="[&#x22;npm&#x22;,&#x22;yarn&#x22;,&#x22;pnpm&#x22;,&#x22;bun&#x22;]">
      <Tab value="npm">
        ```bash
        npx expo install @turnkey/react-native-passkey-stamper uuid
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn expo install @turnkey/react-native-passkey-stamper uuid
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm expo install @turnkey/react-native-passkey-stamper uuid
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx expo install @turnkey/react-native-passkey-stamper uuid
        ```
      </Tab>
    </Tabs>

    Wire it into `wagmi.config.ts` (see [Configuration](/wallets/react-native/configuration) for all options):

    ```ts
    import { createReactNativePasskeyStamper } from "@zerodev/wallet-core/react-native/stampers/passkey"; // [!code ++]
    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"; // [!code --]
    export const RP_ID = "<your domain>"; // [!code ++]

    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 }), // [!code ++]
          sessionStorage: asyncStorageAdapter,
          persistStorage: asyncStorageAdapter,
        }),
      ],
      transports: {
        [sepolia.id]: http(),
        [arbitrumSepolia.id]: http(),
      },
      storage: createStorage({ storage: asyncStorageAdapter }),
      multiInjectedProviderDiscovery: false,
    });
    ```
  </div>

  <div className="fd-step">
    ## Add register / login [#add-register--login]

    ```tsx
    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*&#x2A; needs a device passcode (plus Face ID / Touch ID where available) — test on a physical device. An opaque &#x2A;"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](/wallets/react-native/domain-association#3-create-and-host-the-verification-files), 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](/wallets/react-native/web) 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.
  </div>
</div>
