Skip to main content

Experience Journey

The Experience Journey system provides a state machine for orchestrating complex consumer journeys through multiple stages of an experience. It handles routing, authentication requirements, sequence selection, and distribution participation.

Overview

An “Experience” in Fanfare is a container that can include multiple sequences and distribution types (queues, draws, auctions, timed releases). The ExperienceJourney class manages the consumer’s progression through these stages.
┌─────────────────────────────────────────────────────────────┐
│                      Experience                              │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │                      Sequences                          │ │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐   │ │
│  │  │   General    │  │     VIP      │  │   Early      │   │ │
│  │  │   Access     │  │   Access     │  │   Access     │   │ │
│  │  └──────────────┘  └──────────────┘  └──────────────┘   │ │
│  └─────────────────────────────────────────────────────────┘ │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │                   Distributions                         │ │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────┐  │ │
│  │  │  Queue   │  │   Draw   │  │ Auction  │  │  Timed  │  │ │
│  │  │          │  │          │  │          │  │ Release │  │ │
│  │  └──────────┘  └──────────┘  └──────────┘  └─────────┘  │ │
│  └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Creating a Journey

const journey = fanfare.experiences.createJourney("exp_123");
The journey is created idempotently - calling createJourney with the same experience ID returns the existing journey instance.

Journey Stages

JourneyStage

High-level stages of the journey:
StageDescription
not_startedJourney has not begun
enteringEntering the experience
needs_authAuthentication is required
needs_access_codeAccess code is required
routingFinding the appropriate sequence
routedSuccessfully routed to a sequence

SequenceStage

Stages within a routed sequence:
StageDescription
noneNo sequence stage (pre-routing)
upcomingDistribution is scheduled but not active
waitlist_enteredConsumer is on the waitlist
active_enterableActive distribution, consumer can enter
participatingActively participating in distribution
admittedAdmitted with valid token
admission_expiredAdmission token has expired
endedDistribution has ended

State Snapshot

The journey state is accessible via the state atom:
interface JourneySnapshot {
  revision: number;
  updatedAt: number;
  journeyStage: JourneyStage;
  sequenceStage: SequenceStage;
  requirements: Requirement[];
  availableActions: {
    journey: JourneyAction[];
    sequence: SequenceAction[];
  };
  context: JourneyContext;
  events: JourneyEvent[];
  lastSeenEventId?: string;
}

Subscribing to State Changes

import { computed } from "nanostores";

// Subscribe to full state
journey.state.listen((snapshot) => {
  console.log("Journey stage:", snapshot.journeyStage);
  console.log("Sequence stage:", snapshot.sequenceStage);
});

// Or use computed stores for specific values
journey.journeyStage$.listen((stage) => {
  console.log("Journey stage changed:", stage);
});

journey.sequenceStage$.listen((stage) => {
  console.log("Sequence stage changed:", stage);
});

journey.requirements$.listen((requirements) => {
  console.log("Requirements:", requirements);
});

Lifecycle Methods

start()

Begins the journey. Optionally accepts an access code.
await journey.start();

// Or with access code
await journey.start({ accessCode: "VIP2024" });

// Or with reset
await journey.start({ reset: true });

destroy()

Removes the journey and clears state.
journey.destroy();

Requirement Handling

Requirements

The journey may require certain conditions to be met:
interface Requirement {
  type: "authentication" | "access_code" | "bot_check";
  required: boolean;
  reason?: string;
  metadata?: Record<string, unknown>;
}

authenticate()

Call after the consumer has authenticated:
// After successful authentication
await journey.authenticate();

provideAccessCode()

Provide an access code:
await journey.provideAccessCode("VIP2024");

skipAccessCode()

Skip the access code requirement if optional:
const req = journey.requirements$.get().find((r) => r.type === "access_code");
if (!req?.required) {
  await journey.skipAccessCode();
}

completeBotCheck()

Mark bot check as complete:
await journey.completeBotCheck();

Distribution Participation

refreshDistribution()

Refresh the current distribution context:
await journey.refreshDistribution();

startPolling() / stopPolling()

Poll for distribution updates:
// Start polling every 5 seconds
journey.startPolling(5000);

// Stop polling
journey.stopPolling();

enterQueue()

Enter the active queue:
if (snapshot.availableActions.sequence.includes("enter_queue")) {
  await journey.enterQueue();
}

enterDraw()

Enter the active draw:
if (snapshot.availableActions.sequence.includes("enter_draw")) {
  await journey.enterDraw();
}

enterAuction()

Enter the active auction:
if (snapshot.availableActions.sequence.includes("enter_auction")) {
  await journey.enterAuction();
}

enterTimedRelease()

Enter the active timed release:
if (snapshot.availableActions.sequence.includes("enter_timed_release")) {
  await journey.enterTimedRelease();
}

completeTimedRelease()

Complete a timed release (after successful purchase):
if (snapshot.availableActions.sequence.includes("complete_timed_release")) {
  await journey.completeTimedRelease();
}

enterWaitlist()

Join the waitlist for an upcoming distribution:
if (snapshot.availableActions.sequence.includes("enter_waitlist")) {
  await journey.enterWaitlist();
}

leaveWaitlist()

Leave the waitlist:
if (snapshot.availableActions.sequence.includes("leave_waitlist")) {
  await journey.leaveWaitlist();
}

leaveParticipation()

Leave the current distribution:
if (snapshot.availableActions.sequence.includes("leave_participation")) {
  await journey.leaveParticipation();
}

markAdmitted()

Mark the consumer as admitted with a token:
journey.markAdmitted("token_xxx", Date.now() + 3600000); // expires in 1 hour

Actions API

perform()

Execute any available action by name:
await journey.perform("enter_queue");
await journey.perform("provide_access_code", "VIP2024");

Available Actions

Journey actions:
  • start - Begin the journey
  • authenticate - Mark authentication complete
  • provide_access_code - Provide an access code
  • skip_access_code - Skip optional access code
  • request_reroute - Request re-routing
  • retry - Retry current operation
  • complete_bot_check - Mark bot check complete
  • refresh_distribution - Refresh distribution data
Sequence actions:
  • enter_waitlist - Join waitlist
  • leave_waitlist - Leave waitlist
  • enter_queue - Enter queue
  • enter_draw - Enter draw
  • enter_auction - Enter auction
  • enter_timed_release - Enter timed release
  • complete_timed_release - Complete timed release
  • leave_participation - Leave current distribution

Events

Journey Events

interface JourneyEvent {
  id: string;
  ts: number;
  kind: "reroute" | "sequence_change" | "distribution_change" | "requirement" | "error" | "info";
  severity: "info" | "success" | "warning" | "error";
  audience: "user" | "system" | "analytics";
  message: string;
  detail?: Record<string, unknown>;
}

Subscribing to Events

// Get the latest unacknowledged event
journey.latestEvent$.listen((event) => {
  if (event) {
    console.log(event.message);
    journey.ackEvent(event.id);
  }
});

// Or acknowledge all events
journey.ackAllEvents();

Resume Flow

resumeFromMe()

Resume journey state from server data:
// Fetch consumer data
const me = await fanfare.experiences.getMe();

// Resume the journey
const resumed = await journey.resumeFromMe(me);
if (resumed) {
  console.log("Journey resumed successfully");
}

resumeFromServer()

Resume with minimal server data:
await journey.resumeFromServer({
  sequenceId: "seq_123",
  waitlist: {
    waitlistId: "wl_456",
    enteredAt: "2024-01-15T12:00:00Z",
  },
});

Complete Example

import Fanfare from "@waitify-io/fanfare-sdk-core";

async function runExperience() {
  const fanfare = await Fanfare.init({
    organizationId: "org_xxx",
    publishableKey: "pk_live_xxx",
  });

  // Restore session
  await fanfare.restore();

  // Create or get journey
  const journey = fanfare.experiences.createJourney("exp_123");

  // Subscribe to state changes
  journey.state.listen((snapshot) => {
    console.log("Stage:", snapshot.journeyStage, snapshot.sequenceStage);

    // Handle requirements
    if (snapshot.journeyStage === "needs_auth") {
      showAuthModal();
    }

    // Handle available actions
    if (snapshot.availableActions.sequence.includes("enter_queue")) {
      showEnterButton();
    }

    // Handle admission
    if (snapshot.sequenceStage === "admitted") {
      const token = snapshot.context.admittanceToken;
      redirectToCheckout(token);
    }
  });

  // Start the journey
  await journey.start();

  // Start polling for updates
  journey.startPolling(5000);
}

runExperience();

Context Data

The journey context contains all relevant data:
interface JourneyContext {
  experienceId: string;
  experience?: ExperienceSession;
  sequenceId?: string;
  distribution?: DistributionSummary;
  participation?: {
    id: string;
    type: "queue" | "draw" | "auction" | "waitlist" | "timed_release";
  };
  accessCode?: string;
  admittanceToken?: string;
  admittanceExpiresAt?: number;
}
Access context data:
const snapshot = journey.state.get();

console.log("Experience:", snapshot.context.experienceId);
console.log("Sequence:", snapshot.context.sequenceId);
console.log("Participation:", snapshot.context.participation?.type);
console.log("Admission token:", snapshot.context.admittanceToken);