# Product data (https://docs.salla.dev/twilight/react/data/product)





Read this when you build a product page, a card or a slider and need to know what a `Product` looks like and which fields can be missing.

The type is `Product` from `@salla.sa/twilight-theme-engine/types`. Prices are numbers in the store's currency. The fields on this page match what the demo store's API returns today; names in the examples are translated to English.

## Where products come from [#where-products-come-from]

A `Product` reaches your code in three ways. They are the same type but not the same data.

| Source                                                                              | What you get                                                                                                                                   |
| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| [`product.list`](https://docs.salla.dev/twilight/react/api/modules/product) (cards, sliders, search, wishlist)           | A light **list item**: one `image`, no `options`, `skus`, `images` or full detail flags.                                                       |
| [`product.find`](https://docs.salla.dev/twilight/react/api/modules/product) (the [product page](https://docs.salla.dev/twilight/react/routes/product) loader) | The **detail** object: everything in the list item plus `images`, `options`, `skus`, `brand`, `category`, `tags`, `rating`, `bundle` and more. |
| [`useProduct(initial)`](https://docs.salla.dev/twilight/react/hooks/use-product)                                         | The product you gave it, kept live. It returns `{ product, reload }`.                                                                          |

`useProduct` adds no fields. When the customer picks options it replaces `price`, `sale_price`, `regular_price`, `is_on_sale`, `weight`, `sku` and `barcode`, clears `starting_price` and sets `is_out_of_stock`, and `reload()` swaps in a fresh detail object. Pass `{ isPreview: true }` while you only have a list item, so the hook re-seeds when the detail arrives.

[`ProductCard`](https://docs.salla.dev/twilight/react/components/product-card) takes a `Product` (list item or detail). The product page puts the detail object in the loader data as `product`. `ProductContextProvider` and `useProductContext()` from `@salla.sa/twilight-theme-engine/contexts` share one product down your component tree: `useProductContext()` returns `{ product }`, or `null` outside a provider. `HookSlot` does not read it, so a slot handler gets the product only through the slot's `context`.

<Callout type="warn" title="Fields only the detail has">
  `images`, `options`, `skus`, `bundle`, `included_products`, `notify_availability`, `barcode`, `base_currency_price`, `has_read_more`, `can_show_remained_quantity`, `has_size_guide`, `is_wallet_gift_card`, `supports_pickup` and `supports_delivery`. On a list item they are `undefined`. Do not use them in a card without a fallback.
</Callout>

## Read a product in a component [#read-a-product-in-a-component]

```tsx title="app/components/product/ProductSummary.tsx"
import { useProduct } from '@salla.sa/twilight-theme-engine/hooks/useProduct';
import { useMoney } from '@salla.sa/twilight-theme-engine/hooks/useMoney';
import type { Product } from '@salla.sa/twilight-theme-engine/types';

export function ProductSummary({ initial }: { initial: Product }) {
  const { product } = useProduct(initial);
  const { format } = useMoney();

  return (
    <div>
      <img src={product.image?.url} alt={product.image?.alt ?? product.name} />
      <h1>{product.name}</h1>
      {product.is_on_sale ? (
        <p>
          <strong>{format(product.sale_price ?? product.price)}</strong> <s>{format(product.regular_price)}</s>
        </p>
      ) : (
        <p>{format(product.price)}</p>
      )}
      {product.is_out_of_stock && <p>Out of stock</p>}
    </div>
  );
}
```

## Example: a simple product on sale [#example-a-simple-product-on-sale]

The detail object of a product with no options, on sale (trimmed: `description` and `images` are shortened, `images` has 2 of its items).

<JsonExplorer data="productSimple" title="Simple product on sale (demo store)" defaultOpen="1" />

## Example: a product with options [#example-a-product-with-options]

The demo product "Light": one color option, a SKU per value, a 3D main image, and a `starting_price` lower than `price`.

<JsonExplorer data="productWithOptions" title="Product with options (demo store)" defaultOpen="1" />

A list item for the same kind of product is much smaller. Here is the real list item of a product with variants (description removed). Note the string ids, `sale_price: null`, the single `image`, and `has_options` instead of `options`.

<JsonExplorer data="productListItem" title="Product list item (demo store)" defaultOpen="1" />

## Field reference [#field-reference]

The tables are generated from `Product` in `@salla.sa/twilight-theme-engine/types`. "Detail" means the field is reliably present only on `product.find`. Nested objects (images, options, SKUs, category, brand) get their own tables. Prices on `Product` are plain numbers in `currency`; use [`useMoney().format`](https://docs.salla.dev/twilight/react/hooks/use-money) to display them. For the `{ amount, currency }` object, see [shared types](https://docs.salla.dev/twilight/react/data/shared-types).

### Product [#product]

{/* types: ../../../../../theme-engine/src/types/index.ts#Product depth=2 */}

| Field                         | Type                                              | Description                                                                                                                                                                                                  |
| ----------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`                          | `number`                                          | Product id. Number on `products/{id}/details`, but a numeric STRING on list responses (`products`, e.g. "677709975"). Typed `number`: use `Number(product.id)` when you compare ids from a list item.        |
| `name`                        | `string`                                          | Product name in the current language.                                                                                                                                                                        |
| `description`                 | `string`                                          | Product description, as HTML. May be empty.                                                                                                                                                                  |
| `url`                         | `string`                                          | Absolute URL of the product page.                                                                                                                                                                            |
| `promotion_title?`            | `string`                                          | Badge text the merchant set. Can be "" or null.                                                                                                                                                              |
| `subtitle?`                   | `string`                                          | Short line under the name.                                                                                                                                                                                   |
| `type`                        | `ProductType`                                     | Product type.                                                                                                                                                                                                |
| `status`                      | `ProductStatus`                                   | Product availability status.                                                                                                                                                                                 |
| `weight?`                     | `string \| null`                                  | Weight with its unit, as text.                                                                                                                                                                               |
| `calories?`                   | `number \| null`                                  | Calories, for food products.                                                                                                                                                                                 |
| `sku?`                        | `string \| null`                                  | Merchant SKU. Can be "" or null.                                                                                                                                                                             |
| `barcode?`                    | `string \| null`                                  | Barcode. Can be "" or null.                                                                                                                                                                                  |
| `mpn?`                        | `string \| null`                                  | Manufacturer part number. Can be "" or null.                                                                                                                                                                 |
| `gtin?`                       | `string \| null`                                  | Global trade item number. Can be "" or null.                                                                                                                                                                 |
| `price`                       | `string \| number`                                | What the customer pays now. Show this when the product is not on sale.                                                                                                                                       |
| `sale_price`                  | `number \| null`                                  | The discounted price. `null` on list responses when the product is not on sale; `0` on `products/{id}/details` in the same case. Check `is_on_sale` rather than the truthiness of this field.                |
| `regular_price`               | `number`                                          | The price before the discount.                                                                                                                                                                               |
| `starting_price?`             | `number \| null`                                  | Lowest SKU price of a product with variants, otherwise null.                                                                                                                                                 |
| `base_currency_price`         | `number \| { currency: string; amount: number; }` | Price in the store's base currency.                                                                                                                                                                          |
| `currency`                    | `string`                                          | Currency code of the prices.                                                                                                                                                                                 |
| `discount_percentage?`        | `string`                                          | Declared but never sent by the Salla API (absent on list and detail). Compute it from `regular_price` and `price`.                                                                                           |
| `price_as_float?`             | `number`                                          | Price as a plain float.                                                                                                                                                                                      |
| `price_as_float_for_payment?` | `number`                                          | Price as a plain float, as used for payment.                                                                                                                                                                 |
| `currency_for_payment?`       | `string`                                          | Currency code used for payment.                                                                                                                                                                              |
| `quantity?`                   | `number \| null`                                  | Stock on hand. `-1` means UNLIMITED stock (what `products/{id}/details` sends for products without a tracked quantity), not "one short". List responses send `null` and expose `unlimited_quantity` instead. |
| `sold_quantity?`              | `number`                                          | Units sold. Absent or null when not shown.                                                                                                                                                                   |
| `max_quantity`                | `number`                                          | Most the customer can add to the cart. Null on list responses.                                                                                                                                               |
| `discount_ends?`              | `string`                                          | When the discount ends.                                                                                                                                                                                      |
| `rating?`                     | `ProductRating`                                   | Reviews summary. Null on the demo store.                                                                                                                                                                     |
| `category?`                   | `ProductCategory`                                 | The product's category.                                                                                                                                                                                      |
| `brand?`                      | `ProductBrand`                                    | The product's brand.                                                                                                                                                                                         |
| `tags?`                       | `ProductTag[]`                                    | The product's tags. Empty when there are none.                                                                                                                                                               |
| `image`                       | `ProductImage`                                    | The main image. On list items it has only `url`.                                                                                                                                                             |
| `images?`                     | `ProductImage[]`                                  | The image gallery. Detail responses only.                                                                                                                                                                    |
| `options?`                    | `ProductOption[]`                                 | Options the customer picks, such as size or color. Detail responses only.                                                                                                                                    |
| `skus?`                       | `ProductSku[]`                                    | Variant rows. Present only when requested with `with[]=skus`.                                                                                                                                                |
| `notify_availability?`        | `NotifyAvailability`                              | "Notify me when available" settings and subscription state. Detail responses only.                                                                                                                           |
| `donation?`                   | `ProductDonation`                                 | Donation progress, for products of type 'donating'.                                                                                                                                                          |
| `is_taxable`                  | `boolean`                                         | Tax applies to the product.                                                                                                                                                                                  |
| `has_read_more`               | `boolean`                                         | The description is long: collapse it behind "Read more".                                                                                                                                                     |
| `can_add_note`                | `boolean`                                         | The add-to-cart form shows a note field.                                                                                                                                                                     |
| `can_show_remained_quantity`  | `boolean`                                         | Whether to show `quantity` to customers.                                                                                                                                                                     |
| `can_show_sold?`              | `boolean`                                         | Declared but never sent by the Salla API (absent on list and detail).                                                                                                                                        |
| `can_upload_file`             | `boolean`                                         | The add-to-cart form shows a file upload.                                                                                                                                                                    |
| `has_custom_form`             | `boolean`                                         | The merchant added a custom form to the product.                                                                                                                                                             |
| `has_metadata`                | `boolean`                                         | The product has extra spec data.                                                                                                                                                                             |
| `has_options?`                | `boolean`                                         | The product has options.                                                                                                                                                                                     |
| `has_bundle_products?`        | `boolean`                                         | The product is a bundle of other products.                                                                                                                                                                   |
| `is_on_sale`                  | `boolean`                                         | A discount is active.                                                                                                                                                                                        |
| `is_hidden_quantity?`         | `boolean`                                         | The quantity input is hidden (the customer buys one). Sent on `products/{id}/details` only. List responses use the misspelled `is_hidded_quantity`.                                                          |
| `is_hidded_quantity?`         | `boolean`                                         | The API's own (misspelled) name for `is_hidden_quantity` on list responses.                                                                                                                                  |
| `is_available`                | `boolean`                                         | The product can be bought.                                                                                                                                                                                   |
| `is_in_wishlist?`             | `boolean`                                         | Whether the product is in the wishlist. Prefer `useWishlist` over reading this.                                                                                                                              |
| `is_out_of_stock`             | `boolean`                                         | The customer cannot buy it now.                                                                                                                                                                              |
| `is_require_shipping`         | `boolean`                                         | The product needs shipping.                                                                                                                                                                                  |
| `has_3d_image?`               | `boolean`                                         | The product has a 3D image.                                                                                                                                                                                  |
| `has_size_guide`              | `boolean`                                         | A size guide exists for the product.                                                                                                                                                                         |
| `giftable?`                   | `boolean`                                         | The product can be bought as a gift.                                                                                                                                                                         |
| `can_quick_buy?`              | `boolean`                                         | Show a "buy now" button.                                                                                                                                                                                     |
| `show_availability?`          | `boolean`                                         | Show the product's availability state.                                                                                                                                                                       |
| `has_preorder_campaign?`      | `boolean`                                         | A pre-order campaign is active.                                                                                                                                                                              |
| `is_wallet_gift_card?`        | `boolean`                                         | The product is a wallet gift card. Sent on `products/{id}/details`.                                                                                                                                          |
| `supports_pickup?`            | `boolean`                                         | The product can be picked up.                                                                                                                                                                                |
| `supports_delivery?`          | `boolean`                                         | The product can be delivered.                                                                                                                                                                                |
| `included_products?`          | `Product[] \| null`                               | Products bundled into this one (bundle products); `null`/absent otherwise.                                                                                                                                   |
| `add_to_cart_label?`          | `string`                                          | Custom add-to-cart button label. Not sent by the demo store.                                                                                                                                                 |
| `notes?`                      | `string`                                          | The customer's note for the product.                                                                                                                                                                         |
| `digital_files_settings?`     | `JsonObject`                                      | File settings of a digital product.                                                                                                                                                                          |
| `preorder?`                   | `JsonObject`                                      | Pre-order details; `preorder.label` is shown as the card badge.                                                                                                                                              |

#### `rating` [#rating]

| Field   | Type     | Description          |
| ------- | -------- | -------------------- |
| `count` | `number` | Number of ratings.   |
| `stars` | `number` | Average star rating. |

#### `category` [#category]

| Field             | Type             | Description                                       |
| ----------------- | ---------------- | ------------------------------------------------- |
| `id?`             | `number`         | Category id.                                      |
| `name`            | `string`         | Category name. Can be null on list responses.     |
| `url`             | `string`         | Category page URL. Can be null on list responses. |
| `icon?`           | `string`         | CSS icon class of the category.                   |
| `image?`          | `string \| null` | Category image URL, or null.                      |
| `sub_categories?` | `Category[]`     | Child categories.                                 |

#### `category.sub_categories` [#categorysub_categories]

| Field             | Type                 | Description                                                                                                                  |
| ----------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `id`              | `string \| number`   | Id as the API sends it, a hashed string such as "xzjvNQ".                                                                    |
| `id_?`            | `number`             | The raw numeric id, e.g. 868669273.                                                                                          |
| `name`            | `string`             | Category name.                                                                                                               |
| `description?`    | `string`             | Category description.                                                                                                        |
| `url`             | `string`             | Full URL of the category page.                                                                                               |
| `icon?`           | `string \| null`     | CSS icon class, e.g. "sicon-store", or null.                                                                                 |
| `image?`          | `string \| null`     | Flat image URL string (NOT a nested ProductImage object), or null. Aligns with both the API response and Twig.               |
| `products_count?` | `number`             | Number of products in the category.                                                                                          |
| `sub_categories?` | `Category[]`         | Child categories, possibly empty.                                                                                            |
| `items?`          | `Category[] \| null` | Mirror of `sub_categories` on parent categories (an array, possibly empty); `null` on some root categories without children. |

#### `brand` [#brand]

| Field          | Type     | Description                                |
| -------------- | -------- | ------------------------------------------ |
| `id`           | `number` | Brand id. A slug string on the demo store. |
| `name?`        | `string` | Brand name.                                |
| `description?` | `string` | Brand description.                         |
| `url?`         | `string` | Brand page URL.                            |
| `logo?`        | `string` | Brand logo URL.                            |

#### `tags` [#tags]

| Field  | Type     | Description   |
| ------ | -------- | ------------- |
| `id?`  | `number` | Tag id.       |
| `name` | `string` | Tag text.     |
| `url`  | `string` | Tag page URL. |

#### `image` [#image]

| Field                | Type                           | Description                                                                    |
| -------------------- | ------------------------------ | ------------------------------------------------------------------------------ |
| `id?`                | `number`                       | Image id.                                                                      |
| `url?`               | `string`                       | Image URL.                                                                     |
| `alt?`               | `string`                       | Alt text; usually the product name.                                            |
| `video_url?`         | `string`                       | Video URL when the item is a video, otherwise null.                            |
| `video_type?`        | `"video" \| "youtube" \| null` | `youtube` for a YouTube link, `video` for an uploaded file, null for an image. |
| `type?`              | `"image" \| "video"`           | Media kind. The API also sends '3d-image' for a 3D main image.                 |
| `three_d_image_url?` | `string`                       | `.glb` 3D model URL, or "" when none.                                          |
| `main?`              | `boolean`                      | True for the main image.                                                       |
| `sort?`              | `number`                       | Position in the gallery.                                                       |

#### `images` [#images]

Same fields as `image`.

#### `options` [#options]

| Field      | Type                    | Description                                                                                          |
| ---------- | ----------------------- | ---------------------------------------------------------------------------------------------------- |
| `id`       | `number`                | Option id.                                                                                           |
| `name`     | `string`                | Option label.                                                                                        |
| `type`     | `string`                | Kind of input. Unknown types should be treated as a plain list.                                      |
| `required` | `boolean`               | The customer must choose a value.                                                                    |
| `values?`  | `ProductOptionValue[]`  | Values in the older listing shape. Absent on `products/{id}/details`, which sends `details` instead. |
| `details?` | `ProductOptionDetail[]` | Values on a detail response.                                                                         |

#### `options.values` [#optionsvalues]

| Field          | Type      | Description                            |
| -------------- | --------- | -------------------------------------- |
| `id`           | `number`  | Value id.                              |
| `name`         | `string`  | Value label.                           |
| `price?`       | `number`  | Extra price when this value is picked. |
| `image_url?`   | `string`  | Image URL of the value.                |
| `image?`       | `string`  | Image URL of the value.                |
| `is_selected?` | `boolean` | This value is preselected.             |

#### `options.details` [#optionsdetails]

| Field               | Type                      | Description                                                   |
| ------------------- | ------------------------- | ------------------------------------------------------------- |
| `id`                | `number`                  | Value id. This is what a SKU's `related_option_values` lists. |
| `name`              | `string`                  | Value label.                                                  |
| `option_id`         | `number`                  | Id of the option the value belongs to.                        |
| `additional_price`  | `number`                  | Extra price when this value is picked.                        |
| `option_value`      | `string`                  | The raw value: a color hex, an image id, or "".               |
| `image`             | `string`                  | Image URL, or the hex for a color.                            |
| `color`             | `string`                  | Color hex for `color` options.                                |
| `code`              | `string`                  | Merchant code of the value.                                   |
| `is_out`            | `boolean`                 | This value is out of stock.                                   |
| `is_default`        | `0 \| 1`                  | 1 for the preselected value, otherwise 0.                     |
| `skus_availability` | `Record<string, boolean>` | SKU id to in-stock flag, for the SKUs that use this value.    |

#### `skus` [#skus]

| Field                   | Type             | Description                                        |
| ----------------------- | ---------------- | -------------------------------------------------- |
| `id`                    | `number`         | SKU id.                                            |
| `product_id`            | `number`         | Id of the parent product.                          |
| `price`                 | `Money`          | SKU price as a `{ amount, currency }` object.      |
| `regular_price`         | `Money`          | SKU price before any discount.                     |
| `sale_price`            | `Money \| null`  | Discounted SKU price, or null when not discounted. |
| `has_special_price`     | `boolean`        | A discount applies to this SKU.                    |
| `stock_quantity`        | `number \| null` | Stock on hand, or null when unlimited.             |
| `unlimited_quantity`    | `boolean`        | Stock is not tracked.                              |
| `is_default`            | `boolean`        | This is the default SKU.                           |
| `related_options`       | `number[]`       | Ids of the options the SKU is made of.             |
| `related_option_values` | `number[]`       | Ids of the option values that select this SKU.     |

#### `skus.price` [#skusprice]

| Field        | Type     | Description                              |
| ------------ | -------- | ---------------------------------------- |
| `amount`     | `number` | The amount as a number.                  |
| `currency`   | `string` | Currency code.                           |
| `formatted?` | `string` | Ready-made text, when the API sends one. |

#### `skus.regular_price` [#skusregular_price]

Same fields as `skus.price`.

#### `skus.sale_price` [#skussale_price]

Same fields as `skus.price`.

#### `notify_availability` [#notify_availability]

| Field                 | Type                     | Description                                                          |
| --------------------- | ------------------------ | -------------------------------------------------------------------- |
| `channels`            | `string[]`               | Channels the customer can be notified through.                       |
| `subscribed`          | `boolean`                | The customer has asked to be notified.                               |
| `subscribed_options?` | `number[] \| JsonObject` | Options the customer subscribed to, when they subscribed per option. |
| `options?`            | `boolean`                | Whether subscription is offered per option.                          |

#### `donation` [#donation]

| Field              | Type      | Description                                                                        |
| ------------------ | --------- | ---------------------------------------------------------------------------------- |
| `target_message?`  | `string`  | Message shown with the donation goal.                                              |
| `collected_amount` | `number`  | Amount collected so far.                                                           |
| `target_amount`    | `number`  | Amount the donation aims to collect.                                               |
| `target_percent`   | `number`  | Progress towards the goal, as a percentage.                                        |
| `target_end_date?` | `string`  | When the donation campaign ends.                                                   |
| `can_donate`       | `boolean` | Donations are currently accepted. `ProductCard` hides the price when this is true. |

{/* /types */}

### Allowed values and detail-only flags [#allowed-values-and-detail-only-flags]

`type` is one of `product`, `service`, `group_products`, `codes`, `digital`, `food`, `donating`, `booking` or `financial_support`. `status` is one of `sale`, `out`, `out-and-notify` or `hidden`. An option's `type` is `text`, `textarea`, `radio`, `checkbox`, `select`, `image` or `color`; the demo store also sends `thumbnail` and `single-option`. `has_options` is `true` on list items and is not on the detail object, so check `options?.length` on a detail.

### What `useProduct` returns [#what-useproduct-returns]

{/* types: ../../../../../theme-engine/src/hooks/useProduct.ts#UseProductResult depth=0 */}

| Field     | Type                  | Description                                       |
| --------- | --------------------- | ------------------------------------------------- |
| `product` | `Product`             | Current product state (updates on option changes) |
| `reload`  | `() => Promise<void>` | Reload product data from API                      |

{/* /types */}

### Variants by product type [#variants-by-product-type]

The engine has no separate type per kind of product. It reads `type` and these fields:

| Kind                                    | `type`                                          | What is different                                                                                                                                                                     |
| --------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Donation                                | `donating`                                      | `donation` is `{ target_message?, collected_amount, target_amount, target_percent, target_end_date?, can_donate }`. `ProductCard` hides the price when `donation.can_donate` is true. |
| Digital                                 | `digital`                                       | `digital_files_settings` holds the file settings. Check `is_require_shipping` to know if it ships.                                                                                    |
| Booking                                 | `booking`                                       | The quantity is fixed to 1 in the add-to-cart form.                                                                                                                                   |
| Group bundle                            | `group_products`                                | `bundle` and `included_products` hold the products it is made of, and `has_bundle_products` is the flag.                                                                              |
| Service, food, codes, financial support | `service`, `food`, `codes`, `financial_support` | No extra fields in the type. `calories` is for `food`.                                                                                                                                |

## Common tasks [#common-tasks]

### Show the discount percentage [#show-the-discount-percentage]

```tsx
function discountPercent(product: Product): number {
  if (!product.is_on_sale || !product.regular_price) return 0;
  return Math.round((1 - Number(product.price) / product.regular_price) * 100);
}
```

For `regular_price: 199` and `price: 188` it returns `6`. Merchants often type the same text in `promotion_title`, so the badge below may already say it.

### Is it in stock? [#is-it-in-stock]

```tsx
const { product } = useProduct(initial);
const canBuy = product.is_available && !product.is_out_of_stock;
```

`useProduct` flips `is_out_of_stock` when the SDK reports that the chosen option combination is not available.

To show a stock message, treat `quantity === -1` as unlimited:

```tsx
const unlimited = product.quantity === -1;
const remaining = product.can_show_remained_quantity && !unlimited ? product.quantity : null;
```

### Which variant is selected? [#which-variant-is-selected]

The product object does not store the customer's choice. Keep the chosen value ids yourself (from your option inputs), then find the SKU:

```tsx
import type { Product } from '@salla.sa/twilight-theme-engine/types';

function findSku(product: Product, selectedValueIds: number[]) {
  return product.skus?.find(
    (sku) =>
      sku.related_option_values.length === selectedValueIds.length &&
      selectedValueIds.every((id) => sku.related_option_values.includes(id))
  );
}
```

Then `sku.price.amount` is the price of that variant and `sku.stock_quantity` its stock (`null` means unlimited).

### Image gallery URLs [#image-gallery-urls]

```tsx
const urls = product.images?.map((img) => img.url) ?? [product.image?.url];
const main = product.images?.find((img) => img.main)?.url ?? product.image?.url;
```

Check `type` before you render an item as `<img>`: it can be a video (`video_url`) or a 3D image (`three_d_image_url`).

### Show the badge [#show-the-badge]

There is no `badge` field. The card chooses one text in this order: `preorder.label`, `promotion_title`, then (with `withQuantity`) the remaining `quantity`, then the out-of-stock label.

```tsx
const badge = (typeof product.preorder?.label === 'string' && product.preorder.label) || product.promotion_title || null;
```

### Digital, booking and donation [#digital-booking-and-donation]

```tsx
if (product.type === 'donating' && product.donation) {
  const { collected_amount, target_amount, target_percent, can_donate } = product.donation;
}
const needsShipping = product.is_require_shipping;
const fixedQuantity = product.type === 'booking' || product.is_hidden_quantity;
```

## Gotchas [#gotchas]

* **Ids differ by source.** Lists send string ids, detail sends numbers, but the type says `number`. Compare with `String(a.id) === String(b.id)`, and pass `Number(product.id)` to [`useWishlist`](https://docs.salla.dev/twilight/react/hooks/use-wishlist).
* **`sale_price` when there is no sale.** With no sale it can be `null`, `0` or equal to `price`. Branch on `is_on_sale`, not on `sale_price`.
* **Prices are numbers on `Product`** but `{ amount, currency }` objects on `skus[]` (and `base_currency_price`). See [shared types](https://docs.salla.dev/twilight/react/data/shared-types).
* **`null`, empty and missing all happen.** `promotion_title` can be `""` or `null`; `rating` is `null` without reviews; list items leave out whole sections. Use optional chaining and `?? []`.
* **`options` can be `[]`** on a product without options, not missing. Check `.length`.
* **List items are partial.** `image` has no `alt`; `category.url`, `brand.url` and `tags[].url` can be `null`; `quantity` and `max_quantity` are `null`; `is_hidden_quantity` is spelled `is_hidded_quantity`.
* **`description` is HTML** written by the merchant. Render it with `dangerouslySetInnerHTML`, and pass it through `stripUnsafeHtml` from `@salla.sa/twilight-theme-engine/utils` first. List items from the API may have it HTML-escaped (`&lt;p&gt;`), so use the detail description on a product page.
* **Live changes are browser-only.** On the server `useProduct` returns exactly the product you passed.

## Next [#next]

* [`useProduct`](https://docs.salla.dev/twilight/react/hooks/use-product) and [`ProductCard`](https://docs.salla.dev/twilight/react/components/product-card)
* [`product` API module](https://docs.salla.dev/twilight/react/api/modules/product) for `find`, `list` and the query keys
* [Product route](https://docs.salla.dev/twilight/react/routes/product)
* [Shared types](https://docs.salla.dev/twilight/react/data/shared-types) for `Money` and friends
