import initFanfare from "@fanfare-io/fanfare-sdk-core";
import type {
JourneyView,
SequenceView,
} from "@fanfare-io/fanfare-sdk-core/experiences";
const sdk = await initFanfare({
organizationId: "org_123",
publishableKey: "pk_live_123",
});
const journey = sdk.journeys.get("exp_123");
journey.view$.listen((view) => {
render(view);
});
function render(view: JourneyView) {
if (view.journeyStage === "ready") {
mountStartButton(() => view.start());
return;
}
if (view.journeyStage === "routing") {
mountLoading();
return;
}
if (view.journeyStage === "gated") {
mountGate({
gates: view.gates,
onAccessCode: (accessCode) => view.reroute({ accessCode }),
onRetry: () => view.retry(),
});
return;
}
if (view.offers.length > 0) {
// Routed upgrade offers are always gated and informational. Submitting an
// access code is single-shot: only "advanced" commits the code and moves the
// journey into the offered sequence; "unchanged" leaves current participation
// in place, while "gated" pushes the journey into a gated stage where the gate
// UI must take over.
mountUpgradeOffers({
offers: view.offers,
onAccessCode: async (accessCode) => {
const result = await view.submitAccessCode(accessCode);
if (result.outcome === "advanced") {
mountUpgraded(result.sequenceId);
} else if (result.outcome === "gated") {
mountUpgradeBlocked();
} else {
mountUpgradeRejected();
}
},
});
}
renderSequence(view.sequence);
}
function renderSequence(sequence: SequenceView) {
switch (sequence.phase) {
case "unavailable":
mountUnavailable();
return;
case "scheduled":
mountUpcoming({
startsAt: sequence.startsAt,
// WaitlistAttachmentView | undefined: { status, join(), leave() }
waitlist: sequence.waitlist,
});
return;
case "enterable":
mountEnterable(sequence);
return;
case "participating":
if (sequence.mechanism === "appointment") {
// Appointments never expose leave(); manage the booked slot instead.
mountParticipation({
type: sequence.mechanism,
display$: sequence.display$, // live slot, location, check-in state
onCancel: (reason) => sequence.cancel(reason),
onReschedule: (newSlotId, newLocationId) =>
sequence.reschedule(newSlotId, newLocationId),
});
return;
}
mountParticipation({
type: sequence.mechanism,
display$: sequence.display$, // live participating data (read here, not state$)
onLeave: "leave" in sequence ? () => sequence.leave() : undefined,
});
return;
case "settling":
mountSettling(sequence);
return;
case "granted":
// claim() returns the AdmissionGrant synchronously and starts checkout.
mountCheckout(sequence.claim().token);
return;
case "ended":
mountEnded({ reason: sequence.outcome.type });
}
}