> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fanfare.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Shopify SDK Quickstart

> Gate products, connect the drop widget, and run a leak-safe gated checkout from a Shopify storefront.

The Shopify SDK is the storefront adapter for Fanfare. You configure one provider with your store
facts; it assembles the product→experience resolver, the checkout verifier, and the claim/checkout
plumbing internally. The hooks then cover the storefront seams: which product carries which
experience, proving a shopper cleared the drop before checkout, and running the on-buy gated
checkout in the correct order.

For the higher-level walkthrough (installing the Fanfare Shopify app, embedding experiences in your
theme, connecting products), read the [Shopify integration guide](/guides/platform-integrations/shopify)
first. This page covers the SDK package itself.

## Install

```bash theme={null}
pnpm add @fanfare-io/fanfare-sdk-shopify @fanfare-io/fanfare-sdk-core
```

`@fanfare-io/fanfare-sdk-core` is a required peer dependency. `react` and `react-dom` are optional
peers — install them only if you use the `./react` subpath.

```bash theme={null}
# Only if you use the React provider/hooks
pnpm add react react-dom
```

## Two import surfaces

The package has two entry points so a server-only consumer can install it without React.

| Import                                  | When to use                                                                                                                                                                                                                                         | Carries React |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `@fanfare-io/fanfare-sdk-shopify`       | The SSR-safe root barrel: metafield primitives, config types, and the framework-free checkout vocabulary (`GatedLine`, `productGid`/`variantGid`, `FanfareGatedCheckoutResult`). Use from Hydrogen/Oxygen loaders, RSC, and Next/Remix server code. | No            |
| `@fanfare-io/fanfare-sdk-shopify/react` | The `"use client"` provider and hooks. Use in client components.                                                                                                                                                                                    | Yes           |

The resolver, verifier, and claimer are **internal** — `FanfareShopifyProvider` assembles them from
your store config, so you configure one provider rather than wiring loose factories.

## Configure the provider

Wrap your storefront once. In production, `shopify.proxyBaseUrl` points at the Fanfare Shopify app
proxy; that is the only required store fact.

```tsx theme={null}
"use client";

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

export function StorefrontProvider({ children }: { children: React.ReactNode }) {
  return (
    <FanfareShopifyProvider
      organizationId="org_abc123"
      publishableKey="pk_live_..."
      shopify={{ proxyBaseUrl: "https://shop.myshopify.com/apps/fanfare/api" }}
    >
      {children}
    </FanfareShopifyProvider>
  );
}
```

`mode` defaults to `"production"` (real verify + claim via the proxy) and `experiences` defaults to
`{ source: "metafield" }`. Production without a `proxyBaseUrl` throws `FanfareConfigurationError` at
mount — it never silently degrades.

## Resolve a product to an experience

`useFanfareExperience(product)` maps a Shopify product to the experience it gates, or `null` when the
product is not gated. Pass a `ShopifyProductRef` (`{ handle, id }`).

```tsx theme={null}
import { useFanfareExperience } from "@fanfare-io/fanfare-sdk-shopify/react";

function ProductPage({ product }: { product: { handle: string; id: string } }) {
  const { experienceId, loading } = useFanfareExperience({ handle: product.handle, id: product.id });

  if (loading) return <Spinner />;
  if (experienceId === null) return <NativePurchaseUI />; // not gated
  return <GatedPurchaseUI experienceId={experienceId} />;
}
```

`experienceId === null` means *not gated* — render your native purchase UI. It is SSR-safe: any miss
or failure yields `null`, never a throw.

### Metafield primitives

If you already have the raw `experienceIds` value, parse it directly. Both helpers are pure and never
throw.

```ts theme={null}
import { parseExperienceIds, parseFirstExperienceId, FANFARE_PRODUCT_METAFIELD } from "@fanfare-io/fanfare-sdk-shopify";

parseExperienceIds('["exp_123","exp_456"]'); // ["exp_123", "exp_456"]
parseFirstExperienceId('["exp_123"]'); // "exp_123"
FANFARE_PRODUCT_METAFIELD.key; // "experienceIds"
```

## Connect the drop widget

`useFanfareWidgetBridge()` returns the `onJourneyChange` handler you spread onto your
`<ExperienceWidget>`. That spread feeds the cleared-drop grant and the routed journey snapshot into
the provider, so the gate and the checkout can read them.

```tsx theme={null}
import { useFanfareWidgetBridge } from "@fanfare-io/fanfare-sdk-shopify/react";

function GatedPurchaseUI({ experienceId }: { experienceId: string }) {
  const bridge = useFanfareWidgetBridge();
  return <ExperienceWidget experienceId={experienceId} {...bridge} />;
}
```

## Gate checkout entry

`<FanfareCheckoutGate>` is the drop-in guard for the checkout route: it renders `children` only when
the grant for the cart's gated items is valid, otherwise `onDenied`.

```tsx theme={null}
import { FanfareCheckoutGate } from "@fanfare-io/fanfare-sdk-shopify/react";

<FanfareCheckoutGate
  cartItems={cart.lines.map((line) => ({ handle: line.product.handle, id: line.product.id }))}
  onDenied={(reason) => <BackToDropPrompt reason={reason} />}
>
  <Checkout />
</FanfareCheckoutGate>;
```

For manual control, `useFanfareGrant()` returns `{ state: "checking" | "allowed" | "blocked", reason? }`.

## Run the gated checkout (recommended)

`useFanfareCheckout(adapter)` is the recommended on-buy path. Its `gatedCheckout` owns the ordering
that keeps Shopify's gate validation happy — create an **empty** cart, claim it (mint the gate token
and reserve the slot), then add the line(s) to that same cart. You supply the cart I/O; the SDK owns
the order, so the protected line can never be added before the claim.

```tsx theme={null}
import { useFanfareCheckout, productGid, variantGid } from "@fanfare-io/fanfare-sdk-shopify/react";

function BuyButton({ product, variant }: { product: { id: string }; variant: { id: string } }) {
  const { gatedCheckout, pending } = useFanfareCheckout({
    // createCart MUST return an EMPTY cart (no line) — Shopify's cartCreate with no input.
    createCart: () => storefront.cartCreate(),
    // addLine adds one line; merchandiseId is the variant GID.
    addLine: (cart, line) => storefront.cartLinesAdd(cart.id, line.merchandiseId, line.quantity),
    readCart: (cart) => ({ id: cart.id, checkoutUrl: cart.checkoutUrl }),
  });

  async function onBuy() {
    const res = await gatedCheckout({
      gatedLine: {
        shopifyProductId: productGid(product.id), // gid://shopify/Product/...
        merchandiseId: variantGid(variant.id), // gid://shopify/ProductVariant/...
        quantity: 1,
      },
    });

    if (res.ok) {
      window.location.assign(res.checkoutUrl);
    } else if (res.retryable && res.code === "add_line_failed") {
      // The token is already minted and the slot reserved — resume the add on the
      // SAME cart. NEVER start a fresh gatedCheckout here (that would double-reserve).
      const retried = await res.retry?.();
      if (retried?.ok) window.location.assign(retried.checkoutUrl);
    } else {
      showError(res.code);
    }
  }

  return <button disabled={pending} onClick={onBuy}>Buy</button>;
}
```

### The line identifier

`GatedLine` carries **two** Shopify ids, both required, branded so a transposition fails to compile:

| Field              | What                                                              | Why                                     |
| ------------------ | ----------------------------------------------------------------- | --------------------------------------- |
| `shopifyProductId` | Shopify **Product** GID (`gid://shopify/Product/…`)               | The gate's policy anchor                |
| `merchandiseId`    | Shopify **ProductVariant** GID (`gid://shopify/ProductVariant/…`) | Shopify's `cartLinesAdd` merchandise id |

Build them with the `productGid` / `variantGid` constructors (they throw if handed the opposite
entity's GID).

### Result codes

`gatedCheckout` never rejects — every outcome is a `FanfareGatedCheckoutResult`. Branch on
`retryable` (or the presence of `retry`), never on `code` alone.

| `code`                  | `retryable` | Safe to start a fresh `gatedCheckout`? | What to do                          |
| ----------------------- | ----------- | -------------------------------------- | ----------------------------------- |
| `cart_create_failed`    | yes         | yes (nothing claimed)                  | restart                             |
| `no_claim_context`      | no          | n/a                                    | wire the widget grant first         |
| `cart_not_materialized` | yes         | yes                                    | restart                             |
| `claim_failed`          | yes         | yes (nothing reserved)                 | restart                             |
| `add_line_failed`       | **yes**     | **NO — resume only**                   | `res.retry()` (same claimed cart)   |
| `reservation_expired`   | no          | yes (must restart)                     | fresh `gatedCheckout`               |
| `cart_id_mismatch`      | no          | n/a                                    | adapter bug — surface               |
| `checkout_url_missing`  | no          | n/a                                    | adapter bug — surface               |
| `busy`                  | yes         | n/a                                    | disable the buy button on `pending` |

<Warning>
  **Multiple lines are forward-compatible by shape only.** `gatedCheckout({ gatedLine, ungatedLines })`
  accepts extra lines and adds them to the cart, but the gate token corresponds to a **single**
  product/experience — only `gatedLine` is policy-checked. `ungatedLines` are convenience adds, not
  gated. Multi-product enforcement is future work.
</Warning>

## Advanced: bring-your-own-cart claim

If you own your cart system and want to add lines yourself, use the low-level `claim` (also returned
by `useFanfareCheckout`, and available standalone as `useFanfareClaim`). You materialize an empty
cart, `claim` it, then add your gated line — in that order.

```tsx theme={null}
import { useFanfareClaim } from "@fanfare-io/fanfare-sdk-shopify/react";

const { claim } = useFanfareClaim();

let cart = await createEmptyCart(); // you own this
const res = await claim({
  cartId: cart.id,
  purchase: { shopifyProductId: productGid(product.id), merchandiseId: variantGid(variant.id), quantity: 1 },
});
if (res.ok) {
  const checkout = await addGatedLine(cart.id, variant.id); // YOU add the line, after the claim
  window.location.assign(checkout.checkoutUrl);
}
```

`claim(cartId)` (a bare string) stays as the no-purchase-context escape hatch. On
`cart_not_materialized` (recoverable), re-materialize the cart and claim again.

## Notes and gotchas

* **The root barrel is React-free and SSR-safe.** It imports zero React, so a server-only consumer
  installs and uses it (the metafield primitives + the checkout vocabulary) without React.
* **`gatedCheckout` owns the ordering.** Your `createCart` must return an EMPTY cart; the SDK adds the
  gated line only after the claim writes the token. Adding the line inside `createCart` reintroduces
  the line-before-token bug the hook exists to prevent.
* **Never re-claim on `add_line_failed`.** The slot is already reserved; use `res.retry()` to resume
  the add on the same cart. A blind fresh `gatedCheckout` double-reserves (oversell).
* **The gate is enforced server-side.** `GrantRecord` is UX/continuity state; admission is enforced
  through the proxy using the admission credential, and the platform gate token is server-minted into
  the cart metafield.

## Next steps

* Follow the [Shopify integration guide](/guides/platform-integrations/shopify) for app installation and theme embedding.
* Read the [Shopify types reference](/sdk/reference/types) for the full exported type list.
* See [Core SDK Quickstart](/sdk/core/quickstart) for the journey model the resolver feeds from.
