# Run a funnel

A funnel is a sequence of offers you set up in Subbly. Each offer is a
**step**: a headline, a description, and one or more variants or plans, often
at a discount. The Subbly.js SDK gives you the next step for a cart; you draw
it, and the customer takes it or skips it.

## The three phases

A funnel has three phases, and each phase has its own set of methods:

- **Pre-purchase** offers come before checkout, while the customer still edits
  the cart.
- **Mid-purchase** offers come during checkout.
- **Post-purchase** offers come after the customer pays. They run only after
  `subbly.checkout.purchase` succeeds, on the thank-you page.

## The loop

Every phase works the same way:

1. Fetch the next step for the cart with the phase's `Fetch` method. The
   response is `{ step }`.
2. If `step` is `null`, the customer has seen every offer. Stop.
3. Show the offer: `step.title`, `step.description`, and the variants and plans
   in `step.offer.products`. `step.offer.type` says how many the customer may
   take: `single`, `multiple` or `one_click`.
4. Call the phase's `Accept` method with `step.id` and the `productId` of each
   product taken, or the phase's `Reject` method with `step.id` to skip.
5. Fetch again.

Accepting a pre-purchase or a mid-purchase offer adds the products to the cart
and returns a plain cart, not the `subbly.cart` model. `subbly.cart` still holds
the old contents, so call `subbly.cart.load()` afterwards.

Accepting a post-purchase offer charges a new purchase on the card the customer
already used. It returns the invoice, the subscription and the orders it
created, not a cart.

The pre-purchase phase also has `prePurchaseRefresh`, which clears what the
customer accepted or skipped so the sequence starts again from the first step.

## Pre-purchase

Run this loop on the cart page, or wherever the customer reviews the cart before
checkout.

```js title="Offer before checkout"
async function runPrePurchase() {
  const cartId = subbly.cart.id

  while (true) {
    const { step } = await subbly.funnels.prePurchaseFetch(cartId, {
      expand: ['parent']
    })
    if (!step) break

    const productId = await showOffer(step) // resolves with an id, or null

    if (productId) {
      await subbly.funnels.prePurchaseAccept(cartId, step.id, {
        offers: [{ productId }]
      })
      await subbly.cart.load()
    } else {
      await subbly.funnels.prePurchaseReject(cartId, step.id)
    }
  }
}
```

`showOffer` is your own function. It draws the step and resolves with the `id`
of the product the customer picked from `step.offer.products`, or `null` when
they skip. With `expand: ['parent']`, each product also carries its parent
product, so you can show its name and images.

## Mid-purchase

Run the same loop during checkout, with the `midPurchase` methods. The payload
and the return value are the same as in the pre-purchase phase, so reload
`subbly.cart` after each accept here too.

```js title="Offer during checkout"
async function runMidPurchase() {
  const cartId = subbly.cart.id

  while (true) {
    const { step } = await subbly.funnels.midPurchaseFetch(cartId)
    if (!step) break

    const productId = await showOffer(step)

    if (productId) {
      await subbly.funnels.midPurchaseAccept(cartId, step.id, {
        offers: [{ productId }]
      })
      await subbly.cart.load()
    } else {
      await subbly.funnels.midPurchaseReject(cartId, step.id)
    }
  }
}
```

## Post-purchase

Run this loop after `subbly.checkout.purchase` resolves. Pass the ID of the
cart that was paid. The accept needs a signed-in customer, and each accepted
offer becomes its own purchase, so the result carries its own `invoice`,
`subscription` and `orders`.

When the checkout did not set a shipping address or a shipping method, send
`shippingAddressId` and `shippingMethodId` in the payload. When the charge asks
for 3-D Secure, the call rejects with `payment_requires_action`; finish the
verification with the [payment intent methods](/reference/subbly-sdk/payment-intents),
then call the accept again with `paymentIntentId`.

```js title="Offer after checkout"
async function runPostPurchase(cartId) {
  while (true) {
    const { step } = await subbly.funnels.postPurchaseFetch(cartId)
    if (!step) break

    const productId = await showOffer(step)

    if (productId) {
      const { invoice, subscription, orders } =
        await subbly.funnels.postPurchaseAccept(cartId, step.id, {
          offers: [{ productId }]
        })
      showReceipt(invoice, subscription, orders)
    } else {
      await subbly.funnels.postPurchaseReject(cartId, step.id)
    }
  }
}

const cartId = subbly.cart.id
await subbly.checkout.purchase(cartId)
await runPostPurchase(cartId)
```

## Next steps

- [Funnels reference](/reference/subbly-sdk/funnels) — every method, with the
  fields of a step and of the accept payloads.
- [Cart reference](/reference/subbly-sdk/cart) — `subbly.cart.load` and the
  other cart methods.
- [Checkout reference](/reference/subbly-sdk/checkout) — `subbly.checkout.purchase`,
  which the post-purchase phase follows.
