Skip to content
Twilight React Playground
ثيم رائدaren

Product

interfaceBeginnerlive demo

Describes a product as the products API returns it: name, prices, images, stock flags, category, brand and tags.

import { Product, ProductType, ProductStatus, ProductImage, ProductRating, ProductCategory, ProductBrand, ProductTag } from '@salla.sa/twilight-theme-engine/types';

In plain words

A type describes the shape of data: which fields an object has, and what kind of value each one holds. Product is the shape of every product the engine hands you, in a product grid, on the product page or in a home block.

You import it with import type. Your editor then autocompletes product.name and flags a typo such as product.nmae before the code runs. The type is removed when the theme is built, so it never checks the real data: the Salla API can send a value the type does not expect, and the live demo shows where it does.

A product comes in two sizes. A list card (from a product list, a home block or search) has the basics and one image. The details of one product (what the product page loads) add images, options and skus.

Signature

interface Product {
  id: number;                          // a string on list cards: see Gotchas
  name: string;
  description: string;                 // HTML
  url: string;
  type: ProductType;
  status: ProductStatus;
  promotion_title?: string;  subtitle?: string;
  sku?: string | null;  mpn?: string | null;  gtin?: string | null;
  weight?: string | null;  calories?: number | null;

  // Prices
  price: number | string;              // what the shopper pays now
  regular_price: number;               // the price before a sale
  sale_price: number;
  starting_price?: number | null;
  base_currency_price: { currency: string; amount: number } | number;
  currency: string;
  discount_percentage?: string;  discount_ends?: string;

  // Stock
  quantity?: number | null;  sold_quantity?: number;  max_quantity: number;

  // Related data
  image: ProductImage;  images?: ProductImage[];
  rating?: ProductRating;  category?: ProductCategory;  brand?: ProductBrand;  tags?: ProductTag[];
  options?: ProductOption[];  skus?: ProductSku[];
  notify_availability?: NotifyAvailability;  donation?: ProductDonation;

  // Flags
  is_available: boolean;  is_out_of_stock: boolean;  is_on_sale: boolean;
  is_hidden_quantity: boolean;  is_taxable: boolean;  is_require_shipping: boolean;
  has_options?: boolean;  has_read_more: boolean;  has_size_guide: boolean;
  can_add_note: boolean;  can_upload_file: boolean;  giftable?: boolean;  is_in_wishlist?: boolean;
  // …and more flags, add_to_cart_label?, notes?, digital_files_settings?, preorder?
}

type ProductType =
  | 'product' | 'service' | 'group_products' | 'codes' | 'digital'
  | 'food' | 'donating' | 'booking' | 'financial_support';

type ProductStatus = 'sale' | 'out' | 'out-and-notify' | 'hidden';

interface ProductImage {
  id?: number;  url?: string;  alt?: string;  type?: 'image' | 'video';
  video_url?: string;  three_d_image_url?: string;  main?: boolean;  sort?: number;
}
interface ProductRating { count: number; stars: number }
interface ProductCategory {
  id?: number;  name: string;  url: string;  icon?: string;
  image?: string | null;  sub_categories?: Category[];
}
interface ProductBrand { id: number; name?: string; description?: string; url?: string; logo?: string }
interface ProductTag { id?: number; name: string; url: string }

Try it live

One real product from the demo store, as a list card and as details, next to what Product declares. ✗ marks a value the type does not allow.Try this: turn on Only mismatches: id changes kind between the two columns, and a list card has no is_hidden_quantity.
Storefront canvas · ar · RTL

Loading the latest products…

Controls
Only mismatches
What a theme writes
import type { Product } from '@salla.sa/twilight-theme-engine/types';

/** Lists send ids as strings and details as numbers: compare them as strings. */
export function isSameProduct(a: Pick<Product, 'id'>, b: Pick<Product, 'id'>) {
  return String(a.id) === String(b.id);
}

/** `sale_price` is null, 0 or a copy of `price` unless the product is on sale. */
export function wasPrice(product: Product): number | null {
  return product.is_on_sale ? product.regular_price : null;
}

Example

app/components/product/PriceTag.tsx
import type { Product } from '@salla.sa/twilight-theme-engine/types';
import { useMoney } from '@salla.sa/twilight-theme-engine/hooks/useMoney';

type PriceTagProps = {
  // Pick keeps only the fields this component reads.
  product: Pick<Product, 'price' | 'regular_price' | 'is_on_sale'>;
};

export function PriceTag({ product }: PriceTagProps) {
  const { format } = useMoney();

  return (
    <p className="price-tag">
      <strong>{format(product.price)}</strong>
      {product.is_on_sale && <s>{format(product.regular_price)}</s>}
    </p>
  );
}

How it behaves

  • A type only: import type { Product } is erased from the built theme. The same type is used by ProductCard, ProductDetails, AddToCartForm, useProduct, the product context, and product.list and product.find in @salla.sa/twilight-theme-engine/api/product.

  • List cards and details share this one type, which is why images, options, skus and many flags are optional. On the demo store a list card has no images, options, skus, base_currency_price, has_read_more or can_show_remained_quantity.

  • Prices on the demo store: price is the amount to pay now, regular_price the price before a sale, and starting_price, when set, equals the cheapest variant. The engine ProductCard shows sale_price only when is_on_sale is true.

  • base_currency_price arrives as an object { currency, amount } in details on the demo store. The engine reads it with typeof product.base_currency_price === "object" (AddToCartForm, ProductDetails).

  • quantity was -1 in details and null on list cards for every demo store product checked. Stock per variant is skus[].stock_quantity; see ProductOption, ProductSku and friends.

Gotchas

  • id is typed number, but list cards send it as a string ("1303461379") and details as a number (demo store), so a.id === b.id is false for the same product. Compare String(a.id) === String(b.id), pass String(id) to product.queries.detail, and Number(id) to useWishlist().has.

  • List cards spell the flag is_hidded_quantity; only details send is_hidden_quantity (demo store). On a card product.is_hidden_quantity is undefined, so !product.is_hidden_quantity shows a stock count the merchant hid. Decide it from the details, or read both spellings.

  • sale_price is typed number, but when the product is not on sale it is null, 0 or a copy of price (demo store lists and details). product.sale_price ?? product.price can give 0. Check is_on_sale first; price already holds the amount to pay.

  • max_quantity is typed number but list cards send null, and brand.id is typed number but details send a path such as "test-link/brand-1321220630" (demo store). Treat both as data to pass along, never as numbers to calculate with.

  • Many optional fields arrive as null rather than missing: rating, quantity, category.url on a card, and image.url for a product without a photo (demo store). product.rating !== undefined is true for null; test with != null, or use ?..

  • images[].type can be "3d-image" (demo store details), which the "image" | "video" union does not list, so a switch over those two cases skips it.

  • docs/07-data-types.md shows images and options as required, is_giftable, and discount_ends?: Date. The code has optional images and options, giftable, and discount_ends?: string: product.is_giftable does not compile.

Related

Source and docs