types/global.tstypescript
/**
* Global type definitions for the SDK
*/
import type { Decimal } from 'decimal.js';
import type { Offer } from './campaign';
import type { Order, PaymentMethod } from './api';
/**
* The complete catalog of events the SDK emits, mapped to their payload shape.
*
* Each key is an event name you can subscribe to (`sdk.on('cart:updated', …)`),
* and its value is the object your handler receives. Use this as the lookup for
* "what fields does *this* event give me?" — the narrative guide to subscribing
* lives in [the JavaScript API reference](../core/guide/reference/javascript-api.md).
* @category Events
*/
export interface EventMap {
/**
* The cart changed and its totals were recalculated. Fires after every add,
* remove, quantity change, swap, coupon, and shipping change — so it is the one
* event to bind a cart display to, rather than subscribing to each of those.
*
* The payload is the whole cart, not a delta.
*/
'cart:updated': CartState;
/**
* A package was successfully added to the cart.
*
* @example
* ```json
* {
* "packageId": 42,
* "quantity": 1,
* "source": "selector"
* }
* ```
*/
'cart:item-added': {
/** The `ref_id` of the package that was added. */
packageId: number;
/** How many units were added. */
quantity?: number;
/**
* Where the package came from: `selector` when a linked selector supplied
* it, `direct` when it came from the button's own `data-next-package-id`.
*/
source?: string;
};
/**
* A line was removed from the cart — by a remove button, or by a quantity
* control dropping it to zero.
*
* @example
* ```json
* { "packageId": 42 }
* ```
*/
'cart:item-removed': {
/** The package whose line was removed. */
packageId: number;
};
/**
* A cart line's quantity changed. Fired after the write succeeds, so the cart
* store already reflects the new value. Not fired when the requested quantity
* equals the current one.
*
* @example
* ```json
* {
* "packageId": 42,
* "quantity": 3,
* "oldQuantity": 2
* }
* ```
*/
'cart:quantity-changed': {
/** The package whose quantity changed. */
packageId: number;
/** The new quantity. `0` means the line was removed from the cart. */
quantity: number;
/** The quantity before the change. */
oldQuantity: number;
};
/**
* One package replaced another in a single operation, keeping the cart at one
* line instead of removing and adding — which is what a `swap`-mode selector
* does when the visitor picks a different card.
*
* @example
* ```json
* {
* "previousPackageId": 101,
* "newPackageId": 102,
* "priceDifference": 10,
* "source": "selector"
* }
* ```
*/
'cart:package-swapped': {
/** The package that was in the cart before the swap. */
previousPackageId: number;
/** The package now in the cart. */
newPackageId: number;
/** The full line as it was before the swap, when it could be resolved. */
previousItem?: CartItem;
/** The full line as it is after the swap. */
newItem?: CartItem;
/** New price minus old price. Negative when the visitor traded down. */
priceDifference: number;
/** What triggered the swap, e.g. `selector`. */
source?: string;
};
/**
* Campaign data — packages, currency, and settings — finished loading.
*
* @deprecated Declared and subscribed to, but never emitted by this build. A
* handler registered for it will not fire; read
* `useCampaignStore.getState().data` instead.
*/
'campaign:loaded': Campaign;
/**
* The visitor submitted the checkout form and the order request is about to go
* out. Fires before the payment call, so the order does not exist yet.
*/
'checkout:started': CheckoutData;
/** The checkout form finished wiring up its fields, validation, and payment. */
'checkout:form-initialized': {
/** The form element that was initialized. */
form: HTMLFormElement;
};
/** The Spreedly card iframe is ready to accept card details. */
'checkout:spreedly-ready': {};
/**
* An express checkout flow started.
*
* @deprecated Declared but never emitted by this build. Use
* `express-checkout:initialized`, which is the event the express container
* actually fires.
*/
'checkout:express-started': { method: 'paypal' | 'apple_pay' | 'google_pay' };
/**
* The page is holding a finished order: it was opened with `?ref_id=` — a
* success, receipt or upsell page — and the order came back from the orders API.
*
* This is the event to hang purchase tracking on, and the only one the SDK's own
* `dl_purchase` comes from — once per order, see the
* [analytics events reference](../core/guide/reference/analytics-events.md).
* The order it carries was fetched back from the API, so it is a real order with
* a real order number, and for every payment method that finishes at a gateway
* it is the first point at which the SDK knows the shopper actually paid.
*
* **The checkout page does not fire this.** Creating an order is not completing
* one: an express checkout (PayPal, Apple Pay, Google Pay) and a card payment
* needing 3-D Secure both leave the checkout page with the money still unmoved,
* the order carrying a `payment_complete_url` and the shopper redirected to the
* gateway to finish paying. Reporting a purchase there is issue #71, so nothing
* is emitted until the shopper lands back on a page that fetches the order. If
* you need to act the moment an order is *created*, do it in your own submit
* handler — the SDK has no event for it.
*
* Fires on a fresh fetch only. A reload inside the order store's 15-minute
* window is served from its cache and emits nothing.
*
* **It fires on a failed payment's landing page too.** A redirect payment comes
* back to either `success_url` or `payment_failed_url` and the platform puts
* `?ref_id=` on both, so the order loads either way. The SDK's own
* `dl_purchase` tells the two apart; a listener of your own must check for
* itself — an order still carrying `payment_complete_url` was never paid.
*
* @example
* ```ts
* window.next.on('order:completed', order => {
* console.log('Thank you for order', order.number);
* });
* ```
*/
'order:completed': Order;
/**
* The order succeeded but carried no redirect URL, so the SDK could not send
* the visitor onward. Handle this to avoid stranding them on the checkout page.
*/
'order:redirect-missing': {
/** The created order, as returned by the API. */
order: any;
};
/**
* Something failed. Emitted both by any feature's error handler and by the
* central error handler, so one subscriber can watch the whole SDK.
*/
'error:occurred': ErrorData;
/**
* The requested currency was not available for this campaign, so prices are
* shown in another one. Surface this — otherwise the visitor sees prices in a
* currency they did not ask for, with no explanation.
*
* @example
* ```json
* {
* "requested": "CAD",
* "actual": "USD",
* "reason": "api"
* }
* ```
*/
'currency:fallback': {
/** The currency that was asked for. */
requested: string;
/** The currency actually being used. */
actual: string;
/**
* Where the fallback was decided: `cached` when a stored campaign supplied
* it, `api` when the campaign response did.
*/
reason: 'cached' | 'api';
};
/**
* A countdown timer reached zero.
*
* @example
* ```json
* { "persistenceId": "flash-sale" }
* ```
*/
'timer:expired': {
/**
* The timer's persistence id — the key its deadline is stored under, so the
* countdown survives a reload. Identifies which timer expired.
*/
persistenceId: string;
};
/**
* SDK configuration changed at runtime.
*
* @deprecated Declared but never emitted by this build. Read
* `useConfigStore.getState()` instead.
*/
'config:updated': ConfigState;
/**
* A discount code was accepted and applied to the cart. The payload carries the
* full coupon when the SDK has it, and only the code when it does not.
*
* @example
* ```json
* { "code": "SAVE10" }
* ```
*/
'coupon:applied': { coupon: AppliedCoupon } | { code: string };
/** A previously applied discount code was taken off the cart. */
'coupon:removed': {
/** The code that was removed. */
code: string;
};
/**
* A discount code was rejected — unknown, expired, or not valid for this cart.
*
* @example
* ```json
* {
* "code": "SAVE10",
* "message": "This code has expired."
* }
* ```
*/
'coupon:validation-failed': {
/** The code the visitor entered. */
code: string;
/** The reason, already worded for display to the visitor. */
message: string;
};
/**
* A visitor clicked a card in a selector. Fires after the selection state is
* updated but before the cart write completes in `swap` mode.
*
* @example
* ```json
* {
* "selectorId": "main-selector",
* "packageId": 102,
* "previousPackageId": 101,
* "mode": "select",
* "pendingAction": true
* }
* ```
*/
'selector:item-selected': {
/** The selector that fired this, matching its `data-next-selector-id`. */
selectorId: string;
/** The package on the card the visitor clicked. */
packageId: number;
/** The previously selected package, or `undefined` if nothing was selected. */
previousPackageId: number | undefined;
/** The selector's mode at click time: `swap` or `select`. */
mode: string;
/**
* `true` in `select` mode, signalling that an external button still has to
* perform the cart write.
*/
pendingAction: boolean | undefined;
/** The full selected item, when the selector could resolve it. */
item?: SelectorItem;
};
/**
* A selector finished the cart write for a pending selection.
*
* @deprecated Declared but never emitted by this build. Listen for
* `cart:item-added` or `cart:package-swapped` to know the write landed.
*/
'selector:action-completed': {
selectorId: string;
packageId: number;
previousPackageId: number | undefined;
mode: string;
};
/**
* The active selection changed. Fires on every selection update — including
* programmatic ones, such as cart sync auto-selecting an already-in-cart
* package on load — not only on visitor clicks.
*
* @example
* ```json
* {
* "selectorId": "main-selector",
* "packageId": 101,
* "quantity": 1
* }
* ```
*/
'selector:selection-changed': {
/** The selector that fired this. */
selectorId: string;
/** The newly selected package. */
packageId?: number;
/** The quantity currently set on the selected card. */
quantity?: number;
/** The full selected item, when the selector could resolve it. */
item?: SelectorItem;
};
/**
* A quantity stepper inside a selector card changed that card's quantity. This
* is the card's own quantity, not a cart line — in `select` mode nothing has
* been written to the cart yet.
*
* @example
* ```json
* {
* "selectorId": "main-selector",
* "packageId": 101,
* "quantity": 2
* }
* ```
*/
'selector:quantity-changed': {
/** The selector that fired this. */
selectorId: string;
/** The card whose quantity changed. */
packageId: number;
/** The new quantity on that card. */
quantity: number;
};
/**
* A shipping method was picked in the UI.
*
* @deprecated Declared but never emitted by this build. Use
* `shipping:method-changed`, which the cart fires once the method is applied.
*/
'shipping:method-selected': { shippingId: string; selectorId: string };
/**
* The cart's shipping method changed and totals were recalculated.
*
* @example
* ```json
* { "methodId": 5 }
* ```
*/
'shipping:method-changed': {
/** The shipping method now applied to the cart. */
methodId: number;
/** The full shipping method record, including its price and label. */
method: any;
};
// Action Events
/**
* An action feature — an add-to-cart or accept-upsell button — completed its
* work without throwing. Fires for every such button, so check `action` to tell
* which one.
*
* @example
* ```json
* { "action": "AddToCartEnhancer" }
* ```
*/
'action:success': {
/** Class name of the feature that ran, e.g. `AddToCartEnhancer`. */
action: string;
/** Extra context, including `element`: the button that was clicked. */
data?: any;
};
/** An action feature threw while running. The button is re-enabled either way. */
'action:failed': {
/** Class name of the feature that failed, e.g. `AddToCartEnhancer`. */
action: string;
/** The error that was thrown. */
error: Error;
};
// Upsell Events
/**
* The visitor accepted a post-purchase upsell and it was added to the existing
* order. This is the event post-purchase revenue tracking should use — the
* money is additional to the original `order:completed` value.
*
* @example
* ```json
* {
* "packageId": 77,
* "quantity": 1,
* "orderId": "abc123",
* "value": 29.99,
* "discount": 0
* }
* ```
*/
'upsell:accepted': {
/** The upsell package that was added. */
packageId: number;
/** How many units were added. */
quantity: number;
/** The order the upsell was attached to. */
orderId: string;
/** Item revenue for the accepted line(s), after discounts (post-discount). */
value?: number;
/** Total discount applied to the accepted line(s) (pre-discount − value). */
discount?: number;
/** Voucher/coupon code applied to the order, when present. */
coupon?: string;
};
/** A card in an upsell offer's built-in selector was chosen. */
'upsell-selector:item-selected': {
/** The upsell selector that fired this. */
selectorId: string;
/** The package on the chosen card. */
packageId: number;
};
/** The quantity on an upsell offer changed before the visitor accepted it. */
'upsell:quantity-changed': {
/**
* The upsell selector the offer belongs to. Present whenever the offer has
* one — whichever control changed the quantity — and absent in direct mode.
* Before 2026-07-31 a quantity-toggle press sent the key with `undefined`,
* so treat "key present but undefined" as direct mode too if you handle
* events from an older SDK build.
*/
selectorId?: string | undefined;
/** The new quantity. */
quantity: number;
/** The package whose quantity changed, when it is known. */
packageId?: number | undefined;
};
/** An option was chosen inside an upsell offer — a variant or a tier. */
'upsell:option-selected': {
/** The upsell selector that fired this. */
selectorId: string;
/** The package behind the chosen option. */
packageId: number;
};
// Message Events
/**
* A message was shown to the visitor.
*
* @deprecated Declared but never emitted by this build.
*/
'message:displayed': { message: string; type: string };
// Payment Events
/**
* Card details were exchanged for a payment token. The raw card number never
* reaches SDK code — only this token does.
*/
'payment:tokenized': {
/** The payment token to submit with the order. */
token: string;
/** Payment method metadata returned with the token, e.g. card brand and last four. */
pmData: any;
/** Which payment method produced the token. */
paymentMethod: string;
};
/**
* Payment failed. Fires both for card-field errors before submission and for a
* declined order attempt.
*
* @example
* ```json
* { "message": "Your card was declined.", "code": "gateway_declined" }
* ```
*/
'payment:error': {
/**
* The failure, already worded for display to the visitor. When the payment
* form reported several problems at once they arrive joined into this one
* string, in the order the form reported them.
*/
message: string;
/** The gateway's response code, when the failure came back from an order attempt. */
code?: string;
/** The raw error response, for logging. Absent for card-field errors. */
details?: unknown;
};
/**
* An express checkout attempt finished.
*
* @deprecated Declared but never emitted by this build. Listen for
* `order:completed`, which fires on the page the shopper lands on afterwards,
* for express and standard checkout alike.
*/
'checkout:express-completed': { method: string; success: boolean };
/**
* An express checkout attempt failed.
*
* @deprecated Declared but never emitted by this build. Listen for
* `payment:error` or `error:occurred`.
*/
'checkout:express-failed': { method: string; error: string };
// Express Checkout Events
/**
* An express payment button (PayPal, Apple Pay, Google Pay or Link) rendered
* and is ready to click. Fires once per available method, so a page offering
* all four sees it four times.
*/
'express-checkout:initialized': {
/** Which express method became available. */
method: 'paypal' | 'apple_pay' | 'google_pay' | 'link';
/** The container the button was rendered into. */
element: HTMLElement;
};
/**
* An express payment method failed to set up.
*
* @deprecated Declared but never emitted by this build. Listen for
* `error:occurred`.
*/
'express-checkout:error': {
method: 'paypal' | 'apple_pay' | 'google_pay' | 'link';
error: string;
};
/**
* The visitor started an express checkout — the button was clicked and the SDK
* is about to ask the orders API for the order.
*
* @remarks
* `cartTotal` is declared but not sent: the emitting call passes `method` and
* `itemCount` only. Read the cart store for the total.
*/
'express-checkout:started': {
method: 'paypal' | 'apple_pay' | 'google_pay';
cartTotal: { value: number; formatted: string };
itemCount: number;
};
/**
* An express checkout failed — the order was refused, or the gateway's own
* fields rejected it. `payment:error` fires alongside it for the errors that
* carry payment details.
*/
'express-checkout:failed': {
method: 'paypal' | 'apple_pay' | 'google_pay';
error: string;
};
/**
* An express checkout produced an order — the orders API accepted it and the
* SDK is about to redirect the shopper to the payment gateway.
*
* **This is not a purchase.** The order it carries has a `payment_complete_url`
* and no money has moved: the shopper may still cancel at PayPal, or press
* back. Reporting a conversion here is
* [issue #71](https://github.com/NextCommerceCo/campaign-cart/issues/71) —
* hang purchase tracking on
* {@link EventMap."order:completed" | order:completed} instead, which fires on
* the success page for the finished order.
*
* @remarks
* The payload wraps the order: `{ method, order }`, not the order itself.
*/
'express-checkout:completed': {
method: 'paypal' | 'apple_pay' | 'google_pay';
order: any;
};
/**
* An express order carried no redirect URL.
*
* @deprecated Declared but never emitted by this build. Listen for
* `order:redirect-missing`.
*/
'express-checkout:redirect-missing': { order: any };
// Address Autocomplete Events
/**
* The visitor picked a suggested address and the form was filled from it. Fires
* for either autocomplete provider.
*
* @example
* ```json
* { "type": "shipping" }
* ```
*/
'address:autocomplete-filled': {
/** Which address block was filled. */
type: 'shipping' | 'billing';
/** The resolved address parts used to fill the fields. */
components: any;
};
/**
* Address fields were revealed.
*
* @deprecated Declared but never emitted by this build. Use
* `checkout:location-fields-shown`.
*/
'address:location-fields-shown': {};
/**
* The shipping address fields were revealed — the visitor moved past the
* collapsed autocomplete input, so state, city, and postcode are now on screen.
*
* Also dispatched as a DOM `CustomEvent` on `document`, for code that listens
* outside the SDK: `document.addEventListener('checkout:location-fields-shown', …)`.
*/
'checkout:location-fields-shown': {};
/** The billing address fields were revealed. Also dispatched as a DOM `CustomEvent`. */
'checkout:billing-location-fields-shown': {};
// Upsell Events
/** A post-purchase upsell offer was wired up and is on screen. */
'upsell:initialized': {
/** The package being offered. */
packageId: number;
/** The offer's container element. */
element: HTMLElement;
};
/** The upsell add request went out. Use it to show a pending state. */
'upsell:adding': {
/** The package being added. */
packageId: number;
};
/**
* The upsell was added to the order. Fires after the API confirms, and before
* any redirect to the next offer or the receipt.
*
* @example
* ```json
* {
* "packageId": 77,
* "quantity": 1,
* "value": 29.99,
* "willRedirect": true
* }
* ```
*/
'upsell:added': {
/** The package that was added. */
packageId: number;
/** How many units were added. */
quantity: number;
/** The updated order, as returned by the API. */
order: any;
/** Item revenue for the added line, after discounts. */
value?: number;
/**
* Whether the SDK is about to navigate away. When `true`, finish any tracking
* synchronously — a handler that awaits will not complete.
*/
willRedirect?: boolean;
};
/** Adding the upsell failed. The offer stays on screen so the visitor can retry. */
'upsell:error': {
/** The package that failed to add. */
packageId: number;
/** The failure reason. */
error: string;
};
// Accordion Events
/**
* An accordion section was toggled. Fires for both directions — read `isOpen`
* rather than subscribing to the separate opened/closed events.
*/
'accordion:toggled': {
/** The section's id. */
id: string;
/** `true` when the section is now open. */
isOpen: boolean;
/** The section element. */
element: HTMLElement;
};
/** An accordion section opened. */
'accordion:opened': {
/** The section's id. */
id: string;
/** The section element. */
element: HTMLElement;
};
/** An accordion section closed. */
'accordion:closed': {
/** The section's id. */
id: string;
/** The section element. */
element: HTMLElement;
};
/** The visitor declined a post-purchase upsell and moved on. */
'upsell:skipped': {
/** The package that was declined, when the offer identified one. */
packageId?: number;
/** The order the offer belonged to. */
orderId?: string;
};
/**
* A post-purchase upsell offer became visible to the visitor. Pair it with
* `upsell:accepted` and `upsell:skipped` to measure offer performance.
*/
'upsell:viewed': {
/** The package being offered. */
packageId?: number;
/** Path of the page the offer was shown on. */
pagePath?: string;
/** The order the offer belonged to. */
orderId?: string;
};
// Exit Intent Events (simplified)
/** The exit-intent popup was shown — the visitor's pointer left the viewport. */
'exit-intent:shown': {
/** Image the popup was rendered with, when it is image-based. */
imageUrl?: string;
/** Template the popup was rendered from, when it is template-based. */
template?: string;
};
/** The visitor clicked the exit-intent popup's content, rather than dismissing it. */
'exit-intent:clicked': {
/** Image the popup was rendered with. */
imageUrl?: string;
/** Template the popup was rendered from. */
template?: string;
};
/**
* The popup was dismissed — via the close button, the overlay, or the Escape
* key. Fires alongside `exit-intent:closed`.
*/
'exit-intent:dismissed': {
/** Image the popup was rendered with. */
imageUrl?: string;
/** Template the popup was rendered from. */
template?: string;
};
/** The popup was removed from the page, whatever the reason. */
'exit-intent:closed': {
/** Image the popup was rendered with. */
imageUrl?: string;
/** Template the popup was rendered from. */
template?: string;
};
/**
* The visitor took the popup's offer — usually accepting a discount code.
*
* @example
* ```json
* {
* "action": "accept",
* "couponCode": "STAY10"
* }
* ```
*/
'exit-intent:action': {
/** Which action was taken. */
action: string;
/** The code the popup applied, when the action carries one. */
couponCode?: string;
};
// SDK Events
/**
* The SDK finished reading campaign parameters off the page URL — currency,
* forced package, test mode, attribution. Anything that depends on those values
* should wait for this rather than reading them at load.
*/
'sdk:url-parameters-processed': {};
// Offer Events
/**
* An offer was selected.
*
* @deprecated Declared but never emitted by this build. Offers moved to a
* server-side model and the client-side offer features were removed.
*/
'offer:selected': { offerId: number };
/**
* An offer was applied to the cart.
*
* @deprecated Declared but never emitted by this build. Offers are applied
* server-side; read the discounts on `cart:updated` instead.
*/
'offer:applied': { offerId: number };
// Bundle Events
/**
* A bundle card was chosen. A bundle is several packages bought as one unit, so
* the payload carries every line the choice implies, not a single package.
*
* @example
* ```json
* {
* "selectorId": "main-bundle",
* "items": [
* { "packageId": 101, "quantity": 2 },
* { "packageId": 105, "quantity": 1 }
* ]
* }
* ```
*/
'bundle:selected': {
/** The bundle selector that fired this. */
selectorId: string;
/** Every package the chosen bundle puts in the cart, with quantities. */
items: { packageId: number; quantity: number }[];
};
/**
* The chosen bundle's contents changed — a different card, a variant swap, or a
* quantity bump. Fires on every change, including programmatic ones, so it is
* the event to bind a button or a price display to.
*/
'bundle:selection-changed': {
/** The bundle selector that fired this. */
selectorId: string;
/** The bundle's packages after the change. */
items: { packageId: number; quantity: number }[];
};
/**
* The bundle-level quantity stepper changed, multiplying every line in the
* bundle.
*
* @example
* ```json
* {
* "selectorId": "main-bundle",
* "bundleId": "starter",
* "quantity": 2,
* "items": [{ "packageId": 101, "quantity": 4 }]
* }
* ```
*/
'bundle:quantity-changed': {
/** The bundle selector that fired this. */
selectorId: string;
/** The bundle whose quantity changed. */
bundleId: string;
/** The new bundle multiplier. */
quantity: number;
/** The resulting package lines, with the multiplier already applied. */
items: { packageId: number; quantity: number }[];
};
/**
* Bundle prices finished loading and the price slots were filled.
*
* Delivered as a DOM `CustomEvent` on `document`, not through the event bus —
* subscribe with `document.addEventListener('bundle:price-updated', …)`.
* `next.on()` will not receive it.
*/
'bundle:price-updated': { selectorId: string };
/**
* One selector card's prices finished loading and its raw
* `data-package-price-*` attributes were written.
*
* Delivered as a DOM `CustomEvent` on `document`, not through the event bus —
* subscribe with `document.addEventListener('selector:price-updated', …)`.
* `next.on()` will not receive it.
*/
'selector:price-updated': { selectorId: string; packageId: number };
/**
* A package toggle's price finished loading.
*
* Delivered as a DOM `CustomEvent` on `document`, not through the event bus.
*/
'toggle:price-updated': { packageId: number };
// Package Toggle Events
/**
* A package toggle was switched — an add-on like a warranty or express
* shipping going into or out of the cart.
*
* @example
* ```json
* { "packageId": 205, "added": true }
* ```
*/
'toggle:toggled': {
/** The package that was toggled. */
packageId: number;
/** `true` when it is now in the cart, `false` when it was removed. */
added: boolean;
};
/** The full set of toggled-on packages changed. */
'toggle:selection-changed': {
/** Every package currently toggled on. */
selected: number[];
};
// Scroll Hint Events
/**
* A scroll hint recalculated whether it should be visible — on scroll, and when
* the scrollable content resizes.
*
* @example
* ```json
* {
* "isVisible": true,
* "scrollTop": 0,
* "scrollHeight": 1400,
* "clientHeight": 600
* }
* ```
*/
'scroll-hint:updated': {
/** Whether the hint is showing: the target is at the top and can scroll. */
isVisible: boolean;
/** Current scroll offset of the watched element. */
scrollTop: number;
/** Full scrollable height of the watched element. */
scrollHeight: number;
/** Visible height of the watched element. */
clientHeight: number;
};
}
/**
* One line in the cart — a package the shopper has added, plus every price and
* product detail the SDK tracks for it. This is the raw stored shape; for the
* display-ready version with a full pricing breakdown see {@link EnrichedCartLine}.
* @category Cart
*/
export interface CartItem {
/** Unique cart line ID returned by the API. */
id: number;
/** The campaign package `ref_id` for this item. */
packageId: number;
/** Original package ID before any variant swap was applied. */
originalPackageId?: number;
/** Number of packages in the cart (not units — see `qty` for units per package). */
quantity: number;
/** Total package price as a raw number (mirrors `price_total` from the campaign). Used for calculations. */
price: number;
/** Product image URL. */
image: string | undefined;
/** Package display name. */
title: string;
/** Product SKU. */
sku: string | undefined;
/** `true` when this item was added via a post-purchase upsell, not a regular add-to-cart. */
is_upsell: boolean | undefined;
/** Per-unit price as a formatted string (matches `price` from the campaign API). */
price_per_unit?: string | undefined;
/** Number of product units included in this package (matches `qty` from the campaign API). */
qty?: number | undefined;
/** Total package price as a formatted string (matches `price_total` from the campaign API). */
price_total?: string | undefined;
/** Per-unit retail/compare-at price as a formatted string. */
price_retail?: string | undefined;
/** Total retail/compare-at price as a formatted string. */
price_retail_total?: string | undefined;
/** Recurring per-unit price string. Present when `is_recurring` is `true`. */
price_recurring?: string | undefined;
/** Total recurring price string. */
price_recurring_total?: string | undefined;
/** Per-unit price after offer discounts. */
unit_price?: string | undefined;
/** Per-unit price before offer discounts. */
original_unit_price?: string | undefined;
/** Total package price after offer discounts. */
package_price?: string | undefined;
/** Total package price before offer discounts. */
original_package_price?: string | undefined;
/** Total line amount as a formatted string. */
total?: string | undefined;
/** Total discount amount for this line as a formatted string. */
total_discount?: string | undefined;
/** Offer discounts applied to this line. */
discounts?:
| Array<{
offer_id: number;
amount: string;
description?: string;
name?: string;
}>
| undefined;
/** `true` for subscription/recurring items. Check `interval` and `interval_count` for billing cycle details. */
is_recurring?: boolean | undefined;
/** Billing interval for recurring items (`'day'` or `'month'`). */
interval?: string | null | undefined;
/** Number of intervals between billing cycles (e.g. `3` with `interval: 'month'` = every 3 months). */
interval_count?: number | null | undefined;
/** Associated product ID. */
productId?: number | undefined;
/** Associated product display name. */
productName?: string | undefined;
/** Product variant ID. */
variantId?: number | undefined;
/** Product variant display name. */
variantName?: string | undefined;
/** Variant attribute values (e.g. `[{ code: 'color', name: 'Color', value: 'Red' }]`). */
variantAttributes?:
| Array<{ code: string; name: string; value: string }>
| undefined;
/** Variant SKU. */
variantSku?: string | undefined;
/** IDs of other cart items grouped with this one (bundle support). */
groupedItemIds?: number[] | undefined;
/** Selector ID this item belongs to (set by BundleSelectorEnhancer via data-next-selector-id). */
selectorId?: string | undefined;
/** Custom key-value properties for this line item. Two items with the same packageId but different properties are treated as separate lines. */
properties?: Record<string, string>;
}
/**
* A single discount applied to the cart, an item, or shipping — whether from an
* offer or a voucher. Appears in {@link CartState.offerDiscounts},
* {@link CartState.voucherDiscounts}, and {@link ShippingMethod.discounts}.
* @category Coupons
*/
export interface Discount {
/** ID of the offer that generated this discount. */
offer_id?: number;
/** Discount amount as a formatted string (e.g. "$10.00"). */
amount: string;
/** Optional description of the discount (e.g. "10% off"). */
description?: string;
/** Optional name of the discount (e.g. "Spring Sale"). */
name?: string;
/** Optional discount percentage as a numeric string (e.g. "10" or "10.00"). */
percentage?: string;
}
/**
* One selectable option inside a package selector — the data behind a single
* card the shopper can pick. Emitted on `selector:item-selected` and related
* events so handlers know which package a card represents.
* @category Cart
*/
export interface SelectorItem {
/** The card's DOM element. */
element: HTMLElement;
/** The package `ref_id` this card selects. */
packageId: number;
/** Quantity this card adds when selected. */
quantity: number;
/** Unit price for the card, or `undefined` if not resolved yet. */
price: number | undefined;
/** Display name for the card, or `undefined` if none. */
name: string | undefined;
/** `true` if this card was marked selected in the markup on load. */
isPreSelected: boolean;
/** Shipping method id tied to this card, if any. */
shippingId: string | undefined;
}
/**
* The full cart snapshot — items, totals, discounts, and shipping — as held in
* {@link useCartStore} and delivered with every `cart:updated` event. This is
* what you read to render prices, item counts, and totals.
* @category Cart
*/
export interface CartState {
/** All items currently in the cart. */
items: CartItem[];
/** Total unit count across all items (sum of each item's `quantity × qty`). */
totalQuantity: number;
/** `true` when the cart has no items. */
isEmpty: boolean;
/** List of applied coupon codes. */
vouchers: string[];
/** `true` while a package swap animation is in progress. Use to prevent double-clicks. */
swapInProgress?: boolean;
/** ISO currency code of cart data. */
currency?: string;
/** Detailed offer information for offers applied to the cart. */
offerDiscounts?: Discount[];
/** Detailed voucher information for vouchers applied to the cart. */
voucherDiscounts?: Discount[];
/** Cart subtotal before shipping and discounts. */
subtotal: Decimal;
/** The currently selected shipping method and its pricing details. */
shippingMethod?: ShippingMethod;
/** `true` when any discount (coupon or offer) is applied. */
hasDiscounts: boolean;
/** Total discount amount from coupons and offers. */
totalDiscount: Decimal;
/** Total discount as a percentage of the subtotal. */
totalDiscountPercentage: Decimal;
/** Cart grand total (subtotal + shipping − discounts). */
total: Decimal;
/** Raw CartSummary response from the API calculate endpoint. */
summary?: import('./api').CartSummary;
/** `true` while the calculate API is in flight. Use to show loading state on price/total fields. */
isCalculating: boolean;
}
/**
* A cart line prepared for display: the same item as {@link CartItem} but with
* a structured price breakdown (with/without tax, original, savings) and product
* details ready to render. This is the shape `next.getCartData().cartLines`
* returns, built from the cart's items on each call. The `data-next-display`
* fields read the stored {@link CartItem} instead.
* @category Cart
*/
export interface EnrichedCartLine {
/** Cart line ID (matches {@link CartItem.id}). */
id: number;
/** The package `ref_id` for this line. */
packageId: number;
/** Number of packages on this line. */
quantity: number;
/** Price breakdown, each as a raw `value` and a display `formatted` string. */
price: {
/** Line total after discounts. The cart calculate API reports no tax, so this matches `incl_tax`. */
excl_tax: { value: number; formatted: string };
/** Line total after discounts. The cart calculate API reports no tax, so this matches `excl_tax`. */
incl_tax: { value: number; formatted: string };
/** The highest compare-at figure the line carries: its retail price, else its price before offer discounts. */
original: { value: number; formatted: string };
/** `original` minus the line total, never negative. */
savings: { value: number; formatted: string };
};
/** Product details for rendering. */
product: {
title: string;
sku: string;
image: string;
};
/** `true` if added via a post-purchase upsell. */
is_upsell: boolean;
/** `true` for subscription/recurring lines. */
is_recurring: boolean;
/** Billing interval for recurring lines. */
interval?: 'day' | 'month';
/** `true` when one selector put this line and at least one other in the cart, as a bundle selector does. */
is_bundle: boolean;
/** Cart line IDs of the bundle's other lines. Absent unless `is_bundle`. */
bundleComponents?: number[];
}
/**
* The campaign a page is selling — its packages, pricing currency, shipping
* options, and the payment/currency/country choices available to the shopper.
* Loaded once at init and held in {@link useCampaignStore} (on its `.data`
* field), and delivered with the `campaign:loaded` event.
* @category Campaign
*/
export interface Campaign {
/** Internal NEXT campaign id. */
id?: number;
/** ISO currency code all package prices are quoted in. */
currency: string;
/** Campaign language code (e.g. `"en"`). */
language: string;
/** Campaign display name. */
name: string;
/** Every package (offer) the campaign sells. */
packages: Package[];
/** Public key used to initialise the payment gateway. */
payment_env_key: string;
/** Shipping options the shopper can choose from. */
shipping_methods: ShippingOption[];
/** Offers/discount rules configured for the campaign, if any. */
offers?: Offer[];
/** Currencies the shopper may switch between, with display labels. */
available_currencies?: Array<{ code: string; label: string }>;
/** Countries the campaign can ship to, with display labels. */
available_shipping_countries?: Array<{ code: string; label: string }>;
/** Express payment methods enabled (PayPal, Apple Pay, …). */
available_express_payment_methods?: Array<{ code: string; label: string }>;
/** Standard payment methods enabled for checkout. */
available_payment_methods?: Array<{ code: string; label: string }>;
}
/**
* A single purchasable package (offer) within a {@link Campaign} — one buyable
* unit with its own pricing, product, and recurring/variant details. `ref_id`
* is the id you pass everywhere else (add-to-cart, selectors, `data-next-package-id`).
* @category Campaign
*/
export interface Package {
/** The package id used throughout the SDK (add-to-cart, selectors, attributes). */
ref_id: number;
/** The store's external/catalog id for this package. */
external_id: number;
/** Package display name. */
name: string;
/** Per-unit price as a formatted string. */
price: string;
/** Total package price (all units) as a formatted string. */
price_total: string;
/** Per-unit retail/compare-at price, if set. */
price_retail?: string;
/** Total retail/compare-at price, if set. */
price_retail_total?: string;
/** Recurring per-unit price, for subscription packages. */
price_recurring?: string;
/** Total recurring price, for subscription packages. */
price_recurring_total?: string;
/** Number of product units included in the package. */
qty: number;
/** Product image URL. */
image: string;
/** `true` if this is a subscription/recurring package. */
is_recurring: boolean;
/** Billing interval for recurring packages. */
interval?: 'day' | 'month' | null;
/** Number of intervals between billing cycles. */
interval_count?: number | null;
/** Product variant id, when the package maps to a specific variant. */
product_variant_id?: number;
/** Product variant display name. */
product_variant_name?: string;
/** Underlying product id. */
product_id?: number;
/** Underlying product display name. */
product_name?: string;
/** Product SKU, or `null` if none. */
product_sku?: string | null;
/** Whether the product can currently be purchased. */
product_purchase_availability?: string;
/** Whether the product is currently in stock. */
product_inventory_availability?: string;
}
/**
* A shipping choice offered by a {@link Campaign}, as returned by the API. Its
* `ref_id` is what a shopper's selection resolves to; the richer runtime form
* with computed discounts is {@link ShippingMethod}.
* @category Shipping
*/
export interface ShippingOption {
/** Shipping option id. */
ref_id: number;
/** Shipping option code (matches the campaign API). */
code: string;
/** Shipping price as a formatted string. */
price: string;
}
/**
* Google Maps settings for address autocomplete on checkout forms. Optional —
* autocomplete is off unless an API key is supplied.
* @category Checkout
*/
export interface GoogleMapsConfig {
/** Google Maps API key. Autocomplete stays disabled without it. */
apiKey?: string;
/** Region bias for autocomplete results (e.g. `"US"`). */
region?: string;
/** Master switch for address autocomplete. */
enableAutocomplete?: boolean;
/** Extra options passed through to the Google autocomplete widget. */
autocompleteOptions?: any;
}
/**
* How the checkout address form behaves — default country, which countries and
* states to show, and whether to autocomplete. All fields are optional; sensible
* defaults are derived from the campaign's shipping countries.
* @category Checkout
*/
export interface AddressConfig {
/**
* Fallback country when detected country is not available (Low priority fallback).
*
* Automatic fallback priority:
* 1. United States (US) - if available in shipping countries
* 2. First country in available list - if US not available
* 3. This defaultCountry - only if list is empty (edge case)
*
* @example "US"
* @default undefined (auto-fallback to US or first available country)
*/
defaultCountry?: string;
/**
* @deprecated Use campaign API's available_shipping_countries instead.
* Countries are now automatically filtered based on your campaign configuration.
* This field is kept for backward compatibility only (Priority 3 fallback).
* @example ["US", "CA", "GB"]
*/
showCountries?: string[];
/**
* Array of state/province codes to hide from dropdowns (e.g., US territories).
* @example ["AS", "GU", "PR", "VI"]
*/
dontShowStates?: string[];
/**
* Custom countries list with full control over code and name.
* Takes priority over showCountries but not over campaign API countries.
* @example [{ code: "US", name: "United States" }]
*/
countries?: Array<{
code: string;
name: string;
}>;
enableAutocomplete?: boolean;
}
/**
* Everything a page can configure about the SDK.
*
* You write these keys on `window.nextConfig` before the loader script runs. Some
* also have a `<meta name="next-*">` equivalent, and where both exist the meta tag
* wins, because `loadFromWindow()` runs first and `loadFromMeta()` overwrites it
* (`core/sdk-initializer.ts › SDKInitializer.initialize`).
*
* Fields marked "the SDK fills this in" are readable at runtime but are not yours
* to set. Writing them on `window.nextConfig` has no effect.
*
* @example
* ```html
* <script>
* window.nextConfig = {
* apiKey: "{YOUR_CAMPAIGN_API_KEY}",
* storeName: "acme",
* currencyBehavior: "auto",
* analytics: { enabled: true, mode: "auto", debug: false, providers: {} }
* };
* </script>
* <script src="/next-campaign-cart.js" type="module"></script>
* ```
*
* @category Core
*/
export interface ConfigState {
/**
* The campaign API key every request authenticates with.
*
* Also settable as `<meta name="next-api-key">`, which wins over this value. An
* empty string means the SDK never found a key, so no prices load and no order
* can be placed.
*
* A leftover meta tag silently overriding the key here is the usual cause of a
* page loading the wrong campaign. Check the markup before anything else.
*
* @example "{YOUR_CAMPAIGN_API_KEY}"
*/
apiKey: string;
/**
* A campaign identifier shown in the debug panel.
*
* Nothing in the SDK sends it: requests identify the campaign from {@link ConfigState.apiKey}
* alone. Setting it does not point the page at a different campaign.
*
* @example "winter-sale"
* @default "" (not supplied)
*/
campaignId: string;
/**
* Turns on verbose SDK logging in the browser console.
*
* Also turned on by `<meta name="next-debug" content="true">` or `?debugger=true`
* in the URL. Leave it off for shopper traffic.
*
* @example true
* @default false
*/
debug: boolean;
/**
* Opens the on-page debug overlay at boot.
*
* The overlay also reads `?debugger=true` and `window.nextConfig.debugger`
* directly, so changing this value after boot does not open or close it.
*
* @example true
* @default false
*/
debugger: boolean | undefined;
/**
* Which stage of the funnel this page is.
*
* Analytics reports it as `page_type`, so a mislabelled page files its events
* under the wrong funnel step. Prefer the meta tag, which wins over this value.
*
* @example "checkout"
* @see {@link PageType}
*/
pageType: PageType;
/**
* The store identifier used to deduplicate purchases against a server-side copy
* of the same order.
*
* Required by the NEXT Storefront Meta App: with it set, the Meta event carries
* `eventID: "{storeName}-{orderNumber}"`. Leave it unset and browser and server
* events count the same order twice.
*
* @example "acme"
*/
storeName?: string;
/**
* The key that authorises the hosted credit-card fields.
*
* `undefined` means the card fields cannot start, so card payment is unavailable
* and only express or pay-later methods work. Once the campaign loads its own
* `payment_env_key` overwrites whatever was set here, so check the campaign
* response before the page markup when the card fields use an unexpected key.
*/
spreedlyEnvironmentKey?: string | undefined;
/**
* Express checkout and card field behaviour.
*
* Accepted on `window.nextConfig` as either `paymentConfig` or `payment`.
*
* @example
* ```js
* paymentConfig: {
* expressCheckout: {
* enabled: true,
* methodOrder: ["paypal", "apple_pay", "google_pay"]
* }
* }
* ```
* @see {@link PaymentConfig}
*/
paymentConfig: PaymentConfig;
/**
* Google Maps address autocomplete settings.
*
* Written as `googleMaps` on `window.nextConfig`. Leave `apiKey` empty to use the
* NEXT autocomplete instead; a non-empty key takes priority over it.
*
* @example
* ```js
* googleMaps: { apiKey: "{GOOGLE_MAPS_API_KEY}", region: "US" }
* ```
* @see {@link GoogleMapsConfig}
*/
googleMapsConfig: GoogleMapsConfig;
/**
* How the checkout address form behaves.
*
* @example
* ```js
* addressConfig: { dontShowStates: ["AS", "GU", "PR", "VI"], enableAutocomplete: true }
* ```
* @see {@link AddressConfig}
*/
addressConfig: AddressConfig;
/** The visitor's country, as geo-detected at boot. The SDK fills this in. */
detectedCountry?: string;
/** The currency geo-detection chose for the visitor. The SDK fills this in. */
detectedCurrency?: string;
/** The visitor's IP address, from geo-detection. The SDK fills this in. */
detectedIp?: string;
/**
* The currency the visitor is actually being charged in, held for the whole
* session. The SDK fills this in from `?currency=`, a stored session value, or
* geo-detection, in that order.
*/
selectedCurrency?: string;
/** The raw geo-detection response. The SDK fills this in. */
locationData?: any;
/**
* Whether the SDK may change currency when the visitor's country changes.
*
* `"auto"` geo-detects on first load when no `?currency=` parameter and no stored
* currency exist, then locks that currency for the session. `"manual"` never
* auto-detects and falls back to the campaign default.
*
* @example "manual"
* @default "auto"
*/
currencyBehavior?: 'auto' | 'manual';
/**
* Whether the requested currency was unavailable and a fallback was used. The
* SDK fills this in.
*/
currencyFallbackOccurred?: boolean;
/**
* Keeps two campaigns on one domain from sharing a cart.
*
* The SDK derives a scope from the API key and the folder the page is served
* from, so you do not normally set this. Reach for it when a funnel's pages sit
* at different depths (`/hu/` and `/hu/checkout`), which derives two scopes and
* loses the cart between them. Pages that must share a cart declare the same
* value; pages that must not declare different ones.
*
* Also settable as `<meta name="next-storage-scope">`. Unlike every other key
* here, this value wins over the meta tag. It is read while the storage modules
* are created, so it must be set before the loader script runs.
*
* @example "hu-earbuds"
* @default undefined (derived from the API key and page path)
*/
storageScope?: string;
/**
* BCP 47 tag that pins how prices are written — `"de-DE"`, `"fr-FR"`, `"en-US"`.
*
* The locale, not the currency code, decides the decimal separator and which side the
* symbol sits on: the same `EUR` amount is `€69.99` under `en-US` and `69,99 €` under
* `de-DE`. Set this when a store must render the same way for every visitor.
*
* Leave it unset and the visitor's own browser locale is used, which is usually what you
* want — a German shopper already sees `69,99 €`. Pin it only when the store's format
* must not follow the browser.
*
* An unparseable tag (`"de_DE"` with an underscore is the common typo) is rejected with a
* warning and the browser locale is used instead, so a bad value never breaks prices.
*
* @example "de-DE"
* @default undefined (use the browser's locale)
*/
locale?: string;
// Additional configuration properties for complete type coverage
autoInit: boolean | undefined;
rateLimit: number | undefined;
cacheTtl: number | undefined;
retryAttempts: number | undefined;
timeout: number | undefined;
testMode: boolean | undefined;
// API and performance settings
maxRetries: number | undefined;
requestTimeout: number | undefined;
enableAnalytics: boolean | undefined;
enableDebugMode: boolean | undefined;
// Environment and deployment settings
environment: 'development' | 'staging' | 'production' | undefined;
version?: string | undefined;
buildTimestamp?: string | undefined;
// Discount system
discounts: Record<string, DiscountDefinition>;
// Attribution configuration
utmTransfer?: {
enabled: boolean;
applyToExternalLinks?: boolean;
excludedDomains?: string[];
paramsToCopy?: string[];
};
// Tracking configuration (legacy)
tracking?: 'auto' | 'manual' | 'disabled';
// New analytics configuration
analytics?: {
enabled: boolean;
mode: 'auto' | 'manual' | 'disabled';
debug: boolean;
providers: {
gtm: {
enabled: boolean;
settings: {
containerId?: string;
dataLayerName?: string;
environment?: string;
};
};
facebook: {
enabled: boolean;
settings: {
pixelId: string;
accessToken?: string;
testEventCode?: string;
};
blockedEvents?: string[];
};
custom: {
enabled: boolean;
settings: {
endpoint: string;
apiKey?: string;
batchSize?: number;
timeout?: number;
};
};
};
};
// Error monitoring configuration - removed
// Error tracking can be added externally via HTML/scripts
// Cart initialization behavior
clearCartOnInit?: boolean;
}
/**
* Which stage of the funnel a page represents. Drives page-specific behavior and
* analytics.
*
* Set with `<meta name="next-page-type" content="checkout">`, or with
* `window.nextConfig.pageType` — the meta tag wins over the config. It is a meta
* tag only: there is no element attribute for it, and a value written that way is
* never read.
*
* @example
* ```html
* <meta name="next-page-type" content="upsell">
* ```
* @category Core
*/
export type PageType = 'product' | 'cart' | 'checkout' | 'upsell' | 'receipt';
/**
* Configuration for the hosted (iFrame-based) credit-card input fields — keyboard
* type, formatting, labels, styling, and the security tokens required to
* authenticate the fields. Previously named `SpreedlyConfig`; {@link SpreedlyConfig}
* remains as an alias.
* @category Checkout
*/
export interface CardInputConfig {
// Field type configuration - controls keyboard display on mobile
fieldType?: {
number?: 'number' | 'text' | 'tel';
cvv?: 'number' | 'text' | 'tel';
};
// Number format configuration
numberFormat?: 'prettyFormat' | 'plainFormat' | 'maskedFormat';
// Label configuration for accessibility
labels?: {
number?: string;
cvv?: string;
};
// Title attribute for accessibility
titles?: {
number?: string;
cvv?: string;
};
// Placeholder text
placeholders?: {
number?: string;
cvv?: string;
};
// CSS styling for iFrame fields
styles?: {
number?: string;
cvv?: string;
placeholder?: string;
};
// Security parameters - REQUIRED for authentication
nonce?: string; // Unique per session (e.g., UUID)
timestamp?: string; // Epoch time
certificateToken?: string; // Spreedly certificate token
signature?: string; // Server-generated signature
// Fraud detection
fraud?: boolean | { siteId: string }; // Enable fraud detection or specify BYOC fraud site ID
// Other options
enableAutoComplete?: boolean; // Toggle autocomplete functionality
requiredAttributes?: {
number?: boolean;
cvv?: boolean;
};
// Validation parameters
allowBlankName?: boolean; // Skip name validation
allowExpiredDate?: boolean; // Allow expired dates
}
/**
* @deprecated Use {@link CardInputConfig}. Kept as an alias for backward
* compatibility — the two are identical.
* @category Checkout
*/
export type SpreedlyConfig = CardInputConfig;
/**
* Payment setup for checkout — the card-input configuration plus which express
* methods (PayPal, Apple Pay, Google Pay) are enabled and how they behave.
* @category Checkout
*/
export interface PaymentConfig {
// Generic card input configuration (preferred)
cardInputConfig?: CardInputConfig;
// Legacy naming - maintained for backward compatibility
spreedly?: CardInputConfig;
expressCheckout?: {
enabled: boolean;
/**
* Which express buttons to offer, when the campaign does not say.
*
* Write the method names with underscores (`apple_pay`, `google_pay`), the
* same as `methodOrder` beside them and as every other place the SDK names a
* payment method. `applePay`/`googlePay` are the older camelCase spellings and
* still work; a method turned on under either key is turned on.
*/
methods: {
paypal?: boolean;
apple_pay?: boolean;
google_pay?: boolean;
link?: boolean;
/** @deprecated Write `apple_pay`. Still read, so existing config keeps working. */
applePay?: boolean;
/** @deprecated Write `google_pay`. Still read, so existing config keeps working. */
googlePay?: boolean;
};
methodOrder?: ('paypal' | 'apple_pay' | 'google_pay' | 'link')[]; // Order in which payment methods should be displayed
requireValidation?: boolean; // If true, express payment methods in combo form will require form validation
requiredFields?: string[]; // List of fields required for express checkout (e.g., ['email', 'fname', 'lname'])
};
}
/**
* The lifecycle hooks you can register a callback for — points in the render and
* checkout flow where the SDK will invoke your function with {@link CallbackData}.
* @category Events
*/
export type CallbackType =
| 'beforeRender'
| 'afterRender'
| 'beforeCheckout'
| 'afterCheckout'
| 'beforeRedirect'
| 'itemAdded'
| 'itemRemoved'
| 'cartCleared';
/**
* The snapshot passed to a lifecycle callback ({@link CallbackType}) — the
* current cart lines, totals, campaign data, and applied vouchers at the moment
* the hook fires.
* @category Events
*/
export interface CallbackData {
/** Display-ready cart lines at the time the hook fires. */
cartLines: EnrichedCartLine[];
/** The totals slice of {@link CartState}. */
cartTotals: Pick<
CartState,
| 'subtotal'
| 'total'
| 'hasDiscounts'
| 'totalDiscount'
| 'totalDiscountPercentage'
| 'shippingMethod'
>;
campaignData: Campaign | null;
vouchers: string[];
}
/**
* The rules that define a discount code — its type, value, what it applies to,
* and any limits. These are configured up front; when a shopper enters a code
* that matches one, it becomes an {@link AppliedCoupon}.
* @category Coupons
*/
export interface DiscountDefinition {
/** The code a shopper enters (e.g. `"SAVE10"`). */
code: string;
/** Whether `value` is a percentage or a fixed amount. */
type: 'percentage' | 'fixed';
/** The discount amount — a percent (e.g. `10`) or a currency amount. */
value: number;
/** Whether the discount applies to the whole order or specific packages. */
scope: 'order' | 'package';
/** Packages the discount applies to, when `scope` is `'package'`. */
packageIds?: number[];
/** Minimum order value required for the code to apply. */
minOrderValue?: number;
/** Cap on the discount amount, for percentage discounts. */
maxDiscount?: number;
/** Human-readable description of the discount. */
description?: string;
/** Maximum number of times the code may be used. */
usageLimit?: number;
/** Whether this code can be combined with other coupons. */
combinable?: boolean;
}
/**
* A discount code the shopper has successfully applied — its code, the computed
* discount amount, and the {@link DiscountDefinition} it matched.
* @category Coupons
*/
export interface AppliedCoupon {
/** The applied code. */
code: string;
/** The calculated discount amount for the current cart. */
discount: number;
/** The rule this code matched. */
definition: DiscountDefinition;
}
/**
* @deprecated Legacy coupon shape kept for backward compatibility. New code uses
* {@link AppliedCoupon} / {@link DiscountDefinition}.
* @category Coupons
*/
export interface Coupon {
code: string;
amount: number;
type: 'fixed' | 'percentage';
}
/**
* The shipping method currently applied to the cart, with fully computed pricing
* (original price, discount, final price). The runtime counterpart to the
* campaign's {@link ShippingOption}.
* @category Shipping
*/
export interface ShippingMethod {
/** Shipping method ID. */
id: number;
/** Shipping method display name. */
name: string;
/** Shipping method code (matches campaign API). */
code: string;
/** Original shipping price before any discount. */
originalPrice: Decimal;
/** Final shipping price after discount. */
price: Decimal;
/** Absolute discount applied to shipping. */
discountAmount: Decimal;
/** Shipping discount as a percentage of the original price. */
discountPercentage: Decimal;
/** `true` when a shipping discount is applied. */
hasDiscounts: boolean;
/** Detailed shipping discounts applied to the cart. */
discounts?: Discount[];
}
/**
* The method the shopper chose: every {@link PaymentMethod} the orders API takes,
* plus `credit-card`, plus any other name the page offered.
*
* **The names are the API's own**, so a listener reading this and a request going
* out say the same thing. `credit-card` is the single exception: it is what this
* store has always held for a card, and it becomes `card_token` on the order.
* A page writes `credit` for that method, which is the markup's word for it.
*
* The open end is deliberate. A store can be given a new way to pay before this
* SDK release knows about it, so a name that is not in `PaymentMethod` is **passed
* through to the orders API** rather than replaced with a card. The API decides
* whether it can charge that way.
*
* Every method except a directly-charged card sends the shopper away to pay, so
* the created order comes back carrying a `payment_complete_url` to finish at.
* See {@link Order.payment_complete_url}.
* @category Checkout
*/
export type CheckoutPaymentMethod =
| PaymentMethod
| 'credit-card'
| (string & {});
/**
* The state of the checkout in progress — the collected form values, chosen
* payment method, and which step the shopper is on. Delivered with the
* `checkout:started` event.
* @category Checkout
*/
export interface CheckoutData {
/** Collected checkout form field values, keyed by field name. */
formData: Record<string, any>;
/** The payment method the shopper selected. */
paymentMethod: CheckoutPaymentMethod;
/** `true` while the order is being submitted. */
isProcessing?: boolean;
/** Current step index in a multi-step checkout. */
step?: number;
}
/**
* The six fields you can always count on being present on a completed order —
* enough to identify it, show its total, and link to its receipt.
*
* A narrow, older view of {@link Order}, kept exported because integrations import
* it. **No event declares this payload any more:** `order:completed` carries the
* full `Order`, so a listener gets totals excluding tax, tax, shipping, discounts,
* addresses, attribution and typed lines without reaching for anything else. Use
* `Order` in new code; the same object is on `useOrderStore().order`.
*
* The field list is pinned to `Order` by the compiler (`extends Pick<Order, …>`),
* so the two declarations cannot drift apart.
*
* @example
* ```ts
* // `Order` is a superset, so an OrderData-typed helper still accepts the payload.
* function receiptLine(order: OrderData): string {
* return `${order.number} — ${order.total_incl_tax} ${order.currency}`;
* }
*
* window.next.on('order:completed', order => console.log(receiptLine(order)));
* ```
* @category Orders
*/
export interface OrderData
extends Pick<
Order,
| 'ref_id'
| 'number'
| 'currency'
| 'total_incl_tax'
| 'order_status_url'
| 'is_test'
> {
/** Order reference id — used to fetch the order and add upsells. */
ref_id: string;
/** Human-facing order number. */
number: string;
/** ISO currency code the order was placed in. */
currency: string;
/** Order grand total including tax, as a formatted string. */
total_incl_tax: string;
/** URL of the hosted order-status/receipt page. */
order_status_url: string;
/** `true` for test-mode orders (not real purchases). */
is_test: boolean;
/**
* Order line items. Loosely typed for backwards compatibility — the runtime
* value is {@link Order.lines}, so read it through {@link Order} to get
* `product_title`, `quantity`, `is_upsell` and the per-line prices typed.
*/
lines?: any[];
/**
* Customer details attached to the order. Loosely typed for backwards
* compatibility — the runtime value is {@link Order.user} ({@link OrderUser}).
*/
user?: any;
}
/**
* The payload of an `error:occurred` event — a human-readable message plus an
* optional machine code and extra details for debugging.
* @category Core
*/
export interface ErrorData {
/** Human-readable error message. */
message: string;
/** Machine-readable error code, when available. */
code?: string;
/** Extra context about the error, when available. */
details?: any;
}