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

# Spa integration

# SPA Integration Guide

Learn how to integrate Fanfare into a single-page application (SPA) using React, Vue, or Angular.

## Overview

This guide walks you through integrating the Fanfare SDK into a client-side rendered single-page application. By the end, you'll have a working queue experience that manages high-demand product access.

**What you'll learn:**

* Installing and configuring the Fanfare SDK
* Setting up the React provider
* Managing authentication
* Implementing the experience journey flow
* Handling queue states and admission

**Complexity:** Beginner
**Time to complete:** 30 minutes

## Prerequisites

Before starting, ensure you have:

* A Fanfare account with API credentials
* A React 18+ application (or Vue/Angular equivalent)
* Node.js 18+ installed
* Basic understanding of React hooks

## Step 1: Install the SDK

Install the Fanfare React SDK and its peer dependencies:

```bash theme={null}
npm install @fanfare-io/fanfare-sdk-react motion
```

Or with other package managers:

```bash theme={null}
# pnpm
pnpm add @fanfare-io/fanfare-sdk-react motion

# yarn
yarn add @fanfare-io/fanfare-sdk-react motion
```

## Step 2: Configure the Provider

Wrap your application with the `FanfareProvider` to make the SDK available throughout your component tree.

```tsx theme={null}
// src/App.tsx
import { FanfareProvider } from "@fanfare-io/fanfare-sdk-react";
import { ExperiencePage } from "./pages/ExperiencePage";

export function App() {
  return (
    <FanfareProvider
      organizationId="org_your_organization_id"
      publishableKey="pk_live_your_publishable_key"
    >
      <ExperiencePage />
    </FanfareProvider>
  );
}
```

### Provider Options

| Option             | Type        | Default  | Description                        |
| ------------------ | ----------- | -------- | ---------------------------------- |
| `organizationId`   | `string`    | Required | Your Fanfare organization ID       |
| `publishableKey`   | `string`    | Required | Your publishable API key           |
| `autoRestore`      | `boolean`   | `true`   | Restore session on mount           |
| `autoResume`       | `boolean`   | `true`   | Resume operations after restore    |
| `loadingComponent` | `ReactNode` | `null`   | Component shown while initializing |
| `locale`           | `string`    | `"en"`   | Locale for translations            |

## Step 3: Authenticate the Consumer

Before a consumer can participate in an experience, they need to authenticate. Fanfare supports both anonymous (guest) and identified authentication.

```tsx theme={null}
// src/hooks/useConsumerAuth.ts
import { useFanfareAuth } from "@fanfare-io/fanfare-sdk-react";
import { useEffect } from "react";

export function useConsumerAuth() {
  const { isAuthenticated, isGuest, guest, session } = useFanfareAuth();

  useEffect(() => {
    // Automatically create a guest session if not authenticated
    if (!isAuthenticated) {
      guest();
    }
  }, [isAuthenticated, guest]);

  return { isAuthenticated, isGuest, session };
}
```

### Using the Auth Hook

```tsx theme={null}
// src/components/AuthStatus.tsx
import { useFanfareAuth } from "@fanfare-io/fanfare-sdk-react";

export function AuthStatus() {
  const { isAuthenticated, isGuest, session } = useFanfareAuth();

  if (!isAuthenticated) {
    return <div>Connecting...</div>;
  }

  return (
    <div>
      <p>Status: {isGuest ? "Guest" : "Authenticated"}</p>
      <p>Consumer ID: {session?.consumerId}</p>
    </div>
  );
}
```

## Step 4: Create the Experience Journey

Use the `useExperienceJourney` hook to manage the complete flow from entering an experience to admission.

```tsx theme={null}
// src/pages/ExperiencePage.tsx
import { useExperienceJourney } from "@fanfare-io/fanfare-sdk-react";
import { useConsumerAuth } from "../hooks/useConsumerAuth";

export function ExperiencePage() {
  const { isAuthenticated } = useConsumerAuth();

  const { view, error, start } = useExperienceJourney("exp_your_experience_id", {
    autoStart: false,
  });

  // Start the journey once authenticated
  useEffect(() => {
    if (isAuthenticated && view?.journeyStage === "ready") {
      void start();
    }
  }, [isAuthenticated, start, view?.journeyStage]);

  return (
    <div className="experience-container">
      <h1>Product Launch Queue</h1>
      <ExperienceStatus view={view} error={error} />
      <JourneyActions view={view} />
    </div>
  );
}
```

## Step 5: Handle Journey States

The journey progresses through several states. Handle each appropriately:

```tsx theme={null}
// src/components/ExperienceStatus.tsx
import type { JourneyView } from "@fanfare-io/fanfare-sdk-core/experiences";

interface ExperienceStatusProps {
  view: JourneyView | null;
  error: string | null;
}

export function ExperienceStatus({ view, error }: ExperienceStatusProps) {
  if (error) {
    return <p className="error">Error: {error}</p>;
  }

  if (!view || view.journeyStage === "routing") {
    return <p>Finding your access path...</p>;
  }

  if (view.journeyStage === "gated") {
    return <AuthenticationRequired gates={view.gates} />;
  }

  if (view.journeyStage === "routed") {
    switch (view.sequence.phase) {
      case "scheduled":
        return <p>Waiting for access to open...</p>;
      case "enterable":
        return <p>Ready to participate!</p>;
      case "participating":
        return <p>You are participating.</p>;
      case "granted":
        return <p>You have access to checkout.</p>;
      case "unavailable":
      case "ended":
        return <p>No access currently available.</p>;
    }
  }

  return <p>Preparing experience...</p>;
}
```

## Step 6: Implement Journey Actions

Allow consumers to interact with the journey based on the actions exposed by `JourneyView`:

```tsx theme={null}
// src/components/JourneyActions.tsx
import type { JourneyView } from "@fanfare-io/fanfare-sdk-core/experiences";

interface JourneyActionsProps {
  view: JourneyView | null;
}

export function JourneyActions({ view }: JourneyActionsProps) {
  if (view?.journeyStage !== "routed") return null;

  return (
    <div className="journey-actions">
      {view.sequence.phase === "enterable" && "enter" in view.sequence && (
        <button onClick={() => view.sequence.enter()} className="primary-button">
          Join the Queue
        </button>
      )}

      {view.sequence.phase === "participating" && view.sequence.mechanism === "queue" && (
        <p className="hint">You are in the queue. Stay on this page for updates.</p>
      )}

      {view.sequence.phase === "granted" && (
        <AdmittedView
          admissionToken={view.sequence.grant.token}
          expiresAt={view.sequence.grant.expiresAt}
        />
      )}
    </div>
  );
}
```

## Step 7: Display Queue Progress

When participating in a queue, the public journey view tells you the consumer is participating. If your UI needs live queue metrics such as position or wait estimates, read them from the participating sequence's `state$` display store rather than from the raw participation identity.

## Step 8: Handle Admission

When a consumer is admitted, redirect them to checkout or show purchase options:

```tsx theme={null}
// src/components/AdmittedView.tsx
interface AdmittedViewProps {
  admissionToken: string | undefined;
  expiresAt: number | undefined;
}

export function AdmittedView({ admissionToken, expiresAt }: AdmittedViewProps) {
  const [timeRemaining, setTimeRemaining] = useState<number | null>(null);

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

    const interval = setInterval(() => {
      const remaining = Math.max(0, expiresAt - Date.now());
      setTimeRemaining(Math.floor(remaining / 1000));

      if (remaining <= 0) {
        clearInterval(interval);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [expiresAt]);

  const handleCheckout = async () => {
    await fetch("/api/checkout/handoff", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${admissionToken}`,
      },
    });

    window.location.href = "/checkout";
  };

  return (
    <div className="admitted-view">
      <h2>You're In!</h2>
      <p>You have access to purchase.</p>

      {timeRemaining !== null && (
        <p className="timer">
          Time remaining: {Math.floor(timeRemaining / 60)}:{(timeRemaining % 60).toString().padStart(2, "0")}
        </p>
      )}

      <button onClick={handleCheckout} className="checkout-button">
        Proceed to Checkout
      </button>
    </div>
  );
}
```

## Complete Example

Here's a complete, minimal implementation:

```tsx theme={null}
// src/App.tsx
import { FanfareProvider, useExperienceJourney, useFanfareAuth } from "@fanfare-io/fanfare-sdk-react";
import { useEffect } from "react";

function ExperienceWidget() {
  const { isAuthenticated, guest } = useFanfareAuth();
  const { view, start } = useExperienceJourney("exp_xxx");
  const handleCheckout = async (admissionGrant: string) => {
    await fetch("/api/checkout/handoff", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${admissionGrant}`,
      },
    });
    window.location.href = "/checkout";
  };

  // Auto-authenticate as guest
  useEffect(() => {
    if (!isAuthenticated) guest();
  }, [isAuthenticated, guest]);

  useEffect(() => {
    if (isAuthenticated && view?.journeyStage === "ready") {
      void start();
    }
  }, [isAuthenticated, start, view?.journeyStage]);

  return (
    <div>
      <p>Status: {view?.journeyStage ?? "loading"}</p>

      {view?.journeyStage === "routed" && view.sequence.phase === "enterable" && "enter" in view.sequence && (
        <button onClick={() => view.sequence.enter()}>Join Queue</button>
      )}

      {view?.journeyStage === "routed" && view.sequence.phase === "granted" && (
        <button onClick={() => handleCheckout(view.sequence.grant.token)}>Go to Checkout</button>
      )}
    </div>
  );
}

export function App() {
  return (
    <FanfareProvider organizationId="org_xxx" publishableKey="pk_live_xxx">
      <ExperienceWidget />
    </FanfareProvider>
  );
}
```

## Testing Your Integration

1. **Local testing**: Use test credentials from the dashboard and keep SDK environment/API URL overrides unset
2. **Check authentication**: Verify the consumer session is created
3. **Test queue flow**: Enter the queue and verify position updates
4. **Verify admission**: Confirm admission tokens are generated correctly

## Troubleshooting

### SDK doesn't initialize

* Check that `organizationId` and `publishableKey` are correct
* Ensure the provider wraps your component tree
* Check browser console for error messages

### Authentication fails

* Verify API credentials in the Fanfare dashboard
* Check network requests for error responses
* Ensure cookies are enabled for session storage

### Queue position doesn't update

* Verify polling is started with `startPolling()`
* Check that the queue ID matches
* Ensure event listeners are subscribed

### Admission token issues

* Tokens expire after 10 minutes by default
* Verify the token hasn't been used already
* Confirm the consumer is continuing from the active session

## What's Next

* [Anonymous Consumers](/guides/authentication/anonymous-consumers) - Guest authentication patterns
* [Checkout Integration](/guides/checkout-integration/checkout-overview) - Connect to checkout
* [Product Launch](/guides/use-cases/product-launch) - Full product launch example
* [Real-time Updates](/guides/advanced/real-time-updates) - Live UI patterns
