> ## 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.

# Use Passkeys with PIN Backup

> Secure wallet authentication with Face ID / Touch ID on iOS and fingerprint on Android, with a mandatory PIN backup for recovery.

## Overview

On mobile, passkeys work differently from browsers. Instead of WebAuthn, the Chipi Expo SDK uses:

* **`expo-local-authentication`** — Face ID, Touch ID, and fingerprint gate
* **`expo-secure-store`** — iOS Keychain / Android Keystore for protected key storage

When you set `usePasskey: true`, the SDK generates a random encryption key, stores it in the device's secure enclave, and gates it behind biometric authentication.

<Warning>
  **Always pair a passkey with a PIN backup.** The Chipi Passkeys guide
  ([dual-key architecture](/sdk/guides/passkeys)) requires every passkey wallet
  to have a mandatory PIN backup so that biometric deletion, device reset, or
  OS-level passkey loss does not permanently lock the wallet. Pass the user's
  PIN as `encryptKey` alongside `usePasskey: true` — the SDK stores two
  encrypted copies of the private key: one unlocked by the biometric key, one
  unlocked by the PIN. Passkey-only mode (no `encryptKey`) is supported but
  labelled *not recommended* in the SDK source and should not be used in
  production.
</Warning>

<Note>
  This is a native mobile implementation. For browser (Next.js / React), see
  [Use Passkeys (React)](/sdk/react/use-passkeys) which uses WebAuthn instead.
</Note>

## Prerequisites

