ProductOption, ProductSku and friends
Describes a product's options and their choices, the SKU variants those choices select, back-in-stock alerts and donation progress.
import { ProductOption, ProductOptionDetail, ProductOptionValue, ProductSku, NotifyAvailability, ProductDonation } from '@salla.sa/twilight-theme-engine/types';In plain words
A chair might come in three colors and two sizes. Each of those is an option (ProductOption), and each option has choices (ProductOptionDetail, such as "Red"). A SKU (ProductSku) is one combination of choices, with its own price and stock.
Only the details of a product carry this. findSku in the example finds the SKU for the choices a shopper made, and the live demo runs it on real demo store products.
Signature
interface ProductOption {
id: number; name: string; required: boolean;
type: 'text' | 'textarea' | 'radio' | 'checkbox' | 'select' | 'image' | 'color' | string;
details?: ProductOptionDetail[]; // what products/{id}/details sends
values?: ProductOptionValue[]; // the listing shape; absent from details
}
interface ProductOptionDetail {
id: number; option_id: number; name: string;
additional_price: number;
option_value: string; image: string; color: string; code: string;
is_out: boolean;
is_default: 0 | 1;
skus_availability: Record<string, boolean>; // sku id → available
}
interface ProductSku {
id: number; product_id: number;
price: Money; regular_price: Money; sale_price: Money | null; has_special_price: boolean;
stock_quantity: number | null; unlimited_quantity: boolean;
is_default: boolean;
related_options: number[]; // option ids
related_option_values: number[]; // choice (ProductOptionDetail) ids
}
interface ProductOptionValue {
id: number; name: string; price?: number; image_url?: string; image?: string; is_selected?: boolean;
}
interface NotifyAvailability {
channels: ('sms' | 'email' | string)[];
subscribed: boolean;
subscribed_options?: number[] | JsonObject;
options?: boolean;
}
interface ProductDonation {
collected_amount: number; target_amount: number; target_percent: number;
can_donate: boolean; target_message?: string; target_end_date?: string;
}Try it live
options[].details and skus: choose one value per option and the matching ProductSku is found by its related_option_values.Try this: choose a value for every option and compare the SKU price with the product price. Product 2 has options that pick no variant; product 4 has choices marked out.Loading the latest products…
import type { Product, ProductSku } from '@salla.sa/twilight-theme-engine/types';
/**
* The variant for the chosen values, or undefined until every option that
* takes part in a variant has one. `chosen` maps an option id to a choice id.
*/
export function findSku(product: Product, chosen: Record<number, number>): ProductSku | undefined {
const skus = product.skus ?? []; // only products/{id}/details sends skus
const optionIds = [...new Set(skus.flatMap((sku) => sku.related_options))];
const picked = optionIds.map((optionId) => chosen[optionId]);
if (picked.some((choiceId) => choiceId === undefined)) return undefined;
return skus.find((sku) => picked.every((choiceId) => sku.related_option_values.includes(choiceId)));
}
Example
import type { Product, ProductSku } from '@salla.sa/twilight-theme-engine/types';
/**
* The variant for the chosen values, or undefined until every option that
* takes part in a variant has one. `chosen` maps an option id to a choice id.
*/
export function findSku(product: Product, chosen: Record<number, number>): ProductSku | undefined {
const skus = product.skus ?? [];
const optionIds = [...new Set(skus.flatMap((sku) => sku.related_options))];
const picked = optionIds.map((optionId) => chosen[optionId]);
if (picked.some((choiceId) => choiceId === undefined)) return undefined;
return skus.find((sku) => picked.every((choiceId) => sku.related_option_values.includes(choiceId)));
}
How it behaves
Only
product.findandproduct.queries.detail(products/{id}/details) returnoptions[].detailsandskus. A list card has neither, but itshas_optionssays whether the product has options.related_optionsnames the options that form variants. Options a shopper types into (textareaandnumberon the demo store) are not in it and senddetails: [].On the demo store a SKU without a stock limit has
stock_quantity: nullandunlimited_quantity: true, and a choice whose SKU is out of stock hasis_out: true.The engine
AddToCartFormdoes not match SKUs itself: it handsproduct.optionsto Salla's product options web component and passesnotify_availability(channels,subscribed_options) to the add-to-cart button.useProductthen follows the SDK's price-updated events.ProductDonationis the donation progress of adonatingproduct;ProductCardadds ans-product-card-donationclass when a product has one.
Gotchas
ProductOption.typelists"radio","checkbox"and"select", but the demo store sends"single-option","multiple-options","thumbnail","color","country","textarea"and"number". The trailing| stringlets any value compile, so aswitchon the listed names silently matches nothing.values(ProductOptionValue) is absent from details: the choices are indetails. Code written forvaluesfinds no choices on a product page.is_defaultis0 | 1on a choice but abooleanon a SKU.choice.is_default === trueis alwaysfalse; testchoice.is_default === 1.Product-level
quantitywas-1in details for every demo store product checked, including ones whose SKUs have 5 or 0 left. Read stock from the chosen SKU.notify_availability.subscribed_optionsarrives asnullon the demo store, which its type (number[] | JsonObject, optional) does not allow. Test it with!= null.
Related
Describes a product as the products API returns it: name, prices, images, stock flags, category, brand and tags.
product.findFetches one product with everything a product page needs: images, options, SKUs, brand, tags, rating and bundle contents.
AddToCartFormThe product page buy box: options, notes and files, live price, quantity and the Add to cart button, submitted through Salla's SDK.
useProductKeeps a live copy of a product that follows price and stock changes from Salla option pickers, and can reload its details.