Skip to main content

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:
npm install @fanfare-io/fanfare-sdk-react motion
Or with other package managers:
# 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.
// 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

OptionTypeDefaultDescription
organizationIdstringRequiredYour Fanfare organization ID
publishableKeystringRequiredYour publishable API key
autoRestorebooleantrueRestore session on mount
autoResumebooleantrueResume operations after restore
loadingComponentReactNodenullComponent shown while initializing
localestring"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.
// 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

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