* Expo SDK 55 or later
* A physical iOS or Android device **with biometrics enrolled** (Face ID, Touch ID, or fingerprint)
* A [development build](https://docs.expo.dev/develop/development-builds/introduction/) — biometrics are **not** supported in Expo Go

<Warning>
  The iOS Simulator does not support biometric authentication. You must test on
  a real device or configure the simulator to use device passcode fallback.
</Warning>

## Installation

```bash theme={null}
npx expo install expo-local-authentication expo-secure-store
```

## Configuration

Add the `expo-local-authentication` plugin to your `app.json` to request Face ID permission on iOS:

```json theme={null}
{
  "expo": {
    "plugins": [
      [
        "expo-local-authentication",
        {
          "faceIDPermission": "Allow $(PRODUCT_NAME) to use Face ID to secure your wallet."
        }
      ]
    ]
  }
}
```

<Warning>
  If you skip `faceIDPermission`, Apple will reject your app during review and
  Face ID will fall back to device passcode on iOS without an explanation to the
  user.
</Warning>

After updating `app.json`, rebuild your development client:

```bash theme={null}
npx expo run:ios
# or
npx expo run:android
```

## Usage

### Create a wallet with biometric passkey + PIN backup

Pass `usePasskey: true`, an `externalUserId`, and the user's PIN as
`encryptKey`. The SDK uses the biometric-derived key as the primary
encryption key and stores a second copy of the private key encrypted with the
PIN as a recovery fallback.

```typescript theme={null}
import { useCreateWallet, Chain } from '@chipi-stack/chipi-expo';
import * as SecureStore from 'expo-secure-store';
import { useAuth } from '@clerk/clerk-expo';
import { useState } from 'react';

export function CreateWalletScreen() {
  const { getToken, userId } = useAuth();
  const { createWalletAsync, isLoading, error } = useCreateWallet();
  const [pin, setPin] = useState('');

  const handleCreateWallet = async () => {
    const token = await getToken();
    if (!token || !userId || pin.length < 4) return;

    // Face ID / Touch ID prompt is shown automatically.
    // `encryptKey: pin` wires up the PIN backup — see
    // chipi-expo/src/hooks/useCreateWallet.ts for the dual-key swap logic.
    const wallet = await createWalletAsync({
      params: {
        usePasskey: true,
        externalUserId: userId,
        encryptKey: pin,
        chain: Chain.STARKNET,
      },
      bearerToken: token,
    });

    // `createWalletAsync` returns a flat `GetWalletResponse` (no nested
    // `wallet` field — see types/src/wallet.ts: `CreateWalletResponse = GetWalletResponse`).
    await SecureStore.setItemAsync('wallet', JSON.stringify(wallet));
  };

  return (/* your UI — collect the PIN, then call handleCreateWallet */);
}
```

### Sign a transaction (retrieve the key with biometrics)

When you need the encryption key later (e.g. to sign a transfer), retrieve it from secure storage — this automatically triggers the Face ID / Touch ID prompt:

```typescript theme={null}
import { useTransfer, getNativeWalletEncryptKey, ChainToken } from '@chipi-stack/chipi-expo';
import * as SecureStore from 'expo-secure-store';
import { useAuth } from '@clerk/clerk-expo';

export function TransferScreen() {
  const { getToken, userId } = useAuth();
  const { transferAsync, isLoading } = useTransfer();

  const handleTransfer = async () => {
    const token = await getToken();
    if (!token || !userId) return;

    // Triggers Face ID / Touch ID automatically
    const encryptKey = await getNativeWalletEncryptKey(userId);
    if (!encryptKey) throw new Error('No wallet key found. Please re-create your wallet.');

    const storedWallet = await SecureStore.getItemAsync('wallet');
    if (!storedWallet) throw new Error('No wallet found.');

    await transferAsync({
      params: {
        encryptKey,
        wallet: JSON.parse(storedWallet),
        token: ChainToken.USDC,
        recipient: '0x...',
        amount: 1.0,
      },
      bearerToken: token,
    });
  };

  return (/* your UI */);
}
```

### Use `usePasskey: true` directly in transaction hooks

`useTransfer`, `useApprove`, and `useCallAnyContract` now use the Expo native
passkey adapter automatically when wrapped with `@chipi-stack/chipi-expo`'s
`ChipiProvider`.

When you pass `usePasskey: true` to those hooks, also pass
`externalUserId` (usually your auth provider user id), so the SDK can fetch
the correct key from secure storage:

```typescript theme={null}
await transferAsync({
  params: {
    wallet,
    token: ChainToken.USDC,
    recipient: "0x...",
    amount: 1,
    usePasskey: true,
    externalUserId: userId,
  },
  bearerToken: token,
});
```

### Migrate an existing PIN wallet to biometrics

If your users already have a PIN-based wallet, migrate them with one call:

```typescript theme={null}
import { useMigrateWalletToPasskey } from '@chipi-stack/chipi-expo';
import * as SecureStore from 'expo-secure-store';
import { useAuth } from '@clerk/clerk-expo';

export function MigrateScreen({ currentPin }: { currentPin: string }) {
  const { getToken, userId } = useAuth();
  const { migrateWalletToPasskeyAsync, isLoading, error } = useMigrateWalletToPasskey();

  const handleMigrate = async () => {
    const token = await getToken();
    const storedWallet = await SecureStore.getItemAsync('wallet');
    if (!token || !userId || !storedWallet) return;

    // Face ID / Touch ID prompt is shown during migration
    const result = await migrateWalletToPasskeyAsync({
      wallet: JSON.parse(storedWallet),
      oldEncryptKey: currentPin,
      externalUserId: userId,
      bearerToken: token,
    });

    // `migrateWalletToPasskeyAsync` returns MigrateWalletToPasskeyResult,
    // a wrapped shape: { success: boolean, wallet: WalletData, credentialId: string }.
    // See chipi-react/src/hooks/useMigrateWalletToPasskey.ts for the type.
    // Replace the stored wallet with the re-encrypted inner `wallet` field:
    await SecureStore.setItemAsync('wallet', JSON.stringify(result.wallet));
    // currentPin no longer works — biometrics are now required
  };

  return (/* your UI */);
}
```

<Warning>
  After migration, the old PIN (`oldEncryptKey`) will no longer decrypt the
  wallet. Store the updated wallet object returned by
  `migrateWalletToPasskeyAsync` immediately.
</Warning>

## Full Example

<Tabs>
  <Tab title="Create Wallet">
    ```typescript theme={null}
    import { useCreateWallet, Chain } from '@chipi-stack/chipi-expo';
    import type { GetWalletResponse } from '@chipi-stack/chipi-expo';
    import * as SecureStore from 'expo-secure-store';
    import { useAuth } from '@clerk/clerk-expo';
    import { useState } from 'react';
    import { View, Text, TextInput, TouchableOpacity, Alert, StyleSheet } from 'react-native';

    export function CreateWalletWithPasskey() {
      const { getToken, userId } = useAuth();
      const { createWalletAsync, isLoading, error } = useCreateWallet();
      const [pin, setPin] = useState('');
      const [wallet, setWallet] = useState<GetWalletResponse | null>(null);

      const handleCreate = async () => {
        try {
          const token = await getToken();
          if (!token || !userId) {
            Alert.alert('Error', 'No auth token or user ID found.');
            return;
          }
          if (pin.length < 4) {
            Alert.alert('Error', 'Please set a PIN (at least 4 digits) as your backup.');
            return;
          }

          // Face ID / Touch ID prompt shown automatically.
          // `encryptKey: pin` wires the dual-key flow (biometric primary + PIN backup).
          const created = await createWalletAsync({
            params: {
              usePasskey: true,
              externalUserId: userId,
              encryptKey: pin,
              chain: Chain.STARKNET,
            },
            bearerToken: token,
          });

          // `createWalletAsync` returns a flat GetWalletResponse — the wallet
          // fields (publicKey, normalizedPublicKey, etc.) live at the top level
          // of `created`, not nested under `created.wallet`.
          await SecureStore.setItemAsync('wallet', JSON.stringify(created));
          setWallet(created);
          Alert.alert('Success', 'Wallet created — biometrics primary, PIN backup.');
        } catch (err) {
          Alert.alert('Error', err instanceof Error ? err.message : String(err));
        }
      };

      return (
        <View style={styles.container}>
          <Text style={styles.title}>Create Wallet</Text>
          <Text style={styles.subtitle}>Secured with Face ID / Touch ID + PIN backup</Text>

          <TextInput
            style={styles.pinInput}
            placeholder="Set a PIN (min 4 digits — used as backup if biometrics fail)"
            value={pin}
            onChangeText={setPin}
            keyboardType="number-pad"
            secureTextEntry
            maxLength={8}
          />

          <TouchableOpacity
            style={[styles.button, isLoading && styles.buttonDisabled]}
            onPress={handleCreate}
            disabled={isLoading || pin.length < 4}
          >
            <Text style={styles.buttonText}>
              {isLoading ? 'Creating...' : 'Create Wallet with Biometrics + PIN'}
            </Text>
          </TouchableOpacity>

          {error && <Text style={styles.error}>{error.message}</Text>}
          {wallet && (
            <Text style={styles.address} numberOfLines={2}>
              {wallet.normalizedPublicKey ?? wallet.publicKey}
            </Text>
          )}
        </View>
      );
    }

    const styles = StyleSheet.create({
      container: { flex: 1, padding: 20 },
      title: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 },
      subtitle: { color: '#687076', marginBottom: 24 },
      pinInput: { borderWidth: 1, borderColor: '#ccc', borderRadius: 8, padding: 12, fontSize: 16, marginBottom: 16 },
      button: { backgroundColor: '#007AFF', borderRadius: 8, padding: 16, alignItems: 'center' },
      buttonDisabled: { opacity: 0.5 },
      buttonText: { color: '#fff', fontWeight: '600', fontSize: 16 },
      error: { color: '#ff3b30', marginTop: 12 },
      address: { fontFamily: 'monospace', fontSize: 12, marginTop: 16, color: '#333' },
    });
    ```
  </Tab>

  <Tab title="Transfer with Biometrics">
    ```typescript theme={null}
    import { useTransfer, getNativeWalletEncryptKey, ChainToken } from '@chipi-stack/chipi-expo';
    import * as SecureStore from 'expo-secure-store';
    import { useAuth } from '@clerk/clerk-expo';
    import { useState } from 'react';
    import { View, TextInput, TouchableOpacity, Alert, Text, StyleSheet } from 'react-native';

    export function TransferWithPasskey() {
      const { getToken, userId } = useAuth();
      const { transferAsync, isLoading } = useTransfer();
      const [recipient, setRecipient] = useState('');
      const [amount, setAmount] = useState('');

      const handleTransfer = async () => {
        try {
          const token = await getToken();
          if (!token || !userId) return;

          // Triggers Face ID / Touch ID — user authenticates to access the key
          const encryptKey = await getNativeWalletEncryptKey(userId);
          if (!encryptKey) {
            Alert.alert('Error', 'No wallet key found. Create a wallet first.');
            return;
          }

          const storedWallet = await SecureStore.getItemAsync('wallet');
          if (!storedWallet) {
            Alert.alert('Error', 'No wallet found.');
            return;
          }

          const txHash = await transferAsync({
            params: {
              encryptKey,
              wallet: JSON.parse(storedWallet),
              token: ChainToken.USDC,
              recipient,
              amount: Number(amount),
            },
            bearerToken: token,
          });

          Alert.alert('Success', `Transfer sent! TX: ${txHash}`);
          setRecipient('');
          setAmount('');
        } catch (err) {
          Alert.alert('Error', err instanceof Error ? err.message : String(err));
        }
      };

      return (
        <View style={styles.container}>
          <TextInput
            style={styles.input}
            placeholder="Recipient address (0x...)"
            value={recipient}
            onChangeText={setRecipient}
          />
          <TextInput
            style={styles.input}
            placeholder="Amount (USDC)"
            value={amount}
            onChangeText={setAmount}
            keyboardType="decimal-pad"
          />
          <TouchableOpacity
            style={[styles.button, isLoading && styles.disabled]}
            onPress={handleTransfer}
            disabled={isLoading || !recipient || !amount}
          >
            <Text style={styles.buttonText}>
              {isLoading ? 'Sending...' : 'Send USDC (Biometric Auth)'}
            </Text>
          </TouchableOpacity>
        </View>
      );
    }

    const styles = StyleSheet.create({
      container: { gap: 12, padding: 20 },
      input: { borderWidth: 1, borderColor: '#ccc', borderRadius: 8, padding: 12, fontSize: 16 },
      button: { backgroundColor: '#007AFF', borderRadius: 8, padding: 16, alignItems: 'center' },
      disabled: { opacity: 0.5 },
      buttonText: { color: '#fff', fontWeight: '600' },
    });
    ```
  </Tab>
</Tabs>

## How it Works

| Step                | What happens                                                                                                                            |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Wallet creation** | SDK generates a random 32-byte key, prompts Face ID / Touch ID, stores key in iOS Keychain / Android Keystore with biometric protection |
| **Wallet use**      | `getNativeWalletEncryptKey(userId)` retrieves the key — this automatically triggers the biometric prompt                                |
| **Migration**       | Old PIN decrypts the private key, new biometric key re-encrypts it                                                                      |

## Security Notes

* The encryption key is stored in the device's **secure enclave** (iOS Keychain / Android Keystore)
* `requireAuthentication: true` means the key **cannot be read without biometric approval**
* The key never leaves the device
* If the user uninstalls the app or re-installs it, the key is lost — ensure your users understand wallet recovery options

## Utilities Reference

| Export                                        | Description                                                        |
| --------------------------------------------- | ------------------------------------------------------------------ |
| `isNativeBiometricSupported()`                | Check if the device has enrolled biometrics                        |
| `createNativeWalletPasskey(userId, userName)` | Manually create a passkey (called by `useCreateWallet` internally) |
| `getNativeWalletEncryptKey(userId)`           | Retrieve the stored key (triggers biometric prompt)                |
| `hasNativeWalletPasskey()`                    | Check if a passkey was already created on this device              |
| `removeNativeWalletPasskey(userId)`           | Delete the stored key (destructive — use with caution)             |
| `getNativeWalletCredential()`                 | Get credential metadata (credentialId, userId, createdAt)          |

## Related

* [Use Biometrics as a PIN](/sdk/expo/use-biometrics) — Manual approach storing your own PIN with biometric protection
* [useCreateWallet](/sdk/expo/hooks/use-create-wallet)
* [useMigrateWalletToPasskey](/sdk/expo/hooks/use-migrate-wallet-to-passkey)
