Skip to main content

Checkout Integration Overview

Learn how to connect Fanfare experiences to your checkout flow, from admission to order completion.

Overview

When a consumer is admitted through a Fanfare experience, the public SDK surfaces an admissionGrant. This guide explains how to hand that grant to your checkout boundary and complete the purchase. What you’ll learn:
  • Understanding the admission-to-checkout flow
  • Working with admission grants
  • Handoff patterns for different platforms
  • Completing the checkout cycle
Complexity: Beginner Time to complete: 25 minutes

Prerequisites

  • Fanfare SDK integrated and working
  • An existing checkout system
  • Understanding of your e-commerce platform

The Checkout Flow

Checkout flow diagram showing experience entry, participation, access granted, server check, order placement, and completion.

Understanding Admission Grants

When a consumer is admitted, the public SDK surfaces an admission grant. This grant:
  • Proves they were legitimately admitted
  • May include an expiration field on the admitted view or slot props
  • Can be validated server-side
  • Should only be used once

Grant Lifecycle

Admission lifecycle diagram showing created, checkout window, used, expired, and invalid states.

Step 1: Detect Admission

Use ExperienceWidget when you only need to hand admitted consumers to checkout:
import { ExperienceWidget } from "@fanfare-io/fanfare-sdk-react";

function ExperienceWithCheckout({ experienceId }: { experienceId: string }) {
  return (
    <ExperienceWidget
      experienceId={experienceId}
      autoStart
      onGranted={async (admissionGrant) => {
        await fetch("/api/fanfare/admission", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ admissionGrant }),
        });

        window.location.assign("/checkout");
      }}
    />
  );
}

Step 2: Show Checkout Call-to-Action

For custom React UI, branch on the public JourneyView and read the grant from the granted sequence:
import { useExperienceJourney } from "@fanfare-io/fanfare-sdk-react";

function CustomCheckoutCTA({ experienceId }: { experienceId: string }) {
  const { view, start, error } = useExperienceJourney(experienceId, { autoStart: true });

  if (error) return <ErrorPanel message={error} />;
  if (!view || view.journeyStage === "routing") return <LoadingPanel />;
  if (view.journeyStage === "ready") return <button onClick={() => void start()}>Enter</button>;
  if (view.journeyStage === "gated") return <GatePanel view={view} />;

  if (view.sequence.phase === "granted") {
    return (
      <CheckoutButton
        expiresAt={view.sequence.grant.expiresAt}
        onClick={() => sendAdmissionGrant(view.sequence.grant.token)}
      />
    );
  }

  return <ExperienceState sequence={view.sequence} />;
}

Step 3: Pass the Grant to Checkout

Send the grant to a trusted server or checkout boundary. Keep the grant out of URLs, screenshots, debug logs, and third-party analytics.

Server Handoff

async function sendAdmissionGrant(admissionGrant: string) {
  const response = await fetch("/api/fanfare/admission", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ admissionGrant }),
  });

  if (!response.ok) {
    throw new Error("Unable to prepare checkout");
  }

  window.location.assign("/checkout");
}

Step 4: Validate Admission Server-Side

Before allowing checkout actions, validate the admission grant with Fanfare from your server:
// Your checkout API endpoint
app.post("/api/checkout", async (req, res) => {
  const { admissionGrant, cart } = req.body;

  // 1. Validate admission with Fanfare
  const isValid = await validateAdmissionGrant({ admissionGrant });

  if (!isValid) {
    return res.status(403).json({
      error: "Invalid admission",
      code: "ADMISSION_INVALID",
      message: "Your access has expired or is invalid. Please try again.",
    });
  }

  // 2. Process the order
  const order = await createOrder(cart);

  // 3. Record the successful checkout against the admission
  await recordAdmissionCheckout({ admissionGrant, orderId: order.id });

  res.json({ orderId: order.id });
});

Step 5: Complete the Admission

After a successful order, record completion through your Fanfare checkout integration:
async function recordAdmissionCheckout(params: { admissionGrant: string; orderId: string }) {
  await notifyFanfareCheckoutComplete(params);
}

Integration Patterns

Pattern 1: Same-Site Checkout

When checkout is on the same domain:
function handleAdmitted(admissionGrant: string) {
  return sendAdmissionGrant(admissionGrant);
}

Pattern 2: External Checkout (Shopify, etc.)

