Skip to main content
Para’s client SDK provides simplified methods that handle the entire authentication flow in a single call. This guide shows how to build a fully custom authentication UI in React Native and Expo apps using authenticateWithEmailOrPhone() and authenticateWithOAuth(). Available in v2.13.0+
These methods are long-running — they internally poll for session completion and wait for the user to finish interacting with the portal. Call them from a provider or higher-order component that will not unmount during the authentication flow. If the component unmounts while the method is running, the authentication will be interrupted.
While these methods manage the flow end-to-end, you are responsible for opening the portal URLs that Para generates during authentication (for verification, passkey creation, password entry, etc.). Use para.onStatePhaseChange() to listen for these URLs and open them using the in-app browser. See Handling Portal URLs below.

Prerequisites

Before implementing custom authentication, ensure you have completed the basic Para setup for your React Native or Expo application.

Method Reference

This method must be paired with para.onStatePhaseChange() to handle portal URLs that appear during authentication (verification, passkey, password, PIN). See the guide for your platform for the full state listener pattern.
This method must be paired with para.onStatePhaseChange() to handle portal URLs that appear after OAuth completes (e.g. passkey or password setup for returning users). See the guide for your platform for the full state listener pattern.

Handling Portal URLs

During authentication, Para’s state machine emits portal URLs that the user must interact with (e.g. entering a verification code, creating a passkey, or entering a password). Since you’re building a custom UI, you need to subscribe to state changes and open these URLs using the in-app browser. Use para.onStatePhaseChange() to receive a StateSnapshot containing authStateInfo — a flat object with all the URLs and flags you need:
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);

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

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

      // 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;
      }

      // Biometric / security URLs (passkey, password, PIN)
      const url =
        authStateInfo.passkeyKnownDeviceUrl ||
        authStateInfo.passkeyUrl ||
        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;
    };
  }, [isAuthActive]);
}

authStateInfo Fields

FieldTypeDescription
userIdstring | nullThe user’s ID, if available at the current stage.
verificationUrlstring | nullPortal URL for basic login users to complete auth in the hosted portal. Only set during awaiting_session_start for basic login flows. Open in the in-app browser.
passkeyUrlstring | nullPortal URL for passkey login or creation. Open in the in-app browser.
passkeyKnownDeviceUrlstring | nullPortal URL for authorizing from a known device with passkey. Open in the in-app browser when present instead of passkeyUrl.
passwordUrlstring | nullPortal URL for password login or creation. Open in the in-app browser.
pinUrlstring | nullPortal URL for PIN login or creation. Open in the in-app browser.
isPasskeySupportedbooleanWhether the user’s device supports passkeys/WebAuthn.
hasPasskeybooleanWhether the user has a passkey configured.
hasPasswordbooleanWhether the user has a password configured.
hasPinbooleanWhether the user has a PIN configured.
isNewUserbooleanWhether this is a new signup flow.
passkeyHintsBiometricLocationHint[] | nullHints for known device detection.
When authPhase is 'awaiting_account_verification', the user is a non-basic-login new signup who has been sent an OTP code via email or SMS. There is no URL to open — you must show a code input field and call para.verifyNewAccount({ verificationCode }). If the user needs a new code, call para.resendVerificationCode({ type: 'SIGNUP' }). The simplified method is waiting for this step to complete before it proceeds. See the full example below.

State Phase Reference

The StateSnapshot returned by para.onStatePhaseChange() contains three phase fields that tell you exactly where in the flow the user is. Use these to drive your UI.

corePhase — Top-Level Lifecycle

PhaseDescription
setting_upPara is initializing. Show a loading indicator.
auth_flowThe user is in the authentication flow. Refer to authPhase for details.
wallet_flowAuthentication succeeded; wallets are being set up. Refer to walletPhase for details.
authenticatedThe user is fully logged in with wallets ready.
guest_modeThe user is in guest mode (no full authentication).
logging_outA logout is in progress.
errorAn unrecoverable error occurred. Check snapshot.error.

authPhase — Authentication Flow Detail

PhaseDescriptionWhat to show
unauthenticatedNo auth in progress.Login / signup form.
authenticating_email_phoneEmail/phone auth initiated, routing in progress.Loading indicator.
authenticating_oauthStandard OAuth flow in progress.Loading indicator or “Waiting for provider…”.
authenticating_telegramTelegram auth in progress.Telegram popup/iframe.
authenticating_farcasterFarcaster auth in progress.Farcaster QR code.
processing_authenticationServer is processing the auth request.Loading indicator.
verifying_new_accountVerification code is being validated.Loading indicator.
awaiting_account_verificationNew user (non-basic-login) needs to enter an OTP verification code sent to their email or phone. No URL to open — authStateInfo will not have a verificationUrl.Show a code input field. Call para.verifyNewAccount({ verificationCode }) when submitted.
awaiting_session_startPortal URLs are ready. For basic login users, authStateInfo.verificationUrl is set (user completes auth in the portal). For passkey/password/PIN users, passkeyUrl, passwordUrl, and/or pinUrl are set.Open the appropriate URL. Passkey URLs must use a popup on web.
waiting_for_sessionSession polling is active — waiting for the user to complete in the portal.”Completing setup…” loading state.
authenticatedAuth flow is complete. (Core may still be in wallet_flow.)Transition to authenticated UI, or show wallet loading indicator.
guest_modeThe user is in guest mode (no full authentication).Guest UI.
errorAuth flow failed. Check snapshot.error.Error message with retry option.
Basic login vs passkey/password/PIN: Basic login users complete their entire authentication through a portal URL — the verificationUrl handles OTP entry, passkey creation, etc. in a single hosted flow. Passkey, password, and PIN users go through a two-step process where OTP verification happens in your app (via awaiting_account_verification) and biometric setup happens in the portal (via awaiting_session_start).

walletPhase — Wallet Setup Detail

PhaseDescription
checking_wallet_stateChecking if wallets need to be created.
needs_walletsWallets need to be created for this user.
claiming_walletsClaiming pregenerated wallets that were created for this user before signup.
creating_walletsWallets are being created.
waiting_for_walletsWaiting for wallet creation or claiming to complete.
setting_up_after_loginPerforming post-login wallet setup (e.g. refreshing session, syncing wallet state).
wallets_readyWallets are created and ready to use.
no_wallets_neededUser already has wallets; nothing to create.
errorWallet creation failed. Check snapshot.error.

Email / Phone Authentication

Use para.authenticateWithEmailOrPhone() to authenticate a user by email or phone. The method handles the complete flow: determining whether the user is new or returning, session polling, and wallet creation. Combine it with the state listener above to open portal URLs, and handle OTP input when authPhase is 'awaiting_account_verification'.
import { useState } from "react";
import { para } from "../your-para-client";

function EmailAuthScreen() {
  const [isAuthActive, setIsAuthActive] = useState(false);

  // Use the state listener hook from above
  useParaAuthStateListener(isAuthActive);

  const handleEmailAuth = async (email: string) => {
    setIsAuthActive(true);

    try {
      const result = await para.authenticateWithEmailOrPhone({
        auth: { email },
      });

      if (result.hasCreatedWallets && result.recoverySecret) {
        // Non-basic-login new user — display or securely store the recovery secret
        console.log("Recovery secret:", result.recoverySecret);
      }

      // User is now fully authenticated
      console.log("Auth info:", result.authInfo);
      // Navigate to your authenticated screen
    } catch (error) {
      console.error("Authentication failed:", error);
    } finally {
      setIsAuthActive(false);
    }
  };

  // ... render your email input UI
}
For phone number authentication, pass { phone: '+1234567890' } instead of { email }:
const result = await para.authenticateWithEmailOrPhone({
  auth: { phone: `+${countryCode}${phoneNumber}` as `+${number}` },
});

OAuth Authentication

Use para.authenticateWithOAuth() to authenticate a user via a third-party OAuth provider. The method manages the OAuth redirect, polls for completion, waits for session establishment, and creates wallets for new signups.

Standard OAuth (Google, Apple, Discord, X, Facebook)

For standard OAuth providers, use the onOAuthUrl callback to open the OAuth URL in an in-app browser. The state listener from above handles any biometric URLs that appear after OAuth completes.

Installation

Install the In-App Browser package to handle OAuth redirects:
npm install react-native-inappbrowser-reborn
# or
yarn add react-native-inappbrowser-reborn
For iOS, add the following to your Info.plist to define your URL scheme:
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>your-app-scheme</string>
    </array>
  </dict>
</array>
For Android, add your URL scheme to AndroidManifest.xml:
<activity
    android:name=".MainActivity"
    android:launchMode="singleTask">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="your-app-scheme" />
    </intent-filter>
</activity>

Implementation

import { para } from "../your-para-client";
import { OAuthMethod } from "@getpara/react-native-wallet";
import { InAppBrowser } from "react-native-inappbrowser-reborn";

const APP_SCHEME = "your-app-scheme";

async function handleOAuthLogin(provider: OAuthMethod) {
  try {
    const result = await para.authenticateWithOAuth({
      method: provider,
      appScheme: APP_SCHEME,
      redirectCallbacks: {
        onOAuthUrl: async (url) => {
          await InAppBrowser.openAuth(url, APP_SCHEME, {
            ephemeralWebSession: false,
            showTitle: false,
            enableUrlBarHiding: true,
          });
        },
      },
    });

    if (result.hasCreatedWallets && result.recoverySecret) {
      console.log("Recovery secret:", result.recoverySecret);
    }

    console.log("Auth info:", result.authInfo);
    // Navigate to your authenticated screen
  } catch (error) {
    console.error("OAuth failed:", error);
  }
}
Both react-native-inappbrowser-reborn and expo-web-browser use secure browser implementations that leverage the device’s native browser engine rather than a WebView. This provides stronger security protections and support for modern authentication methods.

Telegram & Farcaster

Telegram and Farcaster authentication require a different approach than standard OAuth. These providers authenticate through Para’s hosted portal, which needs to send events back to the SDK when authentication completes. On mobile, this requires opening the portal in a WebView (not an in-app browser) so the portal can communicate via window.ReactNativeWebView.postMessage(). You must forward messages from the WebView to the SDK using para.handleWebViewMessage().
Telegram and Farcaster cannot use InAppBrowser or expo-web-browser because the portal needs a live message channel to send authentication events (TELEGRAM_SUCCESS, FARCASTER_SUCCESS) back to the SDK. Only a WebView provides this channel via onMessage.
import { useState } from "react";
import { Modal } from "react-native";
import { WebView, type WebViewMessageEvent } from "react-native-webview";
import { para } from "../your-para-client";
import type { OAuthMethod } from "@getpara/react-native-wallet";

const APP_SCHEME = "your-app-scheme";

function TelegramOrFarcasterAuth() {
  const [portalUrl, setPortalUrl] = useState<string | null>(null);

  const handleAuth = async (method: OAuthMethod) => {
    try {
      const result = await para.authenticateWithOAuth({
        method,
        appScheme: APP_SCHEME,
        redirectCallbacks: {
          onOAuthUrl: (url) => {
            // Show the portal URL in a WebView instead of an in-app browser
            setPortalUrl(url);
          },
        },
      });

      setPortalUrl(null);

      if (result.hasCreatedWallets && result.recoverySecret) {
        console.log("Recovery secret:", result.recoverySecret);
      }

      console.log("Auth info:", result.authInfo);
      // Navigate to your authenticated screen
    } catch (error) {
      setPortalUrl(null);
      console.error("Auth failed:", error);
    }
  };

  const handleWebViewMessage = (event: WebViewMessageEvent) => {
    try {
      const data = JSON.parse(event.nativeEvent.data);
      // Forward portal events (TELEGRAM_SUCCESS, FARCASTER_SUCCESS, etc.) to the SDK
      para.handleWebViewMessage(data);
    } catch {
      // Ignore non-JSON messages
    }
  };

  return (
    <>
      {/* Your auth buttons */}
      <Button title="Continue with Telegram" onPress={() => handleAuth("TELEGRAM")} />
      <Button title="Continue with Farcaster" onPress={() => handleAuth("FARCASTER")} />

      {/* WebView modal for portal interaction */}
      <Modal visible={!!portalUrl} animationType="slide">
        <WebView
          source={{ uri: portalUrl! }}
          onMessage={handleWebViewMessage}
          javaScriptEnabled
        />
      </Modal>
    </>
  );
}
Install react-native-webview if you haven’t already:
npm install react-native-webview
# or for Expo:
npx expo install react-native-webview

Cancelling Authentication

Both methods accept polling callbacks with an isCanceled function. Return true from isCanceled to stop the polling loop — for example, when the user dismisses the in-app browser or navigates away. The cancellation is clean: no error is thrown, and the optional onCancel callback is fired.
const result = await para.authenticateWithEmailOrPhone({
  auth: { email },
  sessionPollingCallbacks: {
    isCanceled: () => userDismissedBrowser,
    onCancel: () => {
      console.log("User canceled authentication");
    },
  },
});
For OAuth, you can cancel both the OAuth polling and session polling independently:
const result = await para.authenticateWithOAuth({
  method: "GOOGLE",
  appScheme: APP_SCHEME,
  oAuthPollingCallbacks: {
    isCanceled: () => userClickedCancel,
    onCancel: () => console.log("OAuth polling canceled"),
  },
  sessionPollingCallbacks: {
    isCanceled: () => userClickedCancel,
    onCancel: () => console.log("Session polling canceled"),
  },
});
Calling para.logout() also cancels all active polling and resets the state phases back to unauthenticated. This is useful for implementing a “Cancel” button that fully resets the auth flow:
const handleCancel = async () => {
  await para.logout();
  // All polling stops, state phases reset to unauthenticated
};

Handling Results

Both methods return an AuthenticateResponse object with the same shape:
type AuthenticateResponse = {
  authInfo: CoreAuthInfo;      // The user's authentication info (email, userId, etc.)
  hasCreatedWallets: boolean;  // Whether new wallets were created during this flow
  recoverySecret?: string;     // Recovery secret for non-basic-login newly created wallets
};
FieldDescription
authInfoContains the user’s primary authentication information such as email, phone, userId, and any auth extras.
hasCreatedWalletstrue if the user is new and wallets were auto-created during signup. false for returning users.
recoverySecretPresent only for non-basic-login when new wallets are created. Basic login users do not receive a recovery secret. This should be displayed to the user or stored securely — it cannot be retrieved again.

Next Steps