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

# Add Native Passkeys

> Add native passkey authentication to your React Native or Expo app for enhanced security

export const Link = ({href, label, newTab = false}) => {
  const [isHovered, setIsHovered] = useState(false);
  return <a href={href} target={newTab ? '_blank' : '_self'} rel={newTab ? 'noopener noreferrer' : undefined} className="not-prose inline-block relative text-black font-semibold cursor-pointer border-b-0 no-underline" onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
      {label}
      <span className={`absolute left-0 bottom-0 w-full rounded-sm bg-gradient-to-r from-orange-600 to-purple-600 transition-all duration-300 ${isHovered ? 'h-0.5' : 'h-px'}`} />
    </a>;
};

Passkeys provide native biometric authentication using Face ID, Touch ID, or fingerprint -- no browser needed for the passkey step itself. This is an optional enhancement on top of the default email/phone login flow. When enabled, the SDK calls the device's native passkey APIs directly instead of opening a portal URL.

## Prerequisites

<CardGroup cols={2}>
  <Card horizontal title="Email & Phone Login" imgUrl="/images/v3/custom-ui-social.png" href="/v3/react-native/guides/add-email-phone" description="Complete the basic authentication setup first." />

  <Card horizontal title="Framework Setup" imgUrl="/images/v3/framework-react-native.png" href="/v3/react-native/setup/react-native" description="Set up your React Native or Expo project with Para." />
</CardGroup>

## Platform Configuration

Configure your app's passkey credentials so the OS can associate your app with Para's domain.

<Tabs>
  <Tab title="React Native (Bare)">
    <Tabs>
      <Tab title="iOS">
        In order for passkeys to work, you need to set up associated domains in your Xcode project linked to the Para domain.

        <Frame>
          <img src="https://mintcdn.com/getpara/8_K4LmkBd5jFZ3IC/images/v3/supporting-associated-domains.png?fit=max&auto=format&n=8_K4LmkBd5jFZ3IC&q=85&s=f68a03f6c99f32b5c60b422237d996e4" alt="Associated Domains" width="1436" height="272" data-path="images/v3/supporting-associated-domains.png" />
        </Frame>

        #### Set Up Associated Domains

        1. Open your project in Xcode
        2. Select your target and go to "Signing & Capabilities"
        3. Click "+ Capability" and add "Associated Domains"
        4. Add the following domains:
           * webcredentials:app.beta.usecapsule.com
           * webcredentials:app.usecapsule.com

        For additional information on associated domains, refer to the <Link label="Apple Developer documentation" href="https://developer.apple.com/documentation/xcode/supporting-associated-domains#Add-the-associated-domains-entitlement-to-your-app" />.

        <Warning>**Important**: Your `teamId + bundleIdentifier` must be registered with the Para team to set up associated domains. For example, if your Team ID is `A1B2C3D4E5` and Bundle Identifier is `com.yourdomain.yourapp`, provide `A1B2C3D4E5.com.yourdomain.yourapp` to Para. This is required by Apple for passkey security. **Note:** Allow up to 24 hours for domain propagation.</Warning>

        <Note>
          Getting ready for App Review? See <Link label="iOS App Store Submission" href="/v3/general/ios-app-store-submission" /> for Para-specific review tips.
        </Note>

        #### Install CocoaPods for native dependencies:

        ```bash theme={null}
        cd ios
        bundle install
        bundle exec pod install
        cd ..
        ```

        <Note>Remember to run `pod install` after adding new dependencies to your project.</Note>
      </Tab>

      <Tab title="Android">
        #### Set Up Digital Asset Links

        For Android setup, you need to provide your app's SHA-256 certificate fingerprint to Para.

        <Info>**Quick Testing Option**: You can use `com.getpara.example.reactnative` as your package name for immediate testing. This package name is pre-registered and works with the SHA-256 certificate from the default debug.keystore.</Info>

        <Info>The debug keystore at `~/.android/debug.keystore` is auto-generated by the Android SDK on your first build. You do not need to create it manually. Use `keytool -list` to **read** the existing keystore — do not use `keytool -genkey` which creates a new one and may cause a fingerprint mismatch.</Info>

        To get your SHA-256 fingerprint:

        * For debug builds: `keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android`
        * For release builds: `keytool -list -v -keystore <your_keystore_path>`

        <Warning>
          **Debug and release builds use different signing keys.** The SHA-256 fingerprint you register in the Developer Portal must match the keystore used to sign the build you are testing. If you register your release fingerprint but test with a debug build (or vice versa), passkeys will fail with an RP ID validation error. For development, register your debug keystore fingerprint. For production, register your release keystore fingerprint. You can register both in the Developer Portal.
        </Warning>

        <Warning>For production apps, you'll need to: 1. Upgrade your plan in the Developer Portal 2. Register your actual package name 3. Provide your app's SHA-256 fingerprint 4. Wait up to 24 hours for the Digital Asset Links to propagate</Warning>

        #### Verify Digital Asset Links

        After registering your package name and SHA-256 fingerprint, check the verification status in the <Link label="Developer Portal" href="https://developer.getpara.com" /> under your API key's Native Passkey Configuration. The portal automatically polls Google's Digital Asset Links service and will update the status once verification completes. Allow up to 24 hours for propagation.

        #### Device Requirements

        <Note>
          To ensure passkey functionality works correctly:

          * Enable biometric or device unlock settings (fingerprint, face unlock, or PIN)
          * Sign in to a Google account on the device (required for Google Play Services passkey management)
        </Note>
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="Expo">
    <Tabs>
      <Tab title="iOS">
        Configure your `app.json` file to enable passkey functionality and secure communication:

        ```json app.json theme={null}
        {
          "expo": {
            "ios": {
              "bundleIdentifier": "your.app.bundleIdentifier",
              "associatedDomains": [
                "webcredentials:app.beta.usecapsule.com?mode=developer",
                "webcredentials:app.usecapsule.com"
              ]
            }
          }
        }
        ```

        <Warning>**Important**: Your `teamId + bundleIdentifier` must be registered with the Para team to set up associated domains. For example, if your Team ID is `A1B2C3D4E5` and Bundle Identifier is `com.yourdomain.yourapp`, provide `A1B2C3D4E5.com.yourdomain.yourapp` to Para. This is required by Apple for passkey security. Allow up to 24 hours for domain propagation. You can find this setting in the Developer Portal under the 'Configuration' tab of the API key label as Native Passkey Configuration.</Warning>

        <Note>
          Prepping for App Review? Check <Link label="iOS App Store Submission" href="/v3/general/ios-app-store-submission" /> for Sign in with Apple tips, reviewer notes, and account deletion requirements.
        </Note>
      </Tab>

      <Tab title="Android">
        For Android setup in Expo, you'll need to configure your package name and provide your SHA-256 certificate fingerprint.

        <Info>**Quick Testing Option**: You can use `com.getpara.example.expo` as your package name in `app.json` for immediate testing. This package name is pre-registered but only works with the default debug.keystore generated by Expo.</Info>

        <Info>The debug keystore at `~/.android/debug.keystore` is auto-generated by the Android SDK on your first build. You do not need to create it manually. Use `keytool -list` to **read** the existing keystore — do not use `keytool -genkey` which creates a new one and may cause a fingerprint mismatch.</Info>

        Configure your `app.json`:

        ```json app.json theme={null}
        {
          "expo": {
            "android": {
              "package": "com.getpara.example.expo" // For testing
              // or your actual package name for production
            }
          }
        }
        ```

        To get your SHA-256 fingerprint:

        * For debug builds: `keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android`
        * For release builds: `keytool -list -v -keystore <your_keystore_path>`

        <Warning>**Important**: Your SHA-256 certificate fingerprint must be registered with the Para team to set up associated domains. This is required by Google for passkey security. Allow up to 24 hours for domain propagation. You can find this setting in the Developer Portal under the 'Configuration' tab of the API key label as Native Passkey Configuration.</Warning>

        <Warning>
          **Debug and release builds use different signing keys.** The SHA-256 fingerprint you register in the Developer Portal must match the keystore used to sign the build you are testing. If you register your release fingerprint but test with a debug build (or vice versa), passkeys will fail with an RP ID validation error (`[50152]`). For development, register your debug keystore fingerprint. For production, register your release keystore fingerprint. You can register both in the Developer Portal.
        </Warning>
      </Tab>
    </Tabs>
  </Tab>
</Tabs>

## State Listener with Native Passkeys

Modify the state listener to intercept passkey states and call `registerPasskey()` or `loginWithPasskey()` natively, falling back to portal URLs only for password/PIN users:

<Tabs>
  <Tab title="React Native">
    ```typescript theme={null}
    import { useEffect, useRef } from "react";
    import { para } from "../your-para-client";
    import { InAppBrowser } from "react-native-inappbrowser-reborn";
    import type { StateSnapshot } from "@getpara/react-native-wallet";

    const APP_SCHEME = "your-app-scheme";

    function useParaAuthStateListener(isAuthActive: boolean) {
      const lastUrlRef = useRef<string | null>(null);
      const handledRef = useRef<string | null>(null);

      useEffect(() => {
        if (!isAuthActive) return;

        const unsubscribe = para.onStatePhaseChange(async (snapshot: StateSnapshot) => {
          const { authPhase, authStateInfo } = snapshot;

          // Native passkey signup — register using the credential ID
          if (
            authStateInfo.isNewUser &&
            authStateInfo.passkeyId &&
            handledRef.current !== authStateInfo.passkeyId
          ) {
            handledRef.current = authStateInfo.passkeyId;
            await para.registerPasskey(authStateInfo.passkeyId);
            return;
          }

          // Native passkey login — trigger biometric prompt directly
          if (
            !authStateInfo.isNewUser &&
            authStateInfo.hasPasskey &&
            authPhase === "awaiting_session_start" &&
            handledRef.current !== "login"
          ) {
            handledRef.current = "login";
            await para.loginWithPasskey();
            return;
          }

          // Verification URL (basic login verification)
          if (authStateInfo.verificationUrl && authStateInfo.verificationUrl !== lastUrlRef.current) {
            lastUrlRef.current = authStateInfo.verificationUrl;
            InAppBrowser.openAuth(authStateInfo.verificationUrl, APP_SCHEME, {
              ephemeralWebSession: false,
              showTitle: false,
            });
            return;
          }

          // Fallback: password/PIN portal URLs for non-passkey users
          const url = authStateInfo.passwordUrl || authStateInfo.pinUrl;
          if (url && url !== lastUrlRef.current) {
            lastUrlRef.current = url;
            InAppBrowser.openAuth(url, APP_SCHEME, {
              ephemeralWebSession: false,
              showTitle: false,
            });
          }
        });

        return () => {
          unsubscribe();
          lastUrlRef.current = null;
          handledRef.current = null;
        };
      }, [isAuthActive]);
    }
    ```
  </Tab>

  <Tab title="Expo">
    ```typescript theme={null}
    import { useEffect, useRef } from "react";
    import { para } from "../your-para-client";
    import { openAuthSessionAsync } from "expo-web-browser";
    import type { StateSnapshot } from "@getpara/react-native-wallet";

    const APP_SCHEME = "your-app-scheme";

    function useParaAuthStateListener(isAuthActive: boolean) {
      const lastUrlRef = useRef<string | null>(null);
      const handledRef = useRef<string | null>(null);

      useEffect(() => {
        if (!isAuthActive) return;

        const unsubscribe = para.onStatePhaseChange(async (snapshot: StateSnapshot) => {
          const { authPhase, authStateInfo } = snapshot;

          // Native passkey signup — register using the credential ID
          if (
            authStateInfo.isNewUser &&
            authStateInfo.passkeyId &&
            handledRef.current !== authStateInfo.passkeyId
          ) {
            handledRef.current = authStateInfo.passkeyId;
            await para.registerPasskey(authStateInfo.passkeyId);
            return;
          }

          // Native passkey login — trigger biometric prompt directly
          if (
            !authStateInfo.isNewUser &&
            authStateInfo.hasPasskey &&
            authPhase === "awaiting_session_start" &&
            handledRef.current !== "login"
          ) {
            handledRef.current = "login";
            await para.loginWithPasskey();
            return;
          }

          // Verification URL (basic login verification)
          if (authStateInfo.verificationUrl && authStateInfo.verificationUrl !== lastUrlRef.current) {
            lastUrlRef.current = authStateInfo.verificationUrl;
            openAuthSessionAsync(authStateInfo.verificationUrl, APP_SCHEME, {
              preferEphemeralSession: false,
            });
            return;
          }

          // Fallback: password/PIN portal URLs for non-passkey users
          const url = authStateInfo.passwordUrl || authStateInfo.pinUrl;
          if (url && url !== lastUrlRef.current) {
            lastUrlRef.current = url;
            openAuthSessionAsync(url, APP_SCHEME, {
              preferEphemeralSession: false,
            });
          }
        });

        return () => {
          unsubscribe();
          lastUrlRef.current = null;
          handledRef.current = null;
        };
      }, [isAuthActive]);
    }
    ```
  </Tab>
</Tabs>

<Note>
  Native passkeys use the device's biometric prompt (Face ID, Touch ID, fingerprint) directly -- no browser or portal needed. For users with a password or PIN instead of a passkey, the listener falls back to opening the portal URL.
</Note>

## How It Works

`ParaMobile` sets `isNativePasskey = true` internally when initializing the React Native SDK. This causes the SDK to suppress `passkeyUrl` from auth states. Instead:

* **Signup**: `passkeyId` is provided in the auth state. Pass it to `registerPasskey()` to trigger native credential creation.
* **Login**: `hasPasskey` is `true` in the auth state. Call `loginWithPasskey()` to trigger the device's native biometric prompt.

These methods interact with the OS passkey APIs directly, so the user sees a native Face ID, Touch ID, or fingerprint prompt rather than a browser-based flow.

## Next Steps

<CardGroup cols={3}>
  <Card title="Add Password or PIN" imgUrl="/images/v3/general-passwords.png" href="/v3/react-native/guides/add-password-pin" description="Support password or PIN as an alternative to passkeys." />

  <Card title="EVM Integration" imgUrl="/images/v3/network-evm.png" href="/v3/react-native/guides/evm" description="Sign transactions on EVM-compatible chains." />

  <Card title="Session Management" imgUrl="/images/v3/general-sessions.png" href="/v3/react-native/guides/sessions" description="Manage user sessions and token refresh." />
</CardGroup>
