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

> Build custom authentication UI using the Para Web SDK directly — works with vanilla JS, Vue, Svelte, or any framework

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

This guide covers building custom authentication UI using the `@getpara/web-sdk` client directly — no React required. This approach works with any JavaScript framework (Vue, Svelte, Angular, etc.) or plain vanilla JS.

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

<Warning>
  The simplified methods (`authenticateWithEmailOrPhone`, `authenticateWithOAuth`) are **long-running** — they internally poll for session completion and wait for the user to finish interacting with the portal. Ensure the calling context will not be destroyed while the method is running (e.g. avoid calling from a component that may unmount mid-flow).
</Warning>

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

## Prerequisites

Install the Web SDK:

```bash theme={null}
npm install @getpara/web-sdk
```

## Client Setup

Create a Para client instance:

```typescript theme={null}
import { ParaWeb } from "@getpara/web-sdk";

const PARA_API_KEY = "YOUR_API_KEY";

export const para = new ParaWeb(PARA_API_KEY);
```

## Method Reference

<MethodDocs
  defaultExpanded={true}
  preventCollapse={true}
  name="authenticateWithEmailOrPhone()"
  description="Handles the entire email or phone authentication flow in a single call — signup or login, verification, session waiting, and wallet creation."
  async={true}
  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: "AuthenticateResponse",
description: "An object containing `authInfo` (user's authentication info), `hasCreatedWallets` (whether wallets were auto-created), and optionally `recoverySecret` (for new signups)."
}}
/>

<Note>
  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.
</Note>

<MethodDocs
  defaultExpanded={true}
  preventCollapse={true}
  name="authenticateWithOAuth()"
  description="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."
  async={true}
  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: "AuthenticateResponse",
description: "An object containing `authInfo` (user's authentication info), `hasCreatedWallets` (whether wallets were auto-created), and optionally `recoverySecret` (for new signups)."
}}
/>

<Note>
  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.
</Note>

<MethodDocs
  defaultExpanded={true}
  preventCollapse={true}
  name="verifyNewAccount()"
  description="Verifies a new account using the provided verification code."
  async={true}
  parameters={[
{
  name: "verificationCode",
  type: "string",
  required: true,
  description: "The verification code entered by the user."
},
{
  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: "AuthStateSignup",
typeLink: "/v2/references/types/authstatesignup",
description: "The signup authentication state."
}}
/>

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

```typescript theme={null}
import { para } from "./your-para-client";
import type { StateSnapshot, AuthPhase } from "@getpara/web-sdk";

let lastUrl: string | null = null;
let popup: Window | null = null;

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

  // Basic login verification URL — open in popup or iframe
  if (authStateInfo.verificationUrl && authStateInfo.verificationUrl !== lastUrl) {
    lastUrl = authStateInfo.verificationUrl;
    popup = 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 !== lastUrl) {
    lastUrl = passkeyUrl;
    popup = window.open(passkeyUrl, "ParaPasskey", "popup,width=400,height=500");
  } else if (passwordUrl && passwordUrl !== lastUrl) {
    lastUrl = passwordUrl;
    popup = window.open(passwordUrl, "ParaPassword", "popup,width=400,height=500");
  } else if (pinUrl && pinUrl !== lastUrl) {
    lastUrl = pinUrl;
    popup = window.open(pinUrl, "ParaPIN", "popup,width=400,height=500");
  }
});

// Call unsubscribe() when your auth UI is torn down
```

<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 `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](#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.                                                                                                                                                               |

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

```typescript theme={null}
import { para } from "./your-para-client";
import type { AuthPhase } from "@getpara/web-sdk";

// Track auth phase for UI updates
let currentAuthPhase: AuthPhase = "unauthenticated";

const unsubscribe = para.onStatePhaseChange((snapshot) => {
  currentAuthPhase = snapshot.authPhase;
  // Update your UI based on currentAuthPhase
  renderAuthUI();
});

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

    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 (error) {
    console.error("Authentication failed:", error);
  } finally {
    unsubscribe();
  }
}

// When authPhase is "awaiting_account_verification", show a code input
// and call this with the user's code:
async function handleVerifyCode(verificationCode: string) {
  await para.verifyNewAccount({ verificationCode });
}

// To resend the verification code:
async function handleResendCode() {
  await para.resendVerificationCode({ type: "SIGNUP" });
}
```

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

```typescript theme={null}
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/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 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).

```typescript theme={null}
import { para } from "./your-para-client";

let popup: Window | null = null;

async function handleOAuthLogin(method: "GOOGLE" | "APPLE" | "DISCORD" | "X" | "FACEBOOK") {
  try {
    const result = await para.authenticateWithOAuth({
      method,
      redirectCallbacks: {
        onOAuthPopup: (oauthPopup) => {
          popup = oauthPopup;
        },
      },
      oAuthPollingCallbacks: {
        onPoll: () => {
          if (popup?.closed) {
            popup = null;
          }
        },
      },
    });

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

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

### Telegram

Telegram authentication works the same way — pass `"TELEGRAM"` as the method:

```typescript theme={null}
const result = await para.authenticateWithOAuth({
  method: "TELEGRAM",
  redirectCallbacks: {
    onOAuthPopup: (popup) => {
      // The popup contains the Telegram bot interaction
    },
  },
});
```

### Farcaster

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

```typescript theme={null}
let farcasterUri: string | null = null;

async function handleFarcaster() {
  try {
    const result = await para.authenticateWithOAuth({
      method: "FARCASTER",
      redirectCallbacks: {
        onOAuthUrl: (url) => {
          farcasterUri = url;
          // Render this URL as a QR code for the user to scan
        },
      },
      oAuthPollingCallbacks: {
        isCanceled: () => !farcasterUri,
      },
    });

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

## 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 closes a popup or navigates away. The cancellation is clean: no error is thrown, and the optional `onCancel` callback is fired.

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

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

```typescript theme={null}
const result = await para.authenticateWithOAuth({
  method: "GOOGLE",
  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:

```typescript theme={null}
async function handleCancel() {
  await para.logout();
  // All polling stops, state phases reset to unauthenticated
}
```

## Handling Results

Both methods 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. |

## Framework Examples

### Vue 3 (Composition API)

```vue theme={null}
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { para } from "./your-para-client";
import type { AuthPhase, StateSnapshot } from "@getpara/web-sdk";

const email = ref("");
const verificationCode = ref("");
const authPhase = ref<AuthPhase>("unauthenticated");
const isPending = ref(false);

let unsubscribe: (() => void) | null = null;
let popup: Window | null = null;
let lastUrl: string | null = null;

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

    if (authStateInfo.verificationUrl && authStateInfo.verificationUrl !== lastUrl) {
      lastUrl = authStateInfo.verificationUrl;
      popup = window.open(authStateInfo.verificationUrl, "ParaVerification", "popup,width=400,height=500");
      return;
    }

    const url = authStateInfo.passkeyUrl || authStateInfo.passwordUrl || authStateInfo.pinUrl;
    if (url && url !== lastUrl) {
      lastUrl = url;
      popup = window.open(url, "ParaAuth", "popup,width=400,height=500");
    }
  });
});

onUnmounted(() => {
  unsubscribe?.();
  lastUrl = null;
});

async function handleAuth() {
  isPending.value = true;
  try {
    const result = await para.authenticateWithEmailOrPhone({
      auth: { email: email.value },
    });
    console.log("Authenticated:", result.authInfo);
  } catch (err) {
    console.error("Auth failed:", err);
  } finally {
    isPending.value = false;
  }
}

async function handleVerify() {
  await para.verifyNewAccount({ verificationCode: verificationCode.value });
}
</script>
```

### Svelte

```svelte theme={null}
<script lang="ts">
  import { onMount, onDestroy } from "svelte";
  import { para } from "./your-para-client";
  import type { AuthPhase, StateSnapshot } from "@getpara/web-sdk";

  let email = "";
  let verificationCode = "";
  let authPhase: AuthPhase = "unauthenticated";
  let isPending = false;
  let unsubscribe: (() => void) | null = null;
  let popup: Window | null = null;
  let lastUrl: string | null = null;

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

      if (authStateInfo.verificationUrl && authStateInfo.verificationUrl !== lastUrl) {
        lastUrl = authStateInfo.verificationUrl;
        popup = window.open(authStateInfo.verificationUrl, "ParaVerification", "popup,width=400,height=500");
        return;
      }

      const url = authStateInfo.passkeyUrl || authStateInfo.passwordUrl || authStateInfo.pinUrl;
      if (url && url !== lastUrl) {
        lastUrl = url;
        popup = window.open(url, "ParaAuth", "popup,width=400,height=500");
      }
    });
  });

  onDestroy(() => {
    unsubscribe?.();
    lastUrl = null;
  });

  async function handleAuth() {
    isPending = true;
    try {
      const result = await para.authenticateWithEmailOrPhone({
        auth: { email },
      });
      console.log("Authenticated:", result.authInfo);
    } catch (err) {
      console.error("Auth failed:", err);
    } finally {
      isPending = false;
    }
  }

  async function handleVerify() {
    await para.verifyNewAccount({ verificationCode });
  }
</script>
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Simplified React Hooks" description="If you're using React, the pre-built hooks handle state management for you" href="/v2/react/guides/custom-ui-simplified" />

  <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" />

  <Card title="Solana Integration" description="Integrate Para with Solana for signing and transactions" href="/v2/react/guides/external-wallets/solana" />
</CardGroup>
