> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chipipay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Passkeys

> Biometric authentication with automatic PIN backup for wallet security

## What Are Passkeys?

Passkeys use biometric authentication (Face ID, Touch ID, Windows Hello) to protect wallets. Instead of a PIN, a secure encryption key is derived from the biometric prompt. No passwords to remember, no PINs to leak.

**Dual-key architecture:** Every passkey wallet also has a mandatory PIN backup. If biometrics fail (browser change, device reset), the PIN recovers the wallet. Two independent keys, same private key.

```
Dual-key flow:
1. Biometric prompt → passkey-derived key → encrypts private key (primary)
2. User enters PIN → PIN encrypts same private key (backup)
3. Both stored in backend. Either can decrypt.
```

## Platform Support

| Platform                 | Technology                       | Package                      |
| ------------------------ | -------------------------------- | ---------------------------- |
| **Web (Next.js, React)** | WebAuthn PRF                     | `@chipi-stack/chipi-passkey` |
| **Mobile (Expo)**        | Face ID / Touch ID + SecureStore | `@chipi-stack/chipi-expo`    |

## Installation

<CodeGroup>
  ```bash Web (Next.js / React) theme={null}
  npm install @chipi-stack/chipi-passkey@latest
  ```

  ```bash Mobile (Expo) theme={null}
  npx expo install expo-local-authentication expo-secure-store
  ```
</CodeGroup>

## Create Wallet with Passkey + PIN

The recommended flow — passkey is primary, PIN is backup:

```typescript theme={null}
"use client";

import { useState } from "react";
import { useAuth } from "@clerk/nextjs";
import { useCreateWallet, Chain } from "@chipi-stack/nextjs";

export function CreateWallet({ userId }: { userId: string }) {
  const { getToken } = useAuth();
  const { createWalletAsync, isLoading, error } = useCreateWallet();
  const [pin, setPin] = useState("");

  const handleCreate = async () => {
    const bearerToken = await getToken();
    if (!bearerToken) return;

    // usePasskey: true triggers biometric prompt automatically.
    // encryptKey (PIN) becomes the backup — stored as encryptedPrivateKeyBackup.
    const wallet = await createWalletAsync({
      params: {
        encryptKey: pin,
        externalUserId: userId,
        chain: Chain.STARKNET,
        usePasskey: true,
      },
      bearerToken,
    });

    // wallet.authMethod === "passkey+pin"
    // wallet.encryptedPrivateKey → encrypted with passkey key
    // wallet.encryptedPrivateKeyBackup → encrypted with PIN
    localStorage.setItem("wallet", JSON.stringify(wallet));
  };

  return (
    <div>
      <input
        type="password"
        placeholder="Backup PIN (required)"
        value={pin}
        onChange={(e) => setPin(e.target.value)}
      />
      <button onClick={handleCreate} disabled={isLoading || !pin}>
        {isLoading ? "Creating..." : "Create Wallet with Passkey"}
      </button>
      {error && <p>Error: {error.message}</p>}
    </div>
  );
}
```

### What happens internally

1. `usePasskey: true` → calls `createWalletPasskey(userId, userId)` → biometric prompt
2. Returns `encryptKey` (passkey-derived) + `credentialId` + `prfSupported`
3. Since `encryptKey` (PIN) was also provided → dual-key mode:
   * Private key encrypted with passkey key → `encryptedPrivateKey`
   * Private key encrypted with PIN → `encryptedPrivateKeyBackup`
4. Both sent to backend with `authMethod: "passkey+pin"`

**Source:** `chipi-react/src/hooks/useCreateWallet.ts` lines 62-100

## Transfer with Passkey (Automatic PIN Fallback)

```typescript theme={null}
"use client";

import { useState } from "react";
import { useAuth } from "@clerk/nextjs";
import { useTransfer, ChainToken } from "@chipi-stack/nextjs";

export function Transfer({ wallet, userId }: { wallet: any; userId: string }) {
  const { getToken } = useAuth();
  const [recipient, setRecipient] = useState("");
  const [amount, setAmount] = useState("");

  // onPinRequired: called when passkey fails — show a PIN dialog
  const { transferAsync, isLoading, error } = useTransfer({
    onPinRequired: async () => {
      return prompt("Passkey failed. Enter your backup PIN:");
    },
  });

  const handleTransfer = async () => {
    const bearerToken = await getToken();
    if (!bearerToken) return;

    const txHash = await transferAsync({
      params: {
        wallet,
        token: ChainToken.USDC,
        recipient,
        amount: Number(amount),
        usePasskey: true,
        externalUserId: userId,
      },
      bearerToken,
    });

    alert(`Transfer complete: ${txHash}`);
  };

  return (
    <div>
      <input placeholder="Recipient" value={recipient} onChange={(e) => setRecipient(e.target.value)} />
      <input placeholder="Amount" type="number" value={amount} onChange={(e) => setAmount(e.target.value)} />
      <button onClick={handleTransfer} disabled={isLoading}>
        {isLoading ? "Sending..." : "Send with Passkey"}
      </button>
      {error && <p>Error: {error.message}</p>}
    </div>
  );
}
```

### Transfer flow

1. `usePasskey: true` → tries passkey authentication (biometric prompt)
2. If passkey succeeds → decrypts `encryptedPrivateKey` with passkey key → signs tx
3. If passkey fails (PRF unavailable, user cancelled, localStorage cleared):
   * Checks if `wallet.encryptedPrivateKeyBackup` exists
   * If yes → calls `onPinRequired()` → user enters PIN → decrypts backup key → signs tx
   * If no backup or no `onPinRequired` callback → throws descriptive error

**Source:** `chipi-react/src/hooks/useTransfer.ts` lines 65-110

### UseTransferConfig

```typescript theme={null}
interface UseTransferConfig {
  onPinRequired?: () => Promise<string | null>;
}
```

| Option          | Type                            | Description                                                                       |
| --------------- | ------------------------------- | --------------------------------------------------------------------------------- |
| `onPinRequired` | `() => Promise<string \| null>` | Called when passkey fails and backup exists. Return PIN string or null to cancel. |

## PIN-Only Mode (Backward Compatible)

Omit `usePasskey: true` for the same PIN-only flow as before:

```typescript theme={null}
const wallet = await createWalletAsync({
  params: {
    encryptKey: pin,
    externalUserId: userId,
    chain: Chain.STARKNET,
    // No usePasskey — PIN-only mode
  },
  bearerToken,
});
// wallet.authMethod === "pin"
```

## Migrate from PIN to Passkey

For existing PIN-only wallets:

```typescript theme={null}
import { useMigrateWalletToPasskey } from "@chipi-stack/nextjs";

const { migrateWalletToPasskeyAsync } = useMigrateWalletToPasskey();

await migrateWalletToPasskeyAsync({
  wallet,
  oldEncryptKey: currentPin,
  externalUserId: userId,
  bearerToken,
});
// Wallet re-encrypted with passkey key. Backend updated.
```

## Check Passkey Status

```typescript theme={null}
import { usePasskeyStatus } from "@chipi-stack/chipi-passkey/hooks";
import { verifyWalletPasskeyDetailed } from "@chipi-stack/chipi-passkey";

// Quick check
const { status } = usePasskeyStatus();
// status.isSupported, status.hasPasskey, status.isValid

// Detailed check (distinguishes PRF unavailable from missing passkey)
const result = await verifyWalletPasskeyDetailed();
// result: { valid: true } or { valid: false, reason: "prf_unavailable", message: "..." }
```

## Expo (Mobile) Passkeys

On mobile, the same `usePasskey: true` flag works automatically — the Expo `ChipiProvider` injects a native biometric adapter.

```typescript theme={null}
import { ChipiProvider } from "@chipi-stack/chipi-expo";
import { useCreateWallet } from "@chipi-stack/chipi-expo";

// Same API as web — usePasskey: true uses Face ID / Touch ID
const wallet = await createWalletAsync({
  params: {
    encryptKey: pin,
    externalUserId: userId,
    chain: Chain.STARKNET,
    usePasskey: true,
  },
  bearerToken,
});
```

If biometrics fail (user unenrolled, cancelled), the `onPinRequired` callback in `useTransfer` handles the fallback — same as web.

## Hooks Reference

| Hook                          | Purpose                                                                                             |
| ----------------------------- | --------------------------------------------------------------------------------------------------- |
| `usePasskeySetup`             | Create a new passkey manually (advanced — prefer `usePasskey: true` on `useCreateWallet`)           |
| `usePasskeyAuth`              | Authenticate with existing passkey manually (advanced — prefer `usePasskey: true` on `useTransfer`) |
| `usePasskeyStatus`            | Check WebAuthn/biometric support and stored passkey validity                                        |
| `verifyWalletPasskeyDetailed` | Detailed check with reason: `"prf_unavailable"`, `"no_passkey"`, `"error"`                          |
| `useMigrateWalletToPasskey`   | Migrate existing PIN wallet to passkey                                                              |

## Security

* **Dual-key wrapping**: Same private key encrypted by two independent keys. Either recovers.
* **No silent fallback**: If passkey was created with PRF, PBKDF2 fallback is blocked (prevents wrong-key decryption).
* **Backend credential recovery**: `credentialId` stored server-side. If localStorage cleared, SDK recovers from backend.
* **Hardware-backed**: Keys in Secure Enclave (iOS), Android Keystore, or WebAuthn authenticator.

## Related

* [Session Keys Guide](/sdk/guides/session-keys) — Combine passkeys with session keys
* [useCreateWallet](/sdk/nextjs/hooks/use-create-wallet) — Wallet creation hook reference
* [useTransfer](/sdk/nextjs/hooks/use-transfer) — Transfer hook reference
* [useMigrateWalletToPasskey](/sdk/nextjs/hooks/use-migrate-wallet-to-passkey) — Migration hook reference
