Skip to main content

useExperienceJourney

The useExperienceJourney hook provides a React wrapper around the ExperienceJourney state machine for orchestrating complex consumer journeys through experiences.

Signature

function useExperienceJourney(
  experienceId: string | undefined,
  options?: UseExperienceJourneyOptions
): UseExperienceJourneyResult;

Parameters

ParameterTypeDescription
experienceIdstring | undefinedThe experience identifier
optionsUseExperienceJourneyOptionsOptional configuration

Options

interface UseExperienceJourneyOptions {
  /** Access code to use when starting */
  accessCode?: string;

  /** Automatically enter waitlist if available */
  autoEnterWaitlist?: boolean;

  /** Automatically start the journey when mounted */
  autoStart?: boolean;
}

Return Type

interface UseExperienceJourneyResult {
  /** The underlying ExperienceJourney instance */
  journey: ExperienceJourney | null;

  /** Current journey state */
  state: ExperienceJourneyState | null;

  /** Current status (derived from state) */
  status: ExperienceJourneyStatus;

  /** Error message if any */
  error: string | null;

  /** Start or restart the journey */
  start: (options?: ExperienceJourneyOptions) => Promise<ExperienceJourneyState>;
}

Journey Status

type ExperienceJourneyStatus =
  | "idle" // Not started
  | "entering_experience" // Starting journey
  | "routing_sequence" // Finding sequence
  | "needs_authentication" // Auth required
  | "needs_access_code" // Access code required
  | "validating_access" // Validating access
  | "loading_distributions" // Loading distribution data
  | "entering_waitlist" // Joining waitlist
  | "waiting" // On waitlist or sequence upcoming
  | "ready" // Ready to participate
  | "no_sequence_available" // No sequence found
  | "error"; // Error state

Journey State

interface ExperienceJourneyState {
  experienceId: string;
  sequenceId?: string;
  status: ExperienceJourneyStatus;
  snapshot: JourneySnapshot;
}

Basic Usage

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

function ExperiencePage({ experienceId }: { experienceId: string }) {
  const { isAuthenticated, guest } = useFanfareAuth();
  const { journey, state, status, error, start } = useExperienceJourney(experienceId);

  const handleStart = async () => {
    if (!isAuthenticated) {
      await guest();
    }
    await start();
  };

  if (error) {
    return <div className="error">{error}</div>;
  }

  return (
    <div className="experience-page">
      {status === "idle" && <button onClick={handleStart}>Start Experience</button>}

      {status === "needs_authentication" && (
        <div>
          <p>Please log in to continue</p>
          <LoginForm />
        </div>
      )}

      {status === "needs_access_code" && <AccessCodeForm journey={journey!} />}

      {status === "waiting" && (
        <div>
          <p>You are on the waitlist!</p>
          <p>We will notify you when your turn comes.</p>
        </div>
      )}

      {status === "ready" && (
        <div>
          <h2>You are ready!</h2>
          <JourneyParticipation journey={journey!} state={state!} />
        </div>
      )}
    </div>
  );
}

Auto-Start Journey

function AutoStartExperience({ experienceId }: { experienceId: string }) {
  const { status, state, error } = useExperienceJourney(experienceId, {
    autoStart: true,
  });

  if (status === "entering_experience" || status === "routing_sequence") {
    return <LoadingSpinner />;
  }

  if (error) {
    return <ErrorDisplay message={error} />;
  }

  return <JourneyDisplay state={state} status={status} />;
}

With Access Code

function VIPExperience({ experienceId, accessCode }: { experienceId: string; accessCode?: string }) {
  const { status, start } = useExperienceJourney(experienceId, {
    accessCode,
    autoStart: !!accessCode,
  });

  if (!accessCode && status === "idle") {
    return <AccessCodeEntry onSubmit={(code) => start({ accessCode: code })} />;
  }

  if (status === "needs_access_code") {
    return (
      <div>
        <p>Invalid access code. Please try again.</p>
        <AccessCodeEntry onSubmit={(code) => start({ accessCode: code })} />
      </div>
    );
  }

  return <JourneyDisplay status={status} />;
}

Access Code Form

function AccessCodeForm({ journey }: { journey: ExperienceJourney }) {
  const [code, setCode] = useState("");
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await journey.provideAccessCode(code);
    } catch (err) {
      setError("Invalid access code");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <h3>Enter Access Code</h3>
      {error && <div className="error">{error}</div>}
      <input type="text" value={code} onChange={(e) => setCode(e.target.value)} placeholder="Enter your code" />
      <button type="submit">Continue</button>
    </form>
  );
}

Working with Journey State

function JourneyParticipation({ journey, state }: { journey: ExperienceJourney; state: ExperienceJourneyState }) {
  const { snapshot } = state;
  const { availableActions, context } = snapshot;

  const handleAction = async (action: string) => {
    await journey.perform(action);
  };

  return (
    <div className="participation">
      <p>Sequence: {context.sequenceId}</p>
      <p>Distribution: {context.distribution?.type}</p>

      <div className="actions">
        {availableActions.sequence.includes("enter_queue") && (
          <button onClick={() => handleAction("enter_queue")}>Enter Queue</button>
        )}

        {availableActions.sequence.includes("enter_draw") && (
          <button onClick={() => handleAction("enter_draw")}>Enter Draw</button>
        )}

        {availableActions.sequence.includes("enter_auction") && (
          <button onClick={() => handleAction("enter_auction")}>Join Auction</button>
        )}

        {availableActions.sequence.includes("enter_timed_release") && (
          <button onClick={() => handleAction("enter_timed_release")}>Start Shopping</button>
        )}

        {availableActions.sequence.includes("leave_participation") && (
          <button onClick={() => handleAction("leave_participation")} className="secondary">
            Leave
          </button>
        )}
      </div>

      {snapshot.sequenceStage === "admitted" && context.admittanceToken && (
        <div className="admitted">
          <h3>You are admitted!</h3>
          <a href={`/checkout?token=${context.admittanceToken}`}>Proceed to Checkout</a>
        </div>
      )}
    </div>
  );
}

Listening to State Changes

function JourneyWithStateListener({ experienceId }: { experienceId: string }) {
  const { journey, status, start } = useExperienceJourney(experienceId);
  const [events, setEvents] = useState<string[]>([]);

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

    const unsubscribe = journey.state.listen((snapshot) => {
      setEvents((prev) => [...prev, `Stage: ${snapshot.journeyStage} / ${snapshot.sequenceStage}`]);
    });

    return () => unsubscribe();
  }, [journey]);

  return (
    <div>
      <button onClick={() => start()}>Start</button>
      <div className="event-log">
        {events.map((event, i) => (
          <div key={i}>{event}</div>
        ))}
      </div>
    </div>
  );
}

Handling Requirements

function RequirementsHandler({ experienceId }: { experienceId: string }) {
  const { journey, state, status, start } = useExperienceJourney(experienceId, {
    autoStart: true,
  });

  const requirements = state?.snapshot.requirements || [];

  // Handle authentication requirement
  if (status === "needs_authentication") {
    return (
      <AuthenticationFlow
        onComplete={async () => {
          await journey?.authenticate();
        }}
      />
    );
  }

  // Handle access code requirement
  if (status === "needs_access_code") {
    const isRequired = requirements.find((r) => r.type === "access_code")?.required;

    return (
      <div>
        <AccessCodeInput onSubmit={(code) => journey?.provideAccessCode(code)} />
        {!isRequired && <button onClick={() => journey?.skipAccessCode()}>Skip</button>}
      </div>
    );
  }

  return <JourneyDisplay journey={journey} state={state} />;
}

Auto-Enter Waitlist

function WaitlistAutoJoin({ experienceId }: { experienceId: string }) {
  const { status, state } = useExperienceJourney(experienceId, {
    autoStart: true,
    autoEnterWaitlist: true, // Automatically join waitlist if upcoming
  });

  if (status === "waiting") {
    return (
      <div className="waitlist-joined">
        <h3>You are on the waitlist!</h3>
        <p>We will notify you when the experience opens.</p>
      </div>
    );
  }

  return <JourneyDisplay status={status} state={state} />;
}

Complete Example

function CompleteExperienceFlow({ experienceId }: { experienceId: string }) {
  const { isAuthenticated, isGuest, guest } = useFanfareAuth();
  const { journey, state, status, error, start } = useExperienceJourney(experienceId);

  const handleStart = async (accessCode?: string) => {
    // Ensure some form of authentication
    if (!isAuthenticated && !isGuest) {
      await guest();
    }
    await start({ accessCode });
  };

  // Render based on status
  const renderContent = () => {
    switch (status) {
      case "idle":
        return (
          <div className="start-section">
            <h2>Welcome!</h2>
            <button onClick={() => handleStart()}>Enter Experience</button>
          </div>
        );

      case "entering_experience":
      case "routing_sequence":
      case "loading_distributions":
        return <LoadingSpinner message="Setting up your experience..." />;

      case "needs_authentication":
        return <LoginPrompt onLogin={() => journey?.authenticate()} />;

      case "needs_access_code":
        return <AccessCodeForm onSubmit={(code) => handleStart(code)} />;

      case "waiting":
        return (
          <WaitingView
            onJoinWaitlist={() => journey?.enterWaitlist()}
            isOnWaitlist={state?.snapshot.sequenceStage === "waitlist_entered"}
          />
        );

      case "ready":
        return <ReadyView journey={journey!} state={state!} />;

      case "no_sequence_available":
        return <NoAccessView />;

      case "error":
        return <ErrorView message={error} onRetry={() => start()} />;

      default:
        return null;
    }
  };

  return <div className="experience-flow">{renderContent()}</div>;
}

i18n Integration

The hook automatically loads experience-level translations:
function LocalizedExperience({ experienceId }: { experienceId: string }) {
  // The hook loads i18n from the experience and applies it to the I18nProvider
  const { status } = useExperienceJourney(experienceId, { autoStart: true });

  // Translations from the experience are now available
  const t = useTranslations();

  return (
    <div>
      <h1>{t("experience.title")}</h1>
      <p>{t("experience.description")}</p>
    </div>
  );
}

TypeScript

import { useExperienceJourney } from "@waitify-io/fanfare-sdk-react";
import type {
  ExperienceJourneyState,
  ExperienceJourneyStatus,
  ExperienceJourney,
  UseExperienceJourneyOptions,
  UseExperienceJourneyResult,
} from "@waitify-io/fanfare-sdk-react";

function TypedJourney({ experienceId }: { experienceId: string }) {
  const options: UseExperienceJourneyOptions = {
    autoStart: true,
    autoEnterWaitlist: true,
  };

  const result: UseExperienceJourneyResult = useExperienceJourney(experienceId, options);
  const { journey, state, status, start } = result;

  return null;
}