When checkout is on a different domain:
async function handleAdmitted(admissionGrant: string) {
  const { checkoutUrl } = await createExternalCheckoutSession({
    admissionGrant,
    cartId: getCurrentCartId(),
  });

  window.location.assign(checkoutUrl);
}

Pattern 3: Embedded Checkout

When checkout happens in an iframe/modal:
function EmbeddedCheckout({ checkoutSessionId, onComplete }: Props) {
  const iframeRef = useRef<HTMLIFrameElement>(null);

  useEffect(() => {
    // Listen for checkout completion message
    const handleMessage = (event: MessageEvent) => {
      if (event.data.type === "checkout-complete") {
        onComplete(event.data.orderId);
      }
    };

    window.addEventListener("message", handleMessage);
    return () => window.removeEventListener("message", handleMessage);
  }, [onComplete]);

  return <iframe ref={iframeRef} src={`/checkout/embedded/${checkoutSessionId}`} className="checkout-iframe" />;
}

Handling Edge Cases

Grant Expiration During Checkout

function CheckoutForm({ expiresAt }: Props) {
  const [isExpired, setIsExpired] = useState(Date.now() > expiresAt);

  useEffect(() => {
    if (isExpired) return;

    const timeout = setTimeout(() => {
      setIsExpired(true);
    }, expiresAt - Date.now());

    return () => clearTimeout(timeout);
  }, [expiresAt, isExpired]);

  if (isExpired) {
    return (
      <div className="checkout-expired">
        <h2>Session Expired</h2>
        <p>Your checkout session has expired.</p>
        <a href="/experience">Return to Experience</a>
      </div>
    );
  }

  return <YourCheckoutForm />;
}

Network Errors

async function submitOrder(orderData: OrderData) {
  try {
    const response = await fetch("/api/checkout", {
      method: "POST",
      body: JSON.stringify(orderData),
    });

    if (!response.ok) {
      const error = await response.json();

      if (error.code === "ADMISSION_INVALID") {
        // Grant is invalid or expired
        showError("Your access has expired. Please rejoin the experience.");
        redirectToExperience();
        return;
      }

      throw new Error(error.message);
    }

    return response.json();
  } catch (error) {
    // Network error - allow retry
    showError("Connection error. Please try again.");
  }
}

Page Refresh

function CheckoutPage() {
  const [checkoutSession, setCheckoutSession] = useState<CheckoutSession | null>(null);

  useEffect(() => {
    async function loadCheckoutSession() {
      const session = await getCheckoutSession();
      if (!session) {
        window.location.assign("/experience");
        return;
      }
      setCheckoutSession(session);
    }
    loadCheckoutSession();
  }, []);

  if (!checkoutSession) {
    return <LoadingSpinner />;
  }

  return <CheckoutForm checkoutSession={checkoutSession} />;
}

Checkout Success/Failure Handling

Success Flow

async function handleCheckoutSuccess(orderId: string, admissionGrant: string) {
  // 1. Record admission completion
  await recordAdmissionCheckout({ orderId, admissionGrant });

  // 2. Track success
  analytics.track("checkout_complete", { orderId });

  // 3. Show confirmation
  navigate(`/order-confirmation/${orderId}`);
}

Failure Flow

async function handleCheckoutFailure(error: Error) {
  // Don't clear admission - allow retry
  console.error("Checkout failed:", error);

  // Track failure
  analytics.track("checkout_failed", { error: error.message });

  // Show error - user can retry
  showError("Checkout failed. Please try again.");
}

Best Practices

1. Show Clear Timing

Always display remaining time prominently:
<div className="checkout-timer">
  <span>{formatTime(timeRemaining)}</span>
  <span>to complete checkout</span>
</div>

2. Prevent Accidental Navigation

useEffect(() => {
  const handleBeforeUnload = (e: BeforeUnloadEvent) => {
    if (hasCheckoutSession && !orderComplete) {
      e.preventDefault();
      e.returnValue = "You have an active checkout session. Are you sure you want to leave?";
    }
  };

  window.addEventListener("beforeunload", handleBeforeUnload);
  return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [hasCheckoutSession, orderComplete]);

3. Validate Early

Validate the admission grant as soon as the checkout page loads, not just on submit:
useEffect(() => {
  async function validateOnLoad() {
    const isValid = await validateAdmissionGrant({ admissionGrant });
    if (!isValid) {
      redirectToExperience();
    }
  }
  validateOnLoad();
}, []);

What’s Next