Skip to main content
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

Platform Configuration

Configure your app’s passkey credentials so the OS can associate your app with Para’s domain.
In order for passkeys to work, you need to set up associated domains in your Xcode project linked to the Para domain.
Associated Domains

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 .
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.
Getting ready for App Review? See for Para-specific review tips.

Install CocoaPods for native dependencies:

cd ios
bundle install
bundle exec pod install
cd ..
Remember to run pod install after adding new dependencies to your project.

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:
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]);
}
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.

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