← Files WixARCHIVED FILE
skills/wix-headless-templates/storefront/app/wix/storefront/types.ts
13.2 KB · Oct 8, 2026 · 12:02 UTC
// Storefront DTOs — the serializable shapes every hook, component, and page consumes.
// These are plain JSON: safe to pass as Astro island props or across a server/client
// boundary. Image values are already-resolved https URLs (never wix:image://), and every
// displayable price is a ready formatted string.
export type Availability = "IN_STOCK" | "OUT_OF_STOCK" | "PARTIALLY_OUT_OF_STOCK";
/** A product as a listing/grid tile needs it. */
export interface ProductSummary {
id: string;
slug: string;
name: string;
/**
* The price the buyer pays for the cheapest variant, formatted (an automatic discount already
* applied). When `price !== maxPrice` the product is a RANGE — render "price – maxPrice".
*/
price: string;
/** Highest variant price, formatted — differs from `price` when variants are priced differently. */
maxPrice: string;
/**
* The cheapest variant is discounted AND variants are priced differently: the discounted
* maximum is unknown until the PDP loads the variants, so render "From {price}" (no maxPrice,
* no struck price). `maxPrice` equals `price` then.
*/
fromPrice: boolean;
/**
* Struck "was" price, formatted; null when not on sale — and ALWAYS null for a range (a lone
* struck minimum beside a range claims a saving that may not apply to the variant picked).
*/
compareAtPrice: string | null;
/** Names of the automatic discount rules applying to this product ("Summer sale") — render under the price; [] when none. */
discountNames: string[];
/** Price per unit of the cheapest variant as Wix formats it ("€2.50 / 100 g"); null when the product isn't sold by unit. */
pricePerUnit: string | null;
/** The primary merchant ribbon ("New", "Best Seller"); null when none. Same as ribbons[0]. */
ribbon: string | null;
/** EVERY merchant ribbon, primary first ("New", "Sale") — render all of them, one shared style. */
ribbons: string[];
/** The cheapest variant's id — what a direct add sends for a product with no options; null when unknown. */
minPriceVariantId: string | null;
availability: Availability;
/** OUT_OF_STOCK but pre-orderable — label "Pre-order", not "Sold out". */
preorder: boolean;
/** The product sells as a subscription (recurring plans) — the tile routes to the PDP, where the plan is picked. */
hasSubscriptions: boolean;
/** Resolved https URL of the main image ("" when the product has none). */
imageUrl: string;
/** Resolved https URL of the second gallery image ("" when there is only one). */
hoverImageUrl: string;
/** e.g. "2 colors · 3 sizes"; "" for a single-variant product. */
optionsSummary: string;
/**
* Hex colors of a color option's visible choices, catalog order — render as small dots on the
* tile (a preview, not a picker: selection happens in QuickAdd or on the PDP). [] when none.
*/
swatches: string[];
/** True when the product can be added to the cart with no choices (single variant, in stock, no subscription plans). */
quickAddable: boolean;
}
export interface OptionChoice {
choiceId: string;
/** Read-only identifier Wix derives from the name — what `catalogReference.options[option.key]` carries. */
key: string;
name: string;
/** Hex color for swatch rendering; null for text choices. */
colorCode: string | null;
/** At least one variant with this choice is in stock (catalog-level; the PDP store refines it against the current selection). */
inStock: boolean;
/** The gallery image the merchant linked to this choice (shown when it is picked), resolved to https; null when none. */
imageUrl: string | null;
}
export interface ProductOption {
/** The option's customization id — what `selectOption(optionId, choiceId)` and `ProductVariant.choiceIds` use. */
id: string;
/** Read-only identifier Wix derives from the name — the key inside `catalogReference.options`. */
key: string;
name: string;
/** Render choices as color swatches (true) or text pills (false). */
isColor: boolean;
choices: OptionChoice[];
}
export interface ProductModifier {
/** The modifier's customization id. */
id: string;
/**
* The key the cart takes: `catalogReference.options[key] = choice.key` for a choice modifier,
* `customTextFields[key] = text` for a free-text one (its `freeTextSettings.key`).
*/
key: string;
name: string;
/** Input required before adding — Wix treats a modifier as mandatory unless it says otherwise. */
mandatory: boolean;
/** "choices" renders pills; "text" renders a free-text input. */
type: "choices" | "text";
/** The label of a free-text input (Wix's `freeTextSettings.title`); the modifier name for choices. */
title: string;
/** Free text only: character limits from the merchant; null when unlimited / none. */
maxChars: number | null;
minChars: number | null;
choices: { choiceId: string; key: string; name: string }[];
}
/** A recurring plan the product can be bought on. */
export interface SubscriptionPlan {
id: string;
name: string;
description: string;
/** Payment frequency unit; null when Wix didn't say. */
frequency: "DAY" | "WEEK" | "MONTH" | "YEAR" | null;
/** Every N frequency units (1 = every month). */
interval: number;
/** Number of payments; null = until cancelled (auto-renewing). */
billingCycles: number | null;
/** "every 2 months · 6 payments" — the terms in words, ready to render. */
terms: string;
}
export interface ProductVariant {
variantId: string;
/** The option selections this variant answers to, by id: optionId -> choiceId (what resolveVariant matches on). */
choiceIds: Record<string, string>;
/** The same selections by name: optionName -> choiceName (display only). */
choices: Record<string, string>;
/** The price the buyer pays, formatted — an automatic discount beats the regular price. */
price: string;
/** The same selling price as a number in site currency — for ranges, never for display. */
priceAmount: number;
/** Struck "was" price, formatted; null unless it is real and higher than `price`. */
compareAtPrice: string | null;
/** Price per unit as Wix formats it ("€2.50 / 100 g"); null when not sold by unit. */
pricePerUnit: string | null;
/** Merchant SKU; null when none. */
sku: string | null;
/** planId -> the formatted price of this variant on that plan (Wix applies the plan's discount to the variant's price). */
subscriptionPrices: Record<string, string>;
inStock: boolean;
/** Out of stock but pre-orderable — still buyable (the add carries preOrderRequested). */
preorderEnabled: boolean;
/**
* Units left to buy: the tracked stock, or the remaining pre-order allowance when pre-ordering.
* null when stock isn't counted (made to order) or the inventory hasn't loaded yet.
*/
quantity: number | null;
/** The merchant's pre-order note ("Ships in 3 weeks"); null when none or not pre-orderable. */
preorderMessage: string | null;
/**
* The image Wix derived for this variant from its choice's linked media, resolved to https; null when none.
* Wix derives it at product creation only (and only for single-option products), so a choice image the
* merchant added later never lands here: read the choice's own `imageUrl` (the detail store's `imageUrl` does).
*/
imageUrl: string | null;
}
/** One inventory record per variant at the store's default location (fetchInventory). */
export interface VariantInventory {
status: "IN_STOCK" | "OUT_OF_STOCK" | "PREORDER";
/** Units in stock when counted; null when stock is a yes/no flag. */
quantity: number | null;
/** Units still pre-orderable when `status` is PREORDER; null otherwise or when uncounted. */
preorderQuantity: number | null;
preorderMessage: string | null;
}
/** A category breadcrumb: an ancestor (or, on a product, the path to its main category). */
export interface Breadcrumb {
id: string;
name: string;
slug: string;
}
/** A product as the detail page needs it. */
export interface ProductDetail extends ProductSummary {
/** Product description as an HTML string — render with innerHTML, not as text. */
descriptionHtml: string;
/** Merchant info sections (materials, shipping, care…) — title + HTML; render as sections or accordions. */
infoSections: { title: string; html: string }[];
/** Every gallery image as a resolved https URL, main image first, de-duplicated. */
gallery: string[];
/** Path to the product's main category, top-level first ("Clothing" > "T-Shirts"); [] when it has no main category. */
breadcrumbs: Breadcrumb[];
/** Ids of the categories the product is directly assigned to. */
categoryIds: string[];
options: ProductOption[];
modifiers: ProductModifier[];
variants: ProductVariant[];
/** The recurring plans the product is sold on; [] for a one-time-only product. */
subscriptions: SubscriptionPlan[];
/** With plans present: whether a plain one-time purchase is also offered. */
allowOneTimePurchase: boolean;
}
export interface Category {
id: string;
slug: string;
name: string;
/** Plain-text description when the merchant wrote one; "" otherwise. */
description: string;
/** The category's own image, resolved to an https URL; "" when none. */
imageUrl: string;
/** The parent category's id; null for a top-level category. */
parentId: string | null;
/** Position among its siblings (the merchant's order). */
index: number;
/** Products directly in this category (not in its subcategories); null when unknown. */
productCount: number | null;
/** Ancestors, top-level first — only filled by fetchCategoryBySlug; [] elsewhere. */
breadcrumbs: Breadcrumb[];
}
export interface FacetChoice {
/** The choice id — what toggleChoice / searchCatalog filter on. */
id: string;
name: string;
/** Hex color for a swatch facet; null for a text facet. */
colorCode: string | null;
/** Products in the scope carrying this choice (linked choices counted into their primary). */
count: number;
/** Ids of linked choices shown under this one ("Light red" under "Red") — the filter sends them along; [] when none. */
childIds: string[];
}
/** One filterable customization across the catalog (or a category): "Color" with its choices. */
export interface Facet {
/** The customization id (an option's or a modifier's). */
id: string;
name: string;
/** Options and modifiers filter on different fields — the store keeps them apart. */
kind: "option" | "modifier";
isColor: boolean;
choices: FacetChoice[];
}
/** The catalog's (or category's) lowest and highest product price, as numbers in site currency — the slider's bounds. */
export interface PriceRange {
min: number;
max: number;
}
/** What the filter panel needs beyond the product page: the facets and the price bounds of the scope. */
export interface FacetData {
facets: Facet[];
priceRange: PriceRange | null;
}
export interface CartLine {
/** The cart line id — what update/remove take (NOT the product id). */
lineItemId: string;
productName: string;
quantity: number;
/** Per-unit price, formatted. */
unitPrice: string;
/** Line total, formatted. */
linePrice: string;
/** Struck line total (the catalog price × quantity) when the line is discounted; null otherwise. */
compareAtLinePrice: string | null;
/** Units the store can still sell of this line; null when uncounted. The stepper's ceiling. */
availableQuantity: number | null;
/** Resolved https URL ("" when none). */
imageUrl: string;
/** The item's page URL when Wix returned one; "" otherwise (a headless site links by slug). */
productUrl: string;
/** Human-readable option/modifier labels, e.g. ["Color: Ink", "Size: M"]. */
descriptionLines: string[];
/** IN_STOCK | PARTIALLY_IN_STOCK | OUT_OF_STOCK | REMOVED_FROM_CATALOG — not IN_STOCK → the line can't be checked out as-is. */
status: string;
/**
* The recurring plan's terms for a subscription line — "Monthly plan · every month · 12 payments";
* "" for a one-time purchase. A subscription line must read as one in the cart.
*/
subscription: string;
}
/** A named amount from the cart estimate (a discount, a fee, a tax), formatted. */
export interface CartAmount {
name: string;
amount: string;
}
export interface Cart {
lines: CartLine[];
/** Sum of line quantities. */
itemCount: number;
/** Formatted subtotal (after item discounts) — from the cart estimate, not hand-summed. */
subtotal: string;
/** Formatted CART-level discount total (a coupon or cart rule) — "" when none; item discounts are already in subtotal. */
discount: string;
/** Every applied discount by name (coupon, automatic rule) with its amount; [] when none. */
discounts: CartAmount[];
/** Additional fees (handling, platform) with their amounts; [] when none. */
fees: CartAmount[];
/** Taxes by name; [] when none or not yet calculable (no address). */
taxes: CartAmount[];
/** Line prices already include tax — don't render a tax row then. */
pricesIncludeTax: boolean;
/** Formatted total to pay before delivery (shipping resolves at checkout); "" when unknown. */
total: string;
/** The applied coupon; null when none. */
coupon: { id: string; code: string } | null;
/** The buyer's note to the merchant; "" when none. */
note: string;
currency: string;
}
SHA-256: 363ad0be765fafd81486609f28beca49c9174cfc9a5237bbb673c9145a093d95