Skip to main content
This guide walks you through implementing different experience types with the Fanfare SDK. You will learn how to create queues, draws, auctions, and timed releases.

Before You Start

Ensure you have:
  1. Installed the SDK (see Installation)
  2. Configured the provider (see Configuration)
  3. Created an experience in your Fanfare dashboard

Using the Experience Journey

The recommended way to implement any experience is using the useExperienceJourney hook. This hook manages the entire customer journey through your experience, handling:
  • Session creation and restoration
  • Sequence routing (general admission, VIP, etc.)
  • Distribution entry (queue, draw, auction, or timed release)
  • Public journey state updates and admission notifications
import { useExperienceJourney } from "@fanfare-io/fanfare-sdk-react";

function ProductPage() {
  const { view, start, error } = useExperienceJourney("exp_your_experience_id");

  if (view?.journeyStage === "ready") {
    return <button onClick={() => start()}>Start</button>;
  }

  if (view?.journeyStage === "routed" && view.sequence.phase === "enterable" && "enter" in view.sequence) {
    return <button onClick={() => view.sequence.enter()}>Enter</button>;
  }

  // Render the other public states...
}

Public Journey States

Use JourneyView first when building UI. It exposes only the actions that are valid for the current public state.
StateDescription
readyThe journey exists and can be started
routingFanfare is resolving the consumer’s current public path
gatedThe consumer must complete authentication, an access code, or another public requirement
routed.scheduledA matching sequence exists but is not active yet
routed.waitlistThe consumer is on the waitlist
routed.enterableThe consumer can enter, book, bid, or otherwise participate
routed.participatingThe consumer is already participating
routed.grantedThe consumer has access to continue to checkout or the next app step
routed.endedThe sequence has a terminal outcome
routed.unavailableNo public sequence is currently available

Creating a Queue Experience

Queues are first-in-first-out waiting lines. Customers join and wait for their turn.

Basic Queue Implementation

import { useExperienceJourney } from "@fanfare-io/fanfare-sdk-react";

function QueueExperience() {
  const { view, start, error } = useExperienceJourney("exp_queue_experience_id");

  if (view?.journeyStage === "ready") {
    return (
      <div className="queue-landing">
        <h1>Exclusive Product Launch</h1>
        <p>Join the queue to get access to our limited release.</p>
        <button onClick={() => start()}>Join Queue</button>
      </div>
    );
  }

  if (!view || view.journeyStage === "routing") {
    return <p>Setting up your spot...</p>;
  }

  if (view.journeyStage === "routed" && view.sequence.phase === "participating") {
    return (
      <div className="queue-waiting">
        <h2>You are in the queue</h2>
        <p>Please keep this page open. You will be automatically admitted.</p>
      </div>
    );
  }

  if (view.journeyStage === "routed" && view.sequence.phase === "granted") {
    const admissionGrant = view.sequence.grant.token;
    const proceedToCheckout = async () => {
      await fetch("/api/checkout/handoff", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${admissionGrant}`,
        },
      });
      window.location.assign("/checkout");
    };

    return (
      <div className="queue-admitted">
        <h2>You are in!</h2>
        <p>You have been admitted. Complete your purchase now.</p>
        <button onClick={proceedToCheckout}>Proceed to Checkout</button>
      </div>
    );
  }

  if (error) {
    return (
      <div className="queue-error">
        <h2>Something went wrong</h2>
        <p>{error}</p>
        <button onClick={() => start()}>Try Again</button>
      </div>
    );
  }

  return null;
}

Using the Experience Widget

For the fastest implementation, use the supported ExperienceWidget. It renders the correct module for the routed distribution type and exposes slots when you need custom UI.
import { ExperienceWidget } from "@fanfare-io/fanfare-sdk-react";
import "@fanfare-io/fanfare-sdk-react/styles";

function ProductPage() {
  return (
    <div className="product-page">
      <h1>Limited Edition Release</h1>
      <ExperienceWidget experienceId="exp_your_experience_id" />
    </div>
  );
}
Use this same widget for queues, draws, auctions, appointments, and timed releases. The routed view.sequence.phase and view.sequence.mechanism decide which module appears.

Creating a Draw Experience

Draws randomly select winners from entrants. The same journey state model applies; only the participating and admitted UI copy changes.
import { useExperienceJourney } from "@fanfare-io/fanfare-sdk-react";

function DrawExperience() {
  const { view, start, error } = useExperienceJourney("exp_draw_experience_id");
  const proceedToCheckout = async (admissionGrant: string) => {
    await fetch("/api/checkout/handoff", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${admissionGrant}`,
      },
    });
    window.location.assign("/checkout");
  };

  if (view?.journeyStage === "ready") {
    return <button onClick={() => start()}>Enter Draw</button>;
  }

  if (view?.journeyStage === "routed" && view.sequence.phase === "participating") {
    return <p>You are entered. Winners will be selected when the draw closes.</p>;
  }

  if (view?.journeyStage === "routed" && view.sequence.phase === "granted") {
    return <button onClick={() => proceedToCheckout(view.sequence.grant.token)}>Complete Purchase</button>;
  }

  return error ? <p>{error}</p> : <p>Loading draw...</p>;
}

Creating an Auction Experience

Auction experiences expose auction-specific actions while the consumer is participating. Render those actions only when they are present on the current sequence view.
import { useState } from "react";
import { useExperienceJourney } from "@fanfare-io/fanfare-sdk-react";

function AuctionExperience() {
  const [bidAmount, setBidAmount] = useState("");
  const { view, start, error } = useExperienceJourney("exp_auction_experience_id", {
    autoStart: true,
  });
  const proceedToCheckout = async (admissionGrant: string) => {
    await fetch("/api/checkout/handoff", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${admissionGrant}`,
      },
    });
    window.location.assign("/checkout");
  };

  if (view?.journeyStage === "ready") {
    return <button onClick={() => start()}>Start Auction</button>;
  }

  if (view?.journeyStage === "routed" && view.sequence.phase === "participating" && "bid" in view.sequence) {
    return (
      <form
        onSubmit={(event) => {
          event.preventDefault();
          void view.sequence.bid(bidAmount);
        }}
      >
        <input value={bidAmount} onChange={(event) => setBidAmount(event.target.value)} />
        <button type="submit">Place Bid</button>
      </form>
    );
  }

  if (view?.journeyStage === "routed" && view.sequence.phase === "granted") {
    return <button onClick={() => proceedToCheckout(view.sequence.grant.token)}>Complete Purchase</button>;
  }

  return error ? <p>{error}</p> : <p>Loading auction...</p>;
}

Creating a Timed Release Experience

Timed releases expose a completion action once the consumer is participating. Keep the button tied to the state-gated action on view.sequence.
import { useExperienceJourney } from "@fanfare-io/fanfare-sdk-react";

function TimedReleaseExperience() {
  const { view, start } = useExperienceJourney("exp_timed_release_experience_id");
  const proceedToCheckout = async (admissionGrant: string) => {
    await fetch("/api/checkout/handoff", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${admissionGrant}`,
      },
    });
    window.location.assign("/checkout");
  };

  if (view?.journeyStage === "ready") {
    return <button onClick={() => start()}>Enter</button>;
  }

  if (view?.journeyStage === "routed" && view.sequence.phase === "participating" && "complete" in view.sequence) {
    return <button onClick={() => view.sequence.complete()}>Continue to checkout</button>;
  }

  if (view?.journeyStage === "routed" && view.sequence.phase === "granted") {
    return <button onClick={() => proceedToCheckout(view.sequence.grant.token)}>Complete Purchase</button>;
  }

  return <p>Loading...</p>;
}

Handling Authentication

Some experiences require user authentication. The journey handles this automatically:
import { useExperienceJourney, AuthForm } from "@fanfare-io/fanfare-sdk-react";

function AuthenticatedExperience() {
  const { view } = useExperienceJourney("exp_authenticated_experience_id");

  if (view?.journeyStage === "gated") {
    return (
      <div className="auth-required">
        <h2>Sign in to continue</h2>
        <AuthForm
          onSuccess={() => {
            void view.reroute();
          }}
        />
      </div>
    );
  }

  // ... rest of the experience
}

Handling Access Codes

For VIP or restricted sequences, handle access code requirements:
import { useExperienceJourney } from "@fanfare-io/fanfare-sdk-react";
import { useState } from "react";

function VIPExperience() {
  const [accessCode, setAccessCode] = useState("");
  const { view, start, error } = useExperienceJourney("exp_vip_experience_id");

  if (view?.journeyStage === "ready" || view?.journeyStage === "gated") {
    const submit = () =>
      view.journeyStage === "gated" ? view.reroute({ accessCode }) : start({ accessCode });

    return (
      <div className="vip-entry">
        <h2>VIP Access</h2>
        <p>Enter your access code to continue.</p>
        <input
          type="text"
          value={accessCode}
          onChange={(e) => setAccessCode(e.target.value)}
          placeholder="Enter access code"
        />
        <button onClick={submit}>Submit</button>
        {error && <p className="error">{error}</p>}
      </div>
    );
  }

  // ... rest of the experience
}

Waitlists for Upcoming Experiences

If an experience has not started yet, customers can join a waitlist:
import { useExperienceJourney } from "@fanfare-io/fanfare-sdk-react";

function UpcomingExperience() {
  const { view } = useExperienceJourney("exp_upcoming_experience_id");

  if (view?.journeyStage === "routed" && view.sequence.phase === "participating" && view.sequence.mechanism === "waitlist") {
    return (
      <div className="waitlist-joined">
        <h2>You are on the waitlist</h2>
        <p>We will notify you when the experience begins.</p>
      </div>
    );
  }

  // ... rest of the experience
}

Best Practices

1. Always Handle All States

Ensure you handle all possible journey states to avoid blank screens:
const handlers: Record<string, () => JSX.Element | null> = {
  ready: () => <LandingView />,
  routing: () => <LoadingSpinner />,
  gated: () => <GateView />,
  "routed.scheduled": () => <UpcomingView />,
  "routed.waitlist": () => <WaitingView />,
  "routed.enterable": () => <EnterView />,
  "routed.participating": () => <ParticipationView />,
  "routed.granted": () => <GrantedView />,
  "routed.ended": () => <EndedView />,
  "routed.unavailable": () => <UnavailableView />,
};

const key =
  view?.journeyStage === "routed"
    ? view.sequence.mechanism === "waitlist"
      ? "routed.waitlist"
      : `routed.${view.sequence.phase}`
    : view?.journeyStage;

return (key && handlers[key]?.()) ?? <LoadingSpinner />;

2. Persist the Admission Token

Always include the admission token when redirecting to checkout:
if (view?.journeyStage === "routed" && view.sequence.phase === "granted") {
  const token = view.sequence.grant.token;
  return <a href={`/checkout?admission_token=${token}`}>Complete Purchase</a>;
}

3. Handle Token Expiration

Admission tokens expire. Check the expiration and handle accordingly:
const admission =
  view?.journeyStage === "routed" && view.sequence.phase === "granted"
    ? view.sequence
    : null;
const expiresAt = admission?.grant.expiresAt ? new Date(admission.grant.expiresAt) : null;
const isExpired = expiresAt && expiresAt < new Date();

if (isExpired) {
  return <p>Your session has expired. Please try again.</p>;
}

4. Provide Clear Feedback

Always show loading states and progress indicators:
if (isLoading) {
  return <LoadingSpinner message="Please wait..." />;
}

Next Steps