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

# Custom Auth UI with React Hooks

> Build custom authentication UI with Para's simplified React hooks that handle the entire auth flow in a single call

export const Card = ({imgUrl, title, description, href, horizontal = false, newTab = false}) => {
  const [isHovered, setIsHovered] = useState(false);
  const handleClick = e => {
    e.preventDefault();
    if (newTab) {
      window.open(href, '_blank', 'noopener,noreferrer');
    } else {
      window.location.href = href;
    }
  };
  return <div className={`not-prose relative my-2 p-[1px] rounded-xl transition-all duration-300 ${isHovered ? 'bg-gradient-to-r from-[#FF4E00] to-[#874AE3]' : 'bg-gray-200'}`} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
      <a href={href} onClick={handleClick} className={`not-prose flex ${horizontal ? 'flex-row' : 'flex-col'} font-normal h-full bg-white overflow-hidden w-full cursor-pointer rounded-[11px] no-underline`}>
        {imgUrl && <div className={`relative overflow-hidden flex-shrink-0 ${horizontal ? 'w-[30%] rounded-l-[11px]' : 'w-full'}`} onClick={e => e.stopPropagation()}>
            <img src={imgUrl} alt={title} className="w-full h-full object-cover pointer-events-none select-none" draggable="false" />
            <div className="absolute inset-0 pointer-events-none" />
          </div>}
        <div className={`flex-grow px-6 py-5 ${horizontal ? 'w-[70%]' : 'w-full'} flex flex-col ${horizontal && imgUrl ? 'justify-center' : 'justify-start'}`}>
          {title && <h2 className="font-semibold text-base text-gray-800 m-0">{title}</h2>}
          {description && <div className={`font-normal text-gray-500 re leading-6 ${horizontal || !imgUrl ? 'mt-0' : 'mt-1'}`}>
              <p className="m-0 text-xs">{description}</p>
            </div>}
        </div>
      </a>
    </div>;
};

export const MethodDocs = ({name, description, parameters = [], returns, deprecated = false, since = null, async = false, static: isStatic = false, tag = null, defaultExpanded = false, preventCollapse = false, id = 'method'}) => {
  const [isExpanded, setIsExpanded] = useState(defaultExpanded || preventCollapse);
  const [isHovered, setIsHovered] = useState(false);
  const [isCopied, setIsCopied] = useState(false);
  const [hoveredParam, setHoveredParam] = useState(null);
  const [hoveredReturn, setHoveredReturn] = useState(false);
  const parseMethodName = fullName => {
    const match = fullName.match(/^([^(]+)(\()([^)]*)(\))$/);
    if (match) {
      return {
        name: match[1],
        openParen: match[2],
        params: match[3],
        closeParen: match[4]
      };
    }
    return {
      name: fullName,
      openParen: '',
      params: '',
      closeParen: ''
    };
  };
  const methodParts = parseMethodName(name);
  const handleCopy = e => {
    e.stopPropagation();
    navigator.clipboard.writeText(name);
    setIsCopied(true);
    setTimeout(() => setIsCopied(false), 2000);
  };
  return <div className={`not-prose rounded-2xl border border-gray-200 overflow-hidden transition-colors duration-200 mb-6 ${isHovered && !preventCollapse ? 'bg-gray-50' : 'bg-white'}`} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
      <button onClick={() => !preventCollapse && setIsExpanded(!isExpanded)} className={`w-full bg-transparent p-6 border-none text-left ${preventCollapse ? 'cursor-default' : 'cursor-pointer'}`}>
        <div className="flex items-start justify-between gap-4">
          <div className="flex-1 flex flex-col gap-2">
            <div className="flex items-center gap-3 flex-wrap">
              <div className="flex items-center gap-2">
                {async && <span className="px-1.5 py-0.5 text-[0.625rem] font-medium bg-purple-200 text-purple-800 rounded-lg">
                    async
                  </span>}
                {isStatic && <span className="px-1.5 py-0.5 text-[0.625rem] font-medium bg-violet-200 text-violet-900 rounded-lg">
                    static
                  </span>}
                {tag && <span className="px-1.5 py-0.5 text-[0.625rem] font-medium bg-teal-200 text-teal-800 rounded-lg">
                    {tag}
                  </span>}
              </div>
              
              <code className="text-lg font-mono font-semibold text-gray-900">
                <span>{methodParts.name}</span>
                <span className="text-gray-500 font-normal">{methodParts.openParen}</span>
                <span className="text-blue-600 font-normal">{methodParts.params}</span>
                <span className="text-gray-500 font-normal">{methodParts.closeParen}</span>
              </code>
              
              {deprecated && <span className="px-1.5 py-0.5 text-[0.625rem] font-medium bg-red-100 text-red-800 rounded-lg flex items-center gap-0.5">
                  ⚠ Deprecated
                </span>}
              {since && <span className="px-1.5 py-0.5 text-[0.625rem] font-medium bg-blue-100 text-blue-800 rounded-lg">
                  Since v{since}
                </span>}
            </div>
            
            <p className="text-sm text-gray-600 leading-6 m-0">
              {description}
            </p>
          </div>
          
          <div className="flex items-center gap-2 flex-shrink-0">
            <button onClick={handleCopy} className="p-2 bg-transparent border-none rounded-md cursor-pointer transition-colors duration-200 text-gray-500 hover:bg-gray-100" title="Copy method signature">
              {isCopied ? <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#16a34a" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <polyline points="20 6 9 17 4 12"></polyline>
                </svg> : <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
                  <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
                </svg>}
            </button>
            
            {!preventCollapse && <span className="text-gray-400">
                {isExpanded ? <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                    <polyline points="18 15 12 9 6 15"></polyline>
                  </svg> : <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                    <polyline points="6 9 12 15 18 9"></polyline>
                  </svg>}
              </span>}
          </div>
        </div>
      </button>

      <div className={`overflow-hidden transition-all duration-300 ease-in-out px-6 border-t border-gray-200 ${isExpanded ? 'max-h-[2000px] opacity-100 pb-6' : 'max-h-0 opacity-0 pb-0'}`}>
        {parameters.length > 0 && <div className="pt-6">
            <div className="flex items-center gap-2 mb-3">
              <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider m-0">
                Parameters
              </h3>
              <span className="text-xs text-gray-500">({parameters.length})</span>
            </div>
            <div>
              {parameters.map((param, index) => <div key={index} className={`pl-4 border-l-2 transition-colors duration-200 ${hoveredParam === index ? 'border-gray-300' : 'border-gray-200'} ${index < parameters.length - 1 ? 'mb-3' : ''}`} onMouseEnter={() => setHoveredParam(index)} onMouseLeave={() => setHoveredParam(null)}>
                  <div className="flex items-baseline gap-2 mb-1 flex-wrap">
                    <code className="font-mono text-sm font-medium text-gray-900">
                      {param.name}
                    </code>
                    <span className="text-sm text-gray-500">:</span>
                    {param.typeLink ? <a href={param.typeLink} className="no-underline">
                        <code className="font-mono text-sm text-blue-600 bg-transparent px-1 py-0.5 rounded-md cursor-pointer transition-all duration-200 hover:bg-gray-100 hover:text-blue-700">
                          {param.type}
                        </code>
                      </a> : <code className="font-mono text-sm text-blue-600 bg-transparent px-1 py-0.5 rounded-md">
                        {param.type}
                      </code>}
                    {param.required && <span className="px-1.5 py-0.5 text-[0.625rem] font-medium bg-yellow-100 text-yellow-800 rounded-lg">
                        Required
                      </span>}
                    {param.optional && <span className="px-1.5 py-0.5 text-[0.625rem] font-medium bg-gray-100 text-gray-600 rounded-lg">
                        Optional
                      </span>}
                  </div>
                  {param.description && <p className="text-sm text-gray-600 mt-1 mb-0">
                      {param.description}
                    </p>}
                  {param.defaultValue !== undefined && <p className="text-sm text-gray-500 mt-1">
                      Default: <code className="font-mono text-[0.625rem] bg-gray-100 px-1.5 py-0.5 rounded-lg">{param.defaultValue}</code>
                    </p>}
                </div>)}
            </div>
          </div>}

        {returns && <div className={`${parameters.length > 0 ? 'mt-6' : 'pt-6'}`}>
            <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wider m-0 mb-3">
              Returns
            </h3>
            <div className={`pl-4 border-l-2 transition-colors duration-200 ${hoveredReturn ? 'border-gray-300' : 'border-gray-200'}`} onMouseEnter={() => setHoveredReturn(true)} onMouseLeave={() => setHoveredReturn(false)}>
              <div className="flex items-baseline gap-2 mb-1">
                {returns.typeLink ? <a href={returns.typeLink} className="no-underline">
                    <code className="font-mono text-sm text-blue-600 bg-transparent px-1 py-0.5 rounded cursor-pointer transition-all duration-200 hover:bg-gray-100 hover:text-blue-700">
                      {returns.type}
                    </code>
                  </a> : <code className="font-mono text-sm text-blue-600 bg-transparent px-1 py-0.5 rounded">
                    {returns.type}
                  </code>}
              </div>
              {returns.description && <p className="text-sm text-gray-600 mt-1 mb-0">
                  {returns.description}
                </p>}
            </div>
          </div>}
      </div>
    </div>;
};

Para provides simplified React hooks that handle the entire authentication flow in a single call. Instead of managing multiple steps (signup/login, verification, session polling, wallet creation) separately, these hooks orchestrate everything automatically and return a unified response.

<Badge color="purple" icon="sparkles" shape="pill">Available in v2.13.0+</Badge>

<Warning>
  These hooks are **long-running** — they internally poll for session completion and wait for the user to finish interacting with the portal. Place the hook call in a **provider or higher-order component** that will not unmount during the authentication flow. If the component unmounts while the hook is running, the authentication will be interrupted.
</Warning>

<Note>
  While the hooks 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. **Passkey URLs must be opened in a popup** — WebAuthn does not work in iframes. See [Handling State Changes](#handling-state-changes) below.
</Note>

## Prerequisites

You must have a Para account set up with authentication methods enabled in your Developer Portal. Install the React SDK:

```bash theme={null}
npm install @getpara/react-sdk --save-exact
```

Ensure your app is wrapped with the `ParaProvider` as described in the [React quickstart guide](/v2/react/quickstart).

## Hooks Reference

<MethodDocs
  defaultExpanded={true}
  preventCollapse={true}
  name="useAuthenticateWithEmailOrPhone()"
  description="A mutation hook that handles the entire email or phone authentication flow in a single call — signup or login, verification, session waiting, and wallet creation."
  parameters={[
{
  name: "auth",
  type: "VerifiedAuth",
  typeLink: "/v2/references/types/verifiedauth",
  required: true,
  description: "The user's email address or phone number, in the form `{ email: '...' } | { phone: '+1...' }`."
},
{
  name: "sessionPollingCallbacks",
  type: "PollingCallbacks",
  optional: true,
  description: "Callbacks fired while polling for session status. Includes `onPoll`, `onCancel`, and `isCanceled`."
},
{
  name: "portalTheme",
  type: "Theme",
  optional: true,
  description: "The theme to apply to generated URLs, if different from your configured theme."
},
{
  name: "useShortUrls",
  type: "boolean",
  optional: true,
  description: "Whether to shorten generated URLs. This may correct any issues with generated QR codes."
}
]}
  returns={{ type: "{ authenticateWithEmailOrPhone: (params) => void, authenticateWithEmailOrPhoneAsync: (params) => Promise<AuthenticateResponse>, data: AuthenticateResponse | undefined, isPending: boolean, error: Error | null, reset: () => void }", description: "A mutation result with `authenticateWithEmailOrPhone` / `authenticateWithEmailOrPhoneAsync` functions, `data`, and standard React Query mutation fields." }}
  async={true}
/>

<Note>
  This hook must be paired with `para.onStatePhaseChange()` to handle portal URLs that appear during authentication (verification, passkey, password, PIN). See [Handling State Changes](/v2/react/guides/custom-ui-simplified#handling-state-changes) for the full state listener pattern.
</Note>

<MethodDocs
  defaultExpanded={true}
  preventCollapse={true}
  name="useAuthenticateWithOAuth()"
  description="A mutation hook that handles the entire OAuth authentication flow in a single call — OAuth redirect/popup, verification, session waiting, and wallet creation. Supports Google, Apple, Discord, X, Facebook, Telegram, and Farcaster."
  parameters={[
{
  name: "method",
  type: "TOAuthMethod",
  required: true,
  description: "The third-party OAuth service to use (e.g. `'GOOGLE'`, `'APPLE'`, `'DISCORD'`, `'X'`, `'FACEBOOK'`, `'TELEGRAM'`, `'FARCASTER'`)."
},
{
  name: "appScheme",
  type: "string",
  optional: true,
  description: "The app scheme to redirect to after OAuth is complete. Required for mobile (React Native) flows."
},
{
  name: "redirectCallbacks",
  type: "OAuthRedirectCallbacks",
  optional: true,
  description: "Callbacks for OAuth popup/redirect handling. Includes `onOAuthPopup` (receives the popup `Window`) and `onOAuthUrl` (receives the OAuth URL string). If `onOAuthUrl` is provided, `onOAuthPopup` will not be called."
},
{
  name: "sessionPollingCallbacks",
  type: "PollingCallbacks",
  optional: true,
  description: "Callbacks fired while polling for session status. Includes `onPoll`, `onCancel`, and `isCanceled`."
},
{
  name: "oAuthPollingCallbacks",
  type: "PollingCallbacks",
  optional: true,
  description: "Callbacks fired while polling for OAuth completion status. Includes `onPoll`, `onCancel`, and `isCanceled`."
},
{
  name: "portalTheme",
  type: "Theme",
  optional: true,
  description: "The theme to apply to generated URLs, if different from your configured theme."
},
{
  name: "useShortUrls",
  type: "boolean",
  optional: true,
  description: "Whether to shorten generated URLs. This may correct any issues with generated QR codes."
}
]}
  returns={{ type: "{ authenticateWithOAuth: (params) => void, authenticateWithOAuthAsync: (params) => Promise<AuthenticateResponse>, data: AuthenticateResponse | undefined, isPending: boolean, error: Error | null, reset: () => void }", description: "A mutation result with `authenticateWithOAuth` / `authenticateWithOAuthAsync` functions, `data`, and standard React Query mutation fields." }}
  async={true}
/>

<Note>
  This hook 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 [Handling State Changes](/v2/react/guides/custom-ui-simplified#handling-state-changes) for the full state listener pattern.
</Note>

## Handling State Changes

During authentication, Para's state machine progresses through phases that require user interaction — either opening portal URLs (for basic login users and biometric flows) or showing a code input (for non-basic-login new signups). Since you're building a custom UI without the `ParaModal`, you need to subscribe to state changes and handle them yourself.

<Warning>
  **Passkey URLs must be opened in a popup window**, not an iframe. WebAuthn/passkey operations require a top-level browsing context and will fail silently in iframes due to browser security restrictions. Password and PIN URLs can be opened in either a popup or an iframe. Verification URLs can also use either approach.
</Warning>

Use `para.onStatePhaseChange()` to receive a `StateSnapshot`. The snapshot contains `authPhase` (what stage the flow is in) and `authStateInfo` (URLs and flags for the current stage):

```tsx theme={null}
import { useEffect, useRef, useState } from "react";
import { useClient } from "@getpara/react-sdk";
import type { StateSnapshot, AuthPhase } from "@getpara/web-sdk";

function useParaAuthStateListener() {
  const para = useClient();
  const popupRef = useRef<Window | null>(null);
  const lastUrlRef = useRef<string | null>(null);
  const [authPhase, setAuthPhase] = useState<AuthPhase>("unauthenticated");
  const [authStateInfo, setAuthStateInfo] = useState<StateSnapshot["authStateInfo"] | null>(null);

  useEffect(() => {
    const unsubscribe = para.onStatePhaseChange((snapshot) => {
      const { authStateInfo, authPhase } = snapshot;
      setAuthPhase(authPhase);
      setAuthStateInfo(authStateInfo);

      // Basic login verification URL — open in popup or iframe
      if (authStateInfo.verificationUrl && authStateInfo.verificationUrl !== lastUrlRef.current) {
        lastUrlRef.current = authStateInfo.verificationUrl;
        popupRef.current = window.open(
          authStateInfo.verificationUrl,
          "ParaVerification",
          "popup,width=400,height=500"
        );
        return;
      }

      // Biometric / security URLs
      const { passkeyUrl, passwordUrl, pinUrl } = authStateInfo;

      // Passkey URLs MUST use a popup — WebAuthn does not work inside iframes
      if (passkeyUrl && passkeyUrl !== lastUrlRef.current) {
        lastUrlRef.current = passkeyUrl;
        popupRef.current = window.open(passkeyUrl, "ParaPasskey", "popup,width=400,height=500");
      } else if (passwordUrl && passwordUrl !== lastUrlRef.current) {
        lastUrlRef.current = passwordUrl;
        popupRef.current = window.open(passwordUrl, "ParaPassword", "popup,width=400,height=500");
      } else if (pinUrl && pinUrl !== lastUrlRef.current) {
        lastUrlRef.current = pinUrl;
        popupRef.current = window.open(pinUrl, "ParaPIN", "popup,width=400,height=500");
      }
    });

    return () => {
      unsubscribe();
      lastUrlRef.current = null;
    };
  }, [para]);

  return { authPhase, authStateInfo, popupRef };
}
```

<Note>
  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 `useVerifyNewAccount()` to submit the code. If the user needs a new code, use `useResendVerificationCode()`. The simplified hook is waiting for this step to complete before it proceeds. See the [full example below](#email--phone-authentication).
</Note>

### `authStateInfo` Fields

| Field                | Type                              | Description                                                                                                                                                                                      |
| -------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `verificationUrl`    | `string \| null`                  | Portal URL for basic login users to complete auth (OTP, passkey, etc.) in the hosted portal. Only set during `awaiting_session_start` for basic login flows. Can be opened in a popup or iframe. |
| `passkeyUrl`         | `string \| null`                  | Portal URL for passkey login or creation. **Must be opened in a popup** — WebAuthn does not work in iframes.                                                                                     |
| `passwordUrl`        | `string \| null`                  | Portal URL for password login or creation. Can be opened in a popup or iframe.                                                                                                                   |
| `pinUrl`             | `string \| null`                  | Portal URL for PIN login or creation. Can be opened in a popup or iframe.                                                                                                                        |
| `isPasskeySupported` | `boolean`                         | Whether the user's device supports passkeys/WebAuthn.                                                                                                                                            |
| `isNewUser`          | `boolean`                         | Whether this is a new signup flow.                                                                                                                                                               |
| `passkeyHints`       | `BiometricLocationHint[] \| null` | Hints for known device detection.                                                                                                                                                                |

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

| Phase           | Description                                                                             |
| --------------- | --------------------------------------------------------------------------------------- |
| `setting_up`    | Para is initializing. Show a loading indicator.                                         |
| `auth_flow`     | The user is in the authentication flow. Refer to `authPhase` for details.               |
| `wallet_flow`   | Authentication succeeded; wallets are being set up. Refer to `walletPhase` for details. |
| `authenticated` | The user is fully logged in with wallets ready.                                         |
| `guest_mode`    | The user is in guest mode (no full authentication).                                     |
| `logging_out`   | A logout is in progress.                                                                |
| `error`         | An unrecoverable error occurred. Check `snapshot.error`.                                |

### `authPhase` — Authentication Flow Detail

| Phase                           | Description                                                                                                                                                                                                             | What to show                                                                                |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `unauthenticated`               | No auth in progress.                                                                                                                                                                                                    | Login / signup form.                                                                        |
| `authenticating_email_phone`    | Email/phone auth initiated, routing in progress.                                                                                                                                                                        | Loading indicator.                                                                          |
| `authenticating_oauth`          | Standard OAuth flow in progress.                                                                                                                                                                                        | Loading indicator or "Waiting for provider...".                                             |
| `authenticating_telegram`       | Telegram auth in progress.                                                                                                                                                                                              | Telegram popup/iframe.                                                                      |
| `authenticating_farcaster`      | Farcaster auth in progress.                                                                                                                                                                                             | Farcaster QR code.                                                                          |
| `processing_authentication`     | Server is processing the auth request.                                                                                                                                                                                  | Loading indicator.                                                                          |
| `verifying_new_account`         | Verification code is being validated.                                                                                                                                                                                   | Loading indicator.                                                                          |
| `awaiting_account_verification` | New 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_start`        | Portal 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_session`           | Session polling is active — waiting for the user to complete in the portal.                                                                                                                                             | "Completing setup..." loading state.                                                        |
| `authenticated`                 | Auth flow is complete. (Core may still be in `wallet_flow`.)                                                                                                                                                            | Transition to authenticated UI, or show wallet loading indicator.                           |
| `guest_mode`                    | The user is in guest mode (no full authentication).                                                                                                                                                                     | Guest UI.                                                                                   |
| `error`                         | Auth flow failed. Check `snapshot.error`.                                                                                                                                                                               | Error message with retry option.                                                            |

<Info>
  **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`).
</Info>

### `walletPhase` — Wallet Setup Detail

| Phase                    | Description                                                                         |
| ------------------------ | ----------------------------------------------------------------------------------- |
| `checking_wallet_state`  | Checking if wallets need to be created.                                             |
| `needs_wallets`          | Wallets need to be created for this user.                                           |
| `claiming_wallets`       | Claiming pregenerated wallets that were created for this user before signup.        |
| `creating_wallets`       | Wallets are being created.                                                          |
| `waiting_for_wallets`    | Waiting for wallet creation or claiming to complete.                                |
| `setting_up_after_login` | Performing post-login wallet setup (e.g. refreshing session, syncing wallet state). |
| `wallets_ready`          | Wallets are created and ready to use.                                               |
| `no_wallets_needed`      | User already has wallets; nothing to create.                                        |
| `error`                  | Wallet creation failed. Check `snapshot.error`.                                     |

## Email / Phone Authentication

Use `useAuthenticateWithEmailOrPhone` to authenticate a user by email or phone number. The hook handles the complete flow: it determines whether the user is new or returning, manages session polling, waits for session establishment, and creates wallets for new signups.

You need to handle two things alongside the hook:

1. **State listener** — subscribe to `onStatePhaseChange` to open portal URLs (verification, passkey, password, PIN) when they become available.
2. **OTP code input** — when `authPhase` is `'awaiting_account_verification'`, show a code input and call `useVerifyNewAccount()` to submit the code. The hook is waiting for this before it proceeds.

```tsx theme={null}
import { useState } from "react";
import {
  useAuthenticateWithEmailOrPhone,
  useVerifyNewAccount,
  useResendVerificationCode,
} from "@getpara/react-sdk";

function EmailAuth() {
  const {
    authenticateWithEmailOrPhoneAsync,
    isPending,
    error,
  } = useAuthenticateWithEmailOrPhone();

  const { verifyNewAccountAsync } = useVerifyNewAccount();
  const { resendVerificationCodeAsync } = useResendVerificationCode();

  // Use the state listener hook from the section above
  const { authPhase, popupRef } = useParaAuthStateListener();

  const [email, setEmail] = useState("");
  const [verificationCode, setVerificationCode] = useState("");

  const handleAuth = async () => {
    try {
      const result = await authenticateWithEmailOrPhoneAsync({
        auth: { email },
        sessionPollingCallbacks: {
          onPoll: () => {
            if (popupRef.current?.closed) {
              popupRef.current = null;
            }
          },
        },
      });

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

      // User is now fully authenticated
      console.log("Auth info:", result.authInfo);
    } catch (err) {
      console.error("Authentication failed:", err);
    }
  };

  // Show OTP input when awaiting verification for non-basic-login new signups
  if (authPhase === "awaiting_account_verification") {
    return (
      <div>
        <p>Enter the verification code sent to {email}</p>
        <input
          value={verificationCode}
          onChange={(e) => setVerificationCode(e.target.value)}
          placeholder="6-digit code"
        />
        <button onClick={() => verifyNewAccountAsync({ verificationCode })}>
          Verify
        </button>
        <button onClick={() => resendVerificationCodeAsync({ type: "SIGNUP" })}>
          Resend Code
        </button>
      </div>
    );
  }

  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter your email"
      />
      <button onClick={handleAuth} disabled={isPending}>
        {isPending ? "Authenticating..." : "Continue"}
      </button>
      {error && <p>{error.message}</p>}
    </div>
  );
}
```

For phone number authentication, pass `{ phone: '+1234567890' }` instead of `{ email }`:

```tsx theme={null}
authenticateWithEmailOrPhoneAsync({
  auth: { phone: `+${countryCode}${phoneNumber}` as `+${number}` },
});
```

## OAuth Authentication

Use `useAuthenticateWithOAuth` to authenticate a user via a third-party OAuth provider. The hook manages the OAuth redirect/popup, polls for completion, waits for session establishment, and creates wallets for new signups.

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

For standard OAuth providers, the `onOAuthPopup` callback gives you the initial popup window. The state listener handles biometric URLs that appear after the OAuth step completes (e.g. when a returning user needs to authenticate with their passkey).

```tsx theme={null}
import { useAuthenticateWithOAuth } from "@getpara/react-sdk";

function OAuthLogin() {
  const {
    authenticateWithOAuthAsync,
    isPending,
    error,
  } = useAuthenticateWithOAuth();

  // Use the state listener hook from the section above
  const { popupRef } = useParaAuthStateListener();

  const handleOAuth = async (method: "GOOGLE" | "APPLE" | "DISCORD" | "X" | "FACEBOOK") => {
    try {
      const result = await authenticateWithOAuthAsync({
        method,
        redirectCallbacks: {
          onOAuthPopup: (popup) => {
            popupRef.current = popup;
          },
        },
        oAuthPollingCallbacks: {
          onPoll: () => {
            if (popupRef.current?.closed) {
              popupRef.current = null;
            }
          },
        },
      });

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

      console.log("Auth info:", result.authInfo);
    } catch (err) {
      console.error("OAuth failed:", err);
    }
  };

  return (
    <div>
      <button onClick={() => handleOAuth("GOOGLE")} disabled={isPending}>
        Continue with Google
      </button>
      <button onClick={() => handleOAuth("APPLE")} disabled={isPending}>
        Continue with Apple
      </button>
      <button onClick={() => handleOAuth("DISCORD")} disabled={isPending}>
        Continue with Discord
      </button>
      {error && <p>{error.message}</p>}
    </div>
  );
}
```

### Telegram

Telegram authentication works the same way — the hook manages the Telegram bot interaction automatically:

```tsx theme={null}
const result = await authenticateWithOAuthAsync({
  method: "TELEGRAM",
  redirectCallbacks: {
    onOAuthPopup: (popup) => {
      popupRef.current = popup;
    },
  },
});
```

### Farcaster

Farcaster uses a QR code flow. Use the `redirectCallbacks.onOAuthUrl` callback to receive the Farcaster Connect URI and display it as a QR code:

```tsx theme={null}
const [farcasterUri, setFarcasterUri] = useState<string | null>(null);

const handleFarcaster = async () => {
  try {
    const result = await authenticateWithOAuthAsync({
      method: "FARCASTER",
      redirectCallbacks: {
        onOAuthUrl: (url) => {
          setFarcasterUri(url);
        },
      },
      oAuthPollingCallbacks: {
        isCanceled: () => !farcasterUri,
      },
    });

    setFarcasterUri(null);
    console.log("Auth info:", result.authInfo);
  } catch (err) {
    console.error("Farcaster auth failed:", err);
  }
};
```

## Cancelling Authentication

Both hooks accept polling callbacks with an `isCanceled` function. Return `true` from `isCanceled` to stop the polling loop — for example, when the user closes a popup or navigates away. The cancellation is clean: no error is thrown, and the optional `onCancel` callback is fired.

```tsx theme={null}
const result = await authenticateWithEmailOrPhoneAsync({
  auth: { email },
  sessionPollingCallbacks: {
    isCanceled: () => {
      // Cancel if the user closed the popup
      return popupRef.current === null || popupRef.current.closed;
    },
    onCancel: () => {
      console.log("User canceled authentication");
    },
  },
});
```

For OAuth, you can cancel both the OAuth polling and session polling independently:

```tsx theme={null}
const result = await authenticateWithOAuthAsync({
  method: "GOOGLE",
  oAuthPollingCallbacks: {
    isCanceled: () => userClickedCancel,
    onCancel: () => console.log("OAuth polling canceled"),
  },
  sessionPollingCallbacks: {
    isCanceled: () => userClickedCancel,
    onCancel: () => console.log("Session polling canceled"),
  },
});
```

Calling `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:

```tsx theme={null}
import { useLogout } from "@getpara/react-sdk";

const { logoutAsync } = useLogout();

const handleCancel = async () => {
  await logoutAsync();
  // All polling stops, state phases reset to unauthenticated
};
```

## Handling Results

Both hooks return an `AuthenticateResponse` object with the same shape:

```typescript theme={null}
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
};
```

| Field               | Description                                                                                                                                                                                                    |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `authInfo`          | Contains the user's primary authentication information such as `email`, `phone`, `userId`, and any auth extras.                                                                                                |
| `hasCreatedWallets` | `true` if the user is new and wallets were auto-created during signup. `false` for returning users.                                                                                                            |
| `recoverySecret`    | Present 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

<CardGroup cols={2}>
  <Card title="Web SDK (Custom UI)" description="Build custom auth UI using the Web SDK directly — works with vanilla JS, Vue, Svelte, or any framework" href="/v2/react/guides/custom-ui-web-sdk" />

  <Card title="Session Management" description="Learn about managing user sessions and JWT authentication" href="/v2/react/guides/sessions" />

  <Card title="EVM Integration" description="Send transactions and sign messages with EVM-compatible libraries" href="/v2/react/guides/external-wallets/evm" />
</CardGroup>
