@godaddy/gd-commerce-storefront 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -5
- package/dist/index.d.ts +232 -178
- package/dist/index.js +237 -72
- package/dist/messages/fr-FR.d.ts +1 -1
- package/dist/messages/fr-FR.js +1 -0
- package/dist/{messages-CHl-bWx9.d.ts → messages-peJIfMO4.d.ts} +1 -0
- package/dist/styles.css +3 -0
- package/docs/server-api.md +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,6 +82,7 @@ A connection failure leaves the surrounding application and its state mounted. C
|
|
|
82
82
|
'--commerce-radius': '0.75rem',
|
|
83
83
|
'--commerce-sale': '#b42318',
|
|
84
84
|
'--commerce-on-sale': '#ffffff',
|
|
85
|
+
'--commerce-card-image-aspect': '1 / 1',
|
|
85
86
|
}}
|
|
86
87
|
>
|
|
87
88
|
{/* Register /collection and /item/:productId in your router. */}
|
|
@@ -108,6 +109,8 @@ The repository example uses React Router 7. Follow each router version's own Rea
|
|
|
108
109
|
| `CommerceStorefront` | Recommended integration: provider and one drawer |
|
|
109
110
|
| `Catalog` | Six products per cursor page, or `pageSize`. Its `title`, `description`, and `showHeader` props are deprecated; compose `CatalogRoot` instead |
|
|
110
111
|
| `CatalogRoot` and catalog parts | Build a [custom catalog or card layout](#custom-catalog-layouts) with the same rules as `Catalog` |
|
|
112
|
+
| `useProductCard`, `useCatalog` | Card and catalog state for your own card parts, such as a category line or a product count |
|
|
113
|
+
| `SampleProduct` | The type of [your own sample products](#your-own-sample-products), for a template shown before a store is connected |
|
|
111
114
|
| `ProductDetails` | Reads `:productId`; validates URL option selections against catalog results. `ProductDetailsRoot` also takes a `productId` prop |
|
|
112
115
|
| `ProductDetailsRoot` and product parts | Build a [custom product layout](#custom-product-layouts) with the same rules as `ProductDetails` |
|
|
113
116
|
| `ProductCard` | Renders one `SKUGroup` with direct add or a product-details link; `ProductCardRoot` and card parts customize it |
|
|
@@ -305,11 +308,25 @@ As with product layouts, the grid, spacing and border classes come from your own
|
|
|
305
308
|
| `ProductCardDescription` | Up to two lines of description; nothing without one. The packaged card leaves it out |
|
|
306
309
|
| `ProductCardAction` | Adds the product directly only when it has one SKU and no options; otherwise links to the product page to choose. Its children replace the label; as a function, they receive the `action`: `add`, `choose-options` or `view-product` |
|
|
307
310
|
|
|
308
|
-
`className` on `CatalogProducts` goes on the element whose direct children are the cards, so a host grid places them, and `className` on `ProductCardRoot` goes on each card, so classes such as `col-span-2` work. While products load, that element holds three placeholder cards, so they follow the host's grid. The error with its retry button and, without a `CatalogEmpty`, the empty message render just before it. Card parts take no product, SKU or price props. Inside `CatalogProducts`, `ProductCardRoot` takes no `product` prop either and throws if it gets one, so a card always shows the product it is rendered for. Samples use the same card
|
|
311
|
+
`className` on `CatalogProducts` goes on the element whose direct children are the cards, so a host grid places them, and `className` on `ProductCardRoot` goes on each card, so classes such as `col-span-2` work. While products load, that element holds three placeholder cards, so they follow the host's grid. The error with its retry button and, without a `CatalogEmpty`, the empty message render just before it. Card parts take no product, SKU or price props. Inside `CatalogProducts`, `ProductCardRoot` takes no `product` prop either and throws if it gets one, so a card always shows the product it is rendered for. Samples use the same card and add only to the sample cart. They have no product page, so their title and image are not links. The packaged samples show a placeholder image and no badge or description; [your own samples](#your-own-sample-products) show their image, description and sale badge. Card images and their loading placeholders are square; set `--commerce-card-image-aspect`, such as `3 / 4`, in the `theme` or on any element around the cards to change that. Card and catalog parts throw if they are rendered outside their root. Packaged cards fill their grid row, with the price and action at the bottom of each card.
|
|
309
312
|
|
|
310
313
|
The root keeps its page cursor in the page URL, as `after`. For a second catalog on the same page, such as a featured row above the main grid, set `urlState={false}` on it so the two do not share one cursor.
|
|
311
314
|
|
|
312
|
-
For a product the host already has, such as a featured product, use `ProductCard` or `ProductCardRoot` with a `product` prop outside the catalog. `useProductCard()` returns read-only `product` (`null` for samples), `name`, `href` (`null` for samples) and `isSample` for building custom card parts.
|
|
315
|
+
For a product the host already has, such as a featured product, use `ProductCard` or `ProductCardRoot` with a `product` prop outside the catalog. `useProductCard()` returns read-only `product` (`null` for samples), `sample` (the sample as you passed it, or `null` for a product), `name`, `href` (`null` for samples) and `isSample` for building custom card parts. `useCatalog()` returns the catalog's `count`, `isSample` and `status` (`loading`, `error` or `ready`) inside `CatalogRoot`, for a custom product count.
|
|
316
|
+
|
|
317
|
+
Elements that are only display, such as a "Bestseller" label, a category line or a "View" link, are your own components, written with these hooks. Write such a component once and read its data where you place it, so the same label works on a card and in the cart:
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
function CategoryLine() {
|
|
321
|
+
const { sample, product } = useProductCard<RoutineSample>();
|
|
322
|
+
// Samples carry your own fields; live products come from your catalog data.
|
|
323
|
+
// The type is not checked, so a sample from another catalog, such as the packaged ones, may lack your fields.
|
|
324
|
+
const category = sample ? sample.category : categoryFor(product);
|
|
325
|
+
return category ? <p className='eyebrow'>{category}</p> : null;
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Every part accepts `className` on an outer wrapper that the package's CSS reset leaves alone, with the same limits as [product parts](#custom-product-layouts).
|
|
313
330
|
|
|
314
331
|
## Custom cart layouts
|
|
315
332
|
|
|
@@ -408,7 +425,7 @@ For a cart page instead of, or as well as, the drawer, compose the same parts in
|
|
|
408
425
|
Two hooks support custom cart markup:
|
|
409
426
|
|
|
410
427
|
- `useCart()` works anywhere inside `CommerceProvider`. It returns read-only `items`, `itemCount`, `subtotal` (minor units, or `null` when the server has not priced the cart), `currency`, `isSample` and `loading`. Combine `itemCount` with `useCommerce().setOpen(true)` for a custom cart button.
|
|
411
|
-
- `useCartItem()` returns the line inside `CartItemRoot`: `id`, `name`, `quantity`, `imageUrl`, `options`, `subtotal` and `
|
|
428
|
+
- `useCartItem()` returns the line inside `CartItemRoot`: `id`, `name`, `quantity`, `imageUrl`, `options`, `subtotal`, `currency` and `sample` (the sample as it was added, or `null` for a store line).
|
|
412
429
|
|
|
413
430
|
## Replacing text
|
|
414
431
|
|
|
@@ -495,6 +512,16 @@ Cart mutations share one queue, including initial cart creation. The cart ID is
|
|
|
495
512
|
|
|
496
513
|
Changing the server-provided cart scope resets cart state and prevents old responses from affecting the new connection. A failed configuration refresh temporarily disables commerce and also invalidates pending responses. Normal configuration loading and failures do not unmount host content.
|
|
497
514
|
|
|
515
|
+
## Troubleshooting
|
|
516
|
+
|
|
517
|
+
**"The store could not be reached" with `returned an HTML page instead of JSON` in the console.** A request under
|
|
518
|
+
`/api/commerce` got the application's HTML page. The Commerce routes are not mounted on this origin, or a static
|
|
519
|
+
file, single-page app or SSR fallback route answers before them. Mount them at `/api/commerce` ahead of those
|
|
520
|
+
routes; with a separate API server in development, proxy `/api/commerce` to it. Opening `/api/commerce/config` in
|
|
521
|
+
the browser should return JSON. A missing route never shows sample products, even with `sampleProducts`: the server
|
|
522
|
+
must report `state: 'unbound'`. See [Before a store is connected](#before-a-store-is-connected) and the server
|
|
523
|
+
package's [example for a site before its store connects](../commerce-server/README.md#a-site-before-its-store-connects).
|
|
524
|
+
|
|
498
525
|
## Guides
|
|
499
526
|
|
|
500
527
|
- [Server API contract](docs/server-api.md)
|
|
@@ -531,8 +558,8 @@ A site can mount the same components before it has a connected store:
|
|
|
531
558
|
</CommerceStorefront>
|
|
532
559
|
```
|
|
533
560
|
|
|
534
|
-
`Catalog.sampleProducts` defaults to `false`.
|
|
535
|
-
(Product 1–6, $10–$60 USD) **only** when `/api/commerce/config` reports `state: 'unbound'`.
|
|
561
|
+
`Catalog.sampleProducts` defaults to `false`. `true` allows six built-in sample products
|
|
562
|
+
(Product 1–6, $10–$60 USD, without images) **only** when `/api/commerce/config` reports `state: 'unbound'`.
|
|
536
563
|
When a store is connected, the server reports a different state and the components show real
|
|
537
564
|
products. You do not need to change props or the component tree. The prop has no effect while
|
|
538
565
|
a store is connected. Real empty catalogs and errors never fall back to samples.
|
|
@@ -561,6 +588,55 @@ state for up to 60 seconds before showing the error. Once polling slows to every
|
|
|
561
588
|
failure is already 60 seconds after the last success, so it shows the error at once. Polling
|
|
562
589
|
stops when the state is `ready`; focus and explicit retry still refresh configuration.
|
|
563
590
|
|
|
591
|
+
#### Your own sample products
|
|
592
|
+
|
|
593
|
+
A template can show its own samples instead, with the same rule. Pass them to `sampleProducts` on
|
|
594
|
+
`Catalog` or `CatalogRoot`:
|
|
595
|
+
|
|
596
|
+
```tsx
|
|
597
|
+
interface RoutineSample extends SampleProduct {
|
|
598
|
+
category: string;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const samples: RoutineSample[] = [
|
|
602
|
+
{
|
|
603
|
+
key: 'daily-cleanser',
|
|
604
|
+
name: 'Daily Cleanser',
|
|
605
|
+
price: 3200,
|
|
606
|
+
description: 'Gentle, pH-balanced foam that removes without stripping.',
|
|
607
|
+
imageUrl: '/images/cleanser.jpg',
|
|
608
|
+
category: 'Cleanse',
|
|
609
|
+
},
|
|
610
|
+
];
|
|
611
|
+
|
|
612
|
+
<Catalog sampleProducts={samples} />
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Each sample needs a unique `key`, a `name` and a `price` in minor units of USD. `compareAtPrice`,
|
|
616
|
+
`description` and `imageUrl` are optional. `imageUrl` is any URL the browser can load, such as a
|
|
617
|
+
file in the site's own public folder; the package does not upload or check it. An image that
|
|
618
|
+
fails to load shows the image placeholder on the card and no image in the cart. `CatalogRoot`
|
|
619
|
+
throws, while no store is connected, for an empty list, an entry that is not an object, a
|
|
620
|
+
duplicate or empty key, a packaged sample's key (`sample-1` to `sample-6`), an empty name, a
|
|
621
|
+
`description` or `imageUrl` that is not a string, or a price or compare-at price that is not a
|
|
622
|
+
whole number of 0 or more. Once a store is connected the samples are not shown, so development
|
|
623
|
+
builds log the problem instead and the store's catalog still renders. A list written inline is
|
|
624
|
+
compared by its values, so it does not rebuild the cards when it is unchanged. In a card with
|
|
625
|
+
`ProductCardTitle`, a sample's image is hidden from screen readers, which read the name from the
|
|
626
|
+
title.
|
|
627
|
+
|
|
628
|
+
The card parts show a sample's image, description, price, and a Sale badge and struck-through
|
|
629
|
+
price when `compareAtPrice` is above `price`. Samples have no product page. Fields of your own,
|
|
630
|
+
such as `category`, come back unchanged from `useProductCard().sample` and, once added, from
|
|
631
|
+
`useCartItem().sample`. The type you pass, as in `useProductCard<RoutineSample>()`, is not
|
|
632
|
+
checked, so where other samples can appear, read your own fields as possibly missing. The sample
|
|
633
|
+
cart keeps each sample as it was added, with its image, so the cart still shows it on a page
|
|
634
|
+
without the catalog. While a catalog with a sample of the same key is mounted, the cart shows
|
|
635
|
+
that sample's current fields, so a price or name loaded later matches the card. The cart lists
|
|
636
|
+
packaged samples in catalog order and your own in the order they were added, after them. Once a
|
|
637
|
+
store is connected, the same card tree shows the store's products, so build the card for both:
|
|
638
|
+
your own parts read `sample` before a store is connected and `product` after.
|
|
639
|
+
|
|
564
640
|
Use a server that reports connection state before you enable `sampleProducts`. Successful
|
|
565
641
|
config responses without `state` still mean ready. A server that does not report `unbound`
|
|
566
642
|
never shows samples. See the [server contract](docs/server-api.md) for the host's
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as StorefrontMessages, i as StorefrontMessageKey, n as MessageValues, o as enUS, r as StorefrontMessage, s as formatMessage, t as CommerceErrorCode } from "./messages-
|
|
1
|
+
import { a as StorefrontMessages, i as StorefrontMessageKey, n as MessageValues, o as enUS, r as StorefrontMessage, s as formatMessage, t as CommerceErrorCode } from "./messages-peJIfMO4.js";
|
|
2
2
|
import { CSSProperties, ReactElement, ReactNode } from "react";
|
|
3
3
|
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
4
|
|
|
@@ -376,9 +376,192 @@ interface AddToCartButtonProps {
|
|
|
376
376
|
}
|
|
377
377
|
declare function AddToCartButton(props: AddToCartButtonProps): ReactElement;
|
|
378
378
|
//#endregion
|
|
379
|
+
//#region src/commerce-provider.d.ts
|
|
380
|
+
interface CommerceContextValue {
|
|
381
|
+
connection: 'loading' | 'unbound' | 'connecting' | 'ready' | 'error';
|
|
382
|
+
connectionError: string | null;
|
|
383
|
+
retryConnection: () => void;
|
|
384
|
+
theme: StorefrontTheme;
|
|
385
|
+
/** The host's swatch lookup, or `undefined` when options render as buttons. */
|
|
386
|
+
getSwatch: GetSwatch | undefined;
|
|
387
|
+
config: StorefrontConfig;
|
|
388
|
+
/** The BCP 47 locale that prices are formatted in, such as `en-US`. */
|
|
389
|
+
locale: string;
|
|
390
|
+
/** The package's text: English, with the provider's `messages` over it. */
|
|
391
|
+
messages: StorefrontMessages;
|
|
392
|
+
cart: CartOrder | null;
|
|
393
|
+
/** Whether the cart drawer is open. Always `false` when no `CartDrawer` or `CartDrawerRoot` is mounted. */
|
|
394
|
+
open: boolean;
|
|
395
|
+
/** Opens or closes the cart drawer. Opening does nothing without a mounted drawer. */
|
|
396
|
+
setOpen: (open: boolean) => void;
|
|
397
|
+
/** Restores the initiating control after the cart drawer closes. */
|
|
398
|
+
restoreFocus: () => void;
|
|
399
|
+
pending: boolean;
|
|
400
|
+
hydrating: boolean;
|
|
401
|
+
error: string | null;
|
|
402
|
+
storageWarning: string | null;
|
|
403
|
+
announcement: string;
|
|
404
|
+
addItem: (item: AddToCartItemInput) => Promise<boolean>;
|
|
405
|
+
changeQuantity: (itemId: string, quantity: number) => Promise<boolean>;
|
|
406
|
+
removeItem: (itemId: string) => Promise<boolean>;
|
|
407
|
+
applyDiscount: (code: string) => Promise<boolean>;
|
|
408
|
+
refresh: () => Promise<boolean>;
|
|
409
|
+
checkout: () => Promise<boolean>;
|
|
410
|
+
}
|
|
411
|
+
declare function useCommerce(): CommerceContextValue;
|
|
412
|
+
/**
|
|
413
|
+
* Formats minor units, such as cents, in the provider's `locale`, the way the package's own parts do. Uses the
|
|
414
|
+
* store's currency unless `currency` is given.
|
|
415
|
+
*/
|
|
416
|
+
declare function useMoney(): (value: number, currency?: string) => string;
|
|
417
|
+
/** `useMoney` for a range, such as `$20.00 – $25.00`, written the way the provider's `locale` writes ranges. */
|
|
418
|
+
declare function useMoneyRange(): (min: number, max: number, currency?: string) => string;
|
|
419
|
+
/**
|
|
420
|
+
* The provider's text for `key`, with `{placeholders}` filled from `values`, the way the package's own parts read
|
|
421
|
+
* it. For custom parts that should match the package's language. English outside a provider.
|
|
422
|
+
*/
|
|
423
|
+
declare function useMessage(): (key: StorefrontMessageKey, values?: MessageValues) => string;
|
|
424
|
+
type StorefrontTheme = CSSProperties & {
|
|
425
|
+
'--commerce-accent'?: string;
|
|
426
|
+
'--commerce-accent-hover'?: string;
|
|
427
|
+
'--commerce-on-accent'?: string;
|
|
428
|
+
'--commerce-surface'?: string;
|
|
429
|
+
'--commerce-text'?: string;
|
|
430
|
+
'--commerce-radius'?: string;
|
|
431
|
+
/** Fills the Sale badge. Defaults to a red that white text reads on. */
|
|
432
|
+
'--commerce-sale'?: string;
|
|
433
|
+
/** Text on the Sale badge. Defaults to white. */
|
|
434
|
+
'--commerce-on-sale'?: string;
|
|
435
|
+
/**
|
|
436
|
+
* The aspect ratio of catalog card images and their loading placeholders, such as `3 / 4`. Defaults to `1 / 1`.
|
|
437
|
+
* Can also be set on a card or any element around it.
|
|
438
|
+
*/
|
|
439
|
+
'--commerce-card-image-aspect'?: string;
|
|
440
|
+
};
|
|
441
|
+
/** The attribute value a host can give a swatch, such as `{ attribute: 'color', name: 'forest', label: 'Forest' }`. */
|
|
442
|
+
interface SwatchValue {
|
|
443
|
+
readonly attribute: string;
|
|
444
|
+
readonly name: string;
|
|
445
|
+
readonly label: string;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Returns a CSS `background` for a value, such as `'#3f6352'` or `'url(/swatches/linen.png) center / cover'`, or
|
|
449
|
+
* nothing for no swatch. An attribute shows swatches only when every value has one.
|
|
450
|
+
*/
|
|
451
|
+
type GetSwatch = (value: SwatchValue) => string | null | undefined;
|
|
452
|
+
interface CommerceProviderProps {
|
|
453
|
+
children: ReactNode;
|
|
454
|
+
catalogPath?: string;
|
|
455
|
+
productPath?: string;
|
|
456
|
+
/** Applied to storefront surfaces and the portalled cart drawer. */
|
|
457
|
+
theme?: StorefrontTheme;
|
|
458
|
+
/**
|
|
459
|
+
* Swatches for attribute values, off by default. Attributes whose values all have a swatch render as swatches on
|
|
460
|
+
* the product page, and cards show them as dots. See `GetSwatch`.
|
|
461
|
+
*/
|
|
462
|
+
getSwatch?: GetSwatch;
|
|
463
|
+
/** Set only when your server has enabled checkout and this return route exists. */
|
|
464
|
+
checkoutSuccessPath?: string;
|
|
465
|
+
/**
|
|
466
|
+
* The BCP 47 locale for formatting prices, such as `de-DE`. Defaults to `en-US`; an invalid tag falls back to it.
|
|
467
|
+
* It never selects UI text; set `messages` for that.
|
|
468
|
+
*/
|
|
469
|
+
locale?: string;
|
|
470
|
+
/**
|
|
471
|
+
* The package's text, such as `frFR` from `@godaddy/gd-commerce-storefront/messages/fr-FR`. Keys left out use
|
|
472
|
+
* English. Independent of `locale`, so set both for a translated store. Part `children` and props such as
|
|
473
|
+
* `CatalogHeader`'s `title` still replace the text where given.
|
|
474
|
+
*/
|
|
475
|
+
messages?: Partial<StorefrontMessages>;
|
|
476
|
+
/**
|
|
477
|
+
* Called after an item is added, live or sample. With a drawer mounted, the drawer also opens. Without one, such as
|
|
478
|
+
* on a site with a cart page, use it to confirm the add.
|
|
479
|
+
*/
|
|
480
|
+
onItemAdded?: (item: AddedCartItem) => void;
|
|
481
|
+
/** Called for every cart change and failure, such as for analytics. See `CommerceEvent`. */
|
|
482
|
+
onEvent?: (event: CommerceEvent) => void;
|
|
483
|
+
}
|
|
484
|
+
/** A cart operation that can fail. */
|
|
485
|
+
type CommerceOperation = 'add' | 'change-quantity' | 'remove' | 'discount' | 'refresh' | 'checkout';
|
|
486
|
+
/**
|
|
487
|
+
* What `onEvent` reports. Live changes are reported once the server returns the updated cart; sample changes have
|
|
488
|
+
* `isSample: true` and never reach the store. New event types may be added, so ignore types you do not handle.
|
|
489
|
+
*/
|
|
490
|
+
type CommerceEvent = {
|
|
491
|
+
readonly type: 'item-added';
|
|
492
|
+
/** `null` for a sample product, which is not a SKU. */
|
|
493
|
+
readonly skuId: string | null;
|
|
494
|
+
readonly name: string;
|
|
495
|
+
readonly quantity: number;
|
|
496
|
+
readonly isSample: boolean;
|
|
497
|
+
} | {
|
|
498
|
+
readonly type: 'quantity-changed';
|
|
499
|
+
readonly itemId: string;
|
|
500
|
+
readonly quantity: number;
|
|
501
|
+
readonly isSample: boolean;
|
|
502
|
+
} | {
|
|
503
|
+
readonly type: 'item-removed';
|
|
504
|
+
readonly itemId: string;
|
|
505
|
+
readonly isSample: boolean;
|
|
506
|
+
} | {
|
|
507
|
+
readonly type: 'discount-applied';
|
|
508
|
+
readonly code: string;
|
|
509
|
+
}
|
|
510
|
+
/** Sent just before the browser leaves for checkout. */ | {
|
|
511
|
+
readonly type: 'checkout-started';
|
|
512
|
+
readonly cartId: string;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* `message` is the one the cart shows, in the provider's language. `code` is the server's failure code, such as
|
|
516
|
+
* `scope_mismatch`, when it sent one.
|
|
517
|
+
*/ | {
|
|
518
|
+
readonly type: 'error';
|
|
519
|
+
readonly operation: CommerceOperation;
|
|
520
|
+
readonly message: string;
|
|
521
|
+
readonly code?: string;
|
|
522
|
+
};
|
|
523
|
+
/** The item passed to `onItemAdded`. */
|
|
524
|
+
interface AddedCartItem {
|
|
525
|
+
readonly name: string;
|
|
526
|
+
readonly quantity: number;
|
|
527
|
+
/** `true` for the sample cart, before a store is connected. */
|
|
528
|
+
readonly isSample: boolean;
|
|
529
|
+
}
|
|
530
|
+
declare function CommerceProvider({
|
|
531
|
+
children,
|
|
532
|
+
catalogPath,
|
|
533
|
+
productPath,
|
|
534
|
+
checkoutSuccessPath,
|
|
535
|
+
theme,
|
|
536
|
+
getSwatch,
|
|
537
|
+
locale,
|
|
538
|
+
messages,
|
|
539
|
+
onItemAdded,
|
|
540
|
+
onEvent
|
|
541
|
+
}: CommerceProviderProps): ReactElement;
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/sample-cart.d.ts
|
|
544
|
+
/**
|
|
545
|
+
* A product to show while no store is connected. Deliberately distinct from SKU groups, SKUs and draft orders: it
|
|
546
|
+
* is never sent to Commerce. Add any fields of your own, such as a category, and read them back from `sample` in
|
|
547
|
+
* `useProductCard()` and `useCartItem()`.
|
|
548
|
+
*/
|
|
549
|
+
interface SampleProduct {
|
|
550
|
+
/** Unique among the site's samples. `sample-1` to `sample-6` are the packaged samples' keys. */
|
|
551
|
+
readonly key: string;
|
|
552
|
+
readonly name: string;
|
|
553
|
+
/** In minor units of `sampleCurrency`, such as 3200 for $32.00. */
|
|
554
|
+
readonly price: number;
|
|
555
|
+
/** A higher earlier price, in minor units. The card then shows the sale badge and the struck-through price. */
|
|
556
|
+
readonly compareAtPrice?: number;
|
|
557
|
+
readonly description?: string;
|
|
558
|
+
/** Any URL the browser can load, such as a file in the site's own public folder. */
|
|
559
|
+
readonly imageUrl?: string;
|
|
560
|
+
}
|
|
561
|
+
//#endregion
|
|
379
562
|
//#region src/cart-state.d.ts
|
|
380
563
|
/** One cart line, for custom cart parts. */
|
|
381
|
-
interface CartItemState {
|
|
564
|
+
interface CartItemState<Sample extends SampleProduct = SampleProduct> {
|
|
382
565
|
/** Empty when the server returned a line without an ID. Such a line cannot be changed or removed. */
|
|
383
566
|
readonly id: string;
|
|
384
567
|
readonly name: string;
|
|
@@ -391,6 +574,8 @@ interface CartItemState {
|
|
|
391
574
|
/** The line total in minor units, or `null` when the server did not price it. */
|
|
392
575
|
readonly subtotal: number | null;
|
|
393
576
|
readonly currency: string;
|
|
577
|
+
/** The sample as it was added, with the site's own fields. `null` for a line in the connected store's cart. */
|
|
578
|
+
readonly sample: Sample | null;
|
|
394
579
|
}
|
|
395
580
|
/** Read-only cart contents: the connected store's cart, or the sample cart while no store is connected. */
|
|
396
581
|
interface CartState {
|
|
@@ -473,8 +658,12 @@ declare function CartClose({
|
|
|
473
658
|
declare function CartNotices({
|
|
474
659
|
className
|
|
475
660
|
}: CartPartProps): ReactElement | null;
|
|
476
|
-
/**
|
|
477
|
-
|
|
661
|
+
/**
|
|
662
|
+
* Read the line from inside `CartItemRoot`, for custom item parts. Pass the type of the site's samples, such as
|
|
663
|
+
* `useCartItem<MySample>()`, to read their own fields from `sample`. The type is not checked: when the cart can hold
|
|
664
|
+
* samples from more than one catalog, read the site's own fields as possibly missing.
|
|
665
|
+
*/
|
|
666
|
+
declare function useCartItem<Sample extends SampleProduct = SampleProduct>(): CartItemState<Sample>;
|
|
478
667
|
interface CartItemsProps extends CartPartProps {
|
|
479
668
|
/** One item, rendered for every cart line. Defaults to the packaged item. */
|
|
480
669
|
children?: ReactNode;
|
|
@@ -574,13 +763,29 @@ declare function CartDrawer(): ReactElement;
|
|
|
574
763
|
interface CatalogPartProps {
|
|
575
764
|
className?: string;
|
|
576
765
|
}
|
|
766
|
+
/** Read-only catalog state for custom catalog parts rendered inside `CatalogRoot`. */
|
|
767
|
+
interface CatalogState {
|
|
768
|
+
/**
|
|
769
|
+
* Products on the current page: `0` while loading or after a first load fails. A failed refetch keeps the last page,
|
|
770
|
+
* so `status` can be `error` while `count` is above `0`.
|
|
771
|
+
*/
|
|
772
|
+
readonly count: number;
|
|
773
|
+
readonly isSample: boolean;
|
|
774
|
+
/** Samples are always `ready`. */
|
|
775
|
+
readonly status: 'loading' | 'error' | 'ready';
|
|
776
|
+
}
|
|
777
|
+
/** Read how many products the catalog shows, and whether they are samples, for a custom summary. */
|
|
778
|
+
declare function useCatalog(): CatalogState;
|
|
577
779
|
interface CatalogRootProps {
|
|
578
780
|
children: ReactNode;
|
|
579
781
|
/** Added to the root `<section>`. */
|
|
580
782
|
className?: string;
|
|
581
|
-
/**
|
|
582
|
-
|
|
583
|
-
|
|
783
|
+
/**
|
|
784
|
+
* Products to show only while the server reports no connected store (`unbound`): `true` for the six packaged
|
|
785
|
+
* samples, or the site's own. Never shown once a store is connected. Defaults to `false`.
|
|
786
|
+
*/
|
|
787
|
+
sampleProducts?: boolean | readonly SampleProduct[];
|
|
788
|
+
/** Products per page, a whole number of 1 or more. Defaults to 6. Samples are always one page. */
|
|
584
789
|
pageSize?: number;
|
|
585
790
|
/**
|
|
586
791
|
* Keep the page cursor in the page URL, as `after`, so a reload or a shared link keeps the page. Defaults to
|
|
@@ -588,7 +793,7 @@ interface CatalogRootProps {
|
|
|
588
793
|
*/
|
|
589
794
|
urlState?: boolean;
|
|
590
795
|
}
|
|
591
|
-
/** Loads one cursor page of products, or the
|
|
796
|
+
/** Loads one cursor page of products, or the samples, and provides them to catalog parts. */
|
|
592
797
|
declare function CatalogRoot({
|
|
593
798
|
children,
|
|
594
799
|
className,
|
|
@@ -649,8 +854,8 @@ interface CatalogProps {
|
|
|
649
854
|
description?: string;
|
|
650
855
|
/** @deprecated To render the page's own H1, compose `CatalogRoot` without `CatalogHeader` instead. */
|
|
651
856
|
showHeader?: boolean;
|
|
652
|
-
/**
|
|
653
|
-
sampleProducts?: boolean;
|
|
857
|
+
/** Products to show only while no store is connected, as in `CatalogRoot`. */
|
|
858
|
+
sampleProducts?: boolean | readonly SampleProduct[];
|
|
654
859
|
/** Products per page. Defaults to 6. */
|
|
655
860
|
pageSize?: number;
|
|
656
861
|
}
|
|
@@ -663,165 +868,6 @@ declare function Catalog({
|
|
|
663
868
|
pageSize
|
|
664
869
|
}: CatalogProps): ReactElement;
|
|
665
870
|
//#endregion
|
|
666
|
-
//#region src/commerce-provider.d.ts
|
|
667
|
-
interface CommerceContextValue {
|
|
668
|
-
connection: 'loading' | 'unbound' | 'connecting' | 'ready' | 'error';
|
|
669
|
-
connectionError: string | null;
|
|
670
|
-
retryConnection: () => void;
|
|
671
|
-
theme: StorefrontTheme;
|
|
672
|
-
/** The host's swatch lookup, or `undefined` when options render as buttons. */
|
|
673
|
-
getSwatch: GetSwatch | undefined;
|
|
674
|
-
config: StorefrontConfig;
|
|
675
|
-
/** The BCP 47 locale that prices are formatted in, such as `en-US`. */
|
|
676
|
-
locale: string;
|
|
677
|
-
/** The package's text: English, with the provider's `messages` over it. */
|
|
678
|
-
messages: StorefrontMessages;
|
|
679
|
-
cart: CartOrder | null;
|
|
680
|
-
/** Whether the cart drawer is open. Always `false` when no `CartDrawer` or `CartDrawerRoot` is mounted. */
|
|
681
|
-
open: boolean;
|
|
682
|
-
/** Opens or closes the cart drawer. Opening does nothing without a mounted drawer. */
|
|
683
|
-
setOpen: (open: boolean) => void;
|
|
684
|
-
/** Restores the initiating control after the cart drawer closes. */
|
|
685
|
-
restoreFocus: () => void;
|
|
686
|
-
pending: boolean;
|
|
687
|
-
hydrating: boolean;
|
|
688
|
-
error: string | null;
|
|
689
|
-
storageWarning: string | null;
|
|
690
|
-
announcement: string;
|
|
691
|
-
addItem: (item: AddToCartItemInput) => Promise<boolean>;
|
|
692
|
-
changeQuantity: (itemId: string, quantity: number) => Promise<boolean>;
|
|
693
|
-
removeItem: (itemId: string) => Promise<boolean>;
|
|
694
|
-
applyDiscount: (code: string) => Promise<boolean>;
|
|
695
|
-
refresh: () => Promise<boolean>;
|
|
696
|
-
checkout: () => Promise<boolean>;
|
|
697
|
-
}
|
|
698
|
-
declare function useCommerce(): CommerceContextValue;
|
|
699
|
-
/**
|
|
700
|
-
* Formats minor units, such as cents, in the provider's `locale`, the way the package's own parts do. Uses the
|
|
701
|
-
* store's currency unless `currency` is given.
|
|
702
|
-
*/
|
|
703
|
-
declare function useMoney(): (value: number, currency?: string) => string;
|
|
704
|
-
/** `useMoney` for a range, such as `$20.00 – $25.00`, written the way the provider's `locale` writes ranges. */
|
|
705
|
-
declare function useMoneyRange(): (min: number, max: number, currency?: string) => string;
|
|
706
|
-
/**
|
|
707
|
-
* The provider's text for `key`, with `{placeholders}` filled from `values`, the way the package's own parts read
|
|
708
|
-
* it. For custom parts that should match the package's language. English outside a provider.
|
|
709
|
-
*/
|
|
710
|
-
declare function useMessage(): (key: StorefrontMessageKey, values?: MessageValues) => string;
|
|
711
|
-
type StorefrontTheme = CSSProperties & {
|
|
712
|
-
'--commerce-accent'?: string;
|
|
713
|
-
'--commerce-accent-hover'?: string;
|
|
714
|
-
'--commerce-on-accent'?: string;
|
|
715
|
-
'--commerce-surface'?: string;
|
|
716
|
-
'--commerce-text'?: string;
|
|
717
|
-
'--commerce-radius'?: string;
|
|
718
|
-
/** Fills the Sale badge. Defaults to a red that white text reads on. */
|
|
719
|
-
'--commerce-sale'?: string;
|
|
720
|
-
/** Text on the Sale badge. Defaults to white. */
|
|
721
|
-
'--commerce-on-sale'?: string;
|
|
722
|
-
};
|
|
723
|
-
/** The attribute value a host can give a swatch, such as `{ attribute: 'color', name: 'forest', label: 'Forest' }`. */
|
|
724
|
-
interface SwatchValue {
|
|
725
|
-
readonly attribute: string;
|
|
726
|
-
readonly name: string;
|
|
727
|
-
readonly label: string;
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Returns a CSS `background` for a value, such as `'#3f6352'` or `'url(/swatches/linen.png) center / cover'`, or
|
|
731
|
-
* nothing for no swatch. An attribute shows swatches only when every value has one.
|
|
732
|
-
*/
|
|
733
|
-
type GetSwatch = (value: SwatchValue) => string | null | undefined;
|
|
734
|
-
interface CommerceProviderProps {
|
|
735
|
-
children: ReactNode;
|
|
736
|
-
catalogPath?: string;
|
|
737
|
-
productPath?: string;
|
|
738
|
-
/** Applied to storefront surfaces and the portalled cart drawer. */
|
|
739
|
-
theme?: StorefrontTheme;
|
|
740
|
-
/**
|
|
741
|
-
* Swatches for attribute values, off by default. Attributes whose values all have a swatch render as swatches on
|
|
742
|
-
* the product page, and cards show them as dots. See `GetSwatch`.
|
|
743
|
-
*/
|
|
744
|
-
getSwatch?: GetSwatch;
|
|
745
|
-
/** Set only when your server has enabled checkout and this return route exists. */
|
|
746
|
-
checkoutSuccessPath?: string;
|
|
747
|
-
/**
|
|
748
|
-
* The BCP 47 locale for formatting prices, such as `de-DE`. Defaults to `en-US`; an invalid tag falls back to it.
|
|
749
|
-
* It never selects UI text; set `messages` for that.
|
|
750
|
-
*/
|
|
751
|
-
locale?: string;
|
|
752
|
-
/**
|
|
753
|
-
* The package's text, such as `frFR` from `@godaddy/gd-commerce-storefront/messages/fr-FR`. Keys left out use
|
|
754
|
-
* English. Independent of `locale`, so set both for a translated store. Part `children` and props such as
|
|
755
|
-
* `CatalogHeader`'s `title` still replace the text where given.
|
|
756
|
-
*/
|
|
757
|
-
messages?: Partial<StorefrontMessages>;
|
|
758
|
-
/**
|
|
759
|
-
* Called after an item is added, live or sample. With a drawer mounted, the drawer also opens. Without one, such as
|
|
760
|
-
* on a site with a cart page, use it to confirm the add.
|
|
761
|
-
*/
|
|
762
|
-
onItemAdded?: (item: AddedCartItem) => void;
|
|
763
|
-
/** Called for every cart change and failure, such as for analytics. See `CommerceEvent`. */
|
|
764
|
-
onEvent?: (event: CommerceEvent) => void;
|
|
765
|
-
}
|
|
766
|
-
/** A cart operation that can fail. */
|
|
767
|
-
type CommerceOperation = 'add' | 'change-quantity' | 'remove' | 'discount' | 'refresh' | 'checkout';
|
|
768
|
-
/**
|
|
769
|
-
* What `onEvent` reports. Live changes are reported once the server returns the updated cart; sample changes have
|
|
770
|
-
* `isSample: true` and never reach the store. New event types may be added, so ignore types you do not handle.
|
|
771
|
-
*/
|
|
772
|
-
type CommerceEvent = {
|
|
773
|
-
readonly type: 'item-added';
|
|
774
|
-
/** `null` for a sample product, which is not a SKU. */
|
|
775
|
-
readonly skuId: string | null;
|
|
776
|
-
readonly name: string;
|
|
777
|
-
readonly quantity: number;
|
|
778
|
-
readonly isSample: boolean;
|
|
779
|
-
} | {
|
|
780
|
-
readonly type: 'quantity-changed';
|
|
781
|
-
readonly itemId: string;
|
|
782
|
-
readonly quantity: number;
|
|
783
|
-
readonly isSample: boolean;
|
|
784
|
-
} | {
|
|
785
|
-
readonly type: 'item-removed';
|
|
786
|
-
readonly itemId: string;
|
|
787
|
-
readonly isSample: boolean;
|
|
788
|
-
} | {
|
|
789
|
-
readonly type: 'discount-applied';
|
|
790
|
-
readonly code: string;
|
|
791
|
-
}
|
|
792
|
-
/** Sent just before the browser leaves for checkout. */ | {
|
|
793
|
-
readonly type: 'checkout-started';
|
|
794
|
-
readonly cartId: string;
|
|
795
|
-
}
|
|
796
|
-
/**
|
|
797
|
-
* `message` is the one the cart shows, in the provider's language. `code` is the server's failure code, such as
|
|
798
|
-
* `scope_mismatch`, when it sent one.
|
|
799
|
-
*/ | {
|
|
800
|
-
readonly type: 'error';
|
|
801
|
-
readonly operation: CommerceOperation;
|
|
802
|
-
readonly message: string;
|
|
803
|
-
readonly code?: string;
|
|
804
|
-
};
|
|
805
|
-
/** The item passed to `onItemAdded`. */
|
|
806
|
-
interface AddedCartItem {
|
|
807
|
-
readonly name: string;
|
|
808
|
-
readonly quantity: number;
|
|
809
|
-
/** `true` for the sample cart, before a store is connected. */
|
|
810
|
-
readonly isSample: boolean;
|
|
811
|
-
}
|
|
812
|
-
declare function CommerceProvider({
|
|
813
|
-
children,
|
|
814
|
-
catalogPath,
|
|
815
|
-
productPath,
|
|
816
|
-
checkoutSuccessPath,
|
|
817
|
-
theme,
|
|
818
|
-
getSwatch,
|
|
819
|
-
locale,
|
|
820
|
-
messages,
|
|
821
|
-
onItemAdded,
|
|
822
|
-
onEvent
|
|
823
|
-
}: CommerceProviderProps): ReactElement;
|
|
824
|
-
//#endregion
|
|
825
871
|
//#region src/commerce-storefront.d.ts
|
|
826
872
|
/** Mount once inside the application's Router and QueryClientProvider. */
|
|
827
873
|
declare function CommerceStorefront({
|
|
@@ -847,9 +893,11 @@ declare function ProductImage({
|
|
|
847
893
|
className
|
|
848
894
|
}: ProductImageProps): ReactElement;
|
|
849
895
|
/** Read-only card state for custom parts rendered inside `ProductCardRoot`. */
|
|
850
|
-
interface ProductCardState {
|
|
851
|
-
/** `null` for a
|
|
896
|
+
interface ProductCardState<Sample extends SampleProduct = SampleProduct> {
|
|
897
|
+
/** `null` for a sample, which is not a catalog product. */
|
|
852
898
|
readonly product: SKUGroup | null;
|
|
899
|
+
/** The sample as the site passed it to `sampleProducts`, with its own fields. `null` for a catalog product. */
|
|
900
|
+
readonly sample: Sample | null;
|
|
853
901
|
readonly name: string;
|
|
854
902
|
/** The product page, or `null` for a sample, which has none. */
|
|
855
903
|
readonly href: string | null;
|
|
@@ -859,8 +907,12 @@ interface ProductCardState {
|
|
|
859
907
|
interface ProductCardPartProps {
|
|
860
908
|
className?: string;
|
|
861
909
|
}
|
|
862
|
-
/**
|
|
863
|
-
|
|
910
|
+
/**
|
|
911
|
+
* Read the card's product or sample, name, and link from inside `ProductCardRoot`. Pass the type of the site's
|
|
912
|
+
* samples, such as `useProductCard<MySample>()`, to read their own fields from `sample`. The type is not checked:
|
|
913
|
+
* on a page that also shows the packaged samples, read the site's own fields as possibly missing.
|
|
914
|
+
*/
|
|
915
|
+
declare function useProductCard<Sample extends SampleProduct = SampleProduct>(): ProductCardState<Sample>;
|
|
864
916
|
interface ProductCardRootProps {
|
|
865
917
|
children: ReactNode;
|
|
866
918
|
/** The catalog product to show. Leave it out inside `CatalogProducts`, which supplies each product. */
|
|
@@ -875,8 +927,9 @@ declare function ProductCardRoot({
|
|
|
875
927
|
className
|
|
876
928
|
}: ProductCardRootProps): ReactElement;
|
|
877
929
|
/**
|
|
878
|
-
* The product's first image, linked to its page.
|
|
879
|
-
* the title is the one link keyboard and screen reader users get
|
|
930
|
+
* The product's first image, linked to its page. A sample shows its `imageUrl`, unlinked, or the packaged samples'
|
|
931
|
+
* placeholder. With a `ProductCardTitle` in the card, the title is the one link keyboard and screen reader users get,
|
|
932
|
+
* and a sample's image is hidden from screen readers so its name is not read twice; without one, the image is exposed.
|
|
880
933
|
*/
|
|
881
934
|
declare function ProductCardImage({
|
|
882
935
|
className
|
|
@@ -885,13 +938,14 @@ declare function ProductCardImage({
|
|
|
885
938
|
declare function ProductCardTitle({
|
|
886
939
|
className
|
|
887
940
|
}: ProductCardPartProps): ReactElement;
|
|
888
|
-
/** Up to
|
|
941
|
+
/** Up to two lines of the product or sample description. Renders nothing without one. */
|
|
889
942
|
declare function ProductCardDescription({
|
|
890
943
|
className
|
|
891
944
|
}: ProductCardPartProps): ReactElement | null;
|
|
892
945
|
/**
|
|
893
946
|
* "Sale" when some SKU's compare-at price is above that SKU's price. Without SKU prices, as in the catalog list, only
|
|
894
|
-
* when the lowest compare-at price is above the highest price.
|
|
947
|
+
* when the lowest compare-at price is above the highest price. For a sample, when its `compareAtPrice` is above its
|
|
948
|
+
* price. Renders nothing otherwise.
|
|
895
949
|
*/
|
|
896
950
|
declare function ProductCardBadge({
|
|
897
951
|
className
|
|
@@ -1133,4 +1187,4 @@ declare function RelatedProducts({
|
|
|
1133
1187
|
//#region src/storefront-surface.d.ts
|
|
1134
1188
|
declare function CommerceStatus(): ReactElement | null;
|
|
1135
1189
|
//#endregion
|
|
1136
|
-
export { AddToCartButton, type AddToCartButtonProps, AddToCartItemInput, type AddToCartLabelState, type AddedCartItem, ApiError, type CartActionPartProps, CartAddress, CartButton, type CartButtonProps, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, CartDescription, CartDiscount, CartDrawer, CartDrawerRoot, type CartDrawerRootProps, CartEmpty, type CartEmptyProps, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, type CartItemRootProps, type CartItemState, CartItems, type CartItemsProps, CartLineItem, CartLineItemDetails, CartNote, CartNotices, CartOrder, type CartPartProps, CartRoot, type CartRootProps, CartSelectedAddon, CartSelectedAddonValue, CartSelectedOption, CartShippingInfo, type CartState, CartSubtotal, CartSummary, type CartSummaryProps, CartSummaryTotals, CartTax, type CartTextPartProps, CartTitle, Catalog, CatalogEmpty, type CatalogEmptyProps, CatalogHeader, type CatalogHeaderProps, CatalogPagination, type CatalogPartProps, CatalogProducts, type CatalogProductsProps, type CatalogProps, CatalogRoot, type CatalogRootProps, CatalogSummary, type CommerceContextValue, type CommerceErrorCode, type CommerceEvent, type CommerceOperation, CommerceProvider, type CommerceProviderProps, CommerceStatus, CommerceStorefront, Connection, ConnectionEdge, DraftOrderContext, type GetSwatch, InventoryCount, LabelFilter, LineItemTotals, MediaObject, type MessageValues, Money, OrderTotals, PageInfo, type PartLabel, PriceRange, ProductCard, ProductCardAction, type ProductCardActionLabelState, type ProductCardActionProps, ProductCardBadge, ProductCardDescription, ProductCardImage, type ProductCardPartProps, ProductCardPrice, type ProductCardProps, ProductCardRoot, type ProductCardRootProps, type ProductCardState, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, type ProductDetailsAddToCartProps, ProductDetailsAvailability, ProductDetailsBackLink, type ProductDetailsBackLinkProps, ProductDetailsBadge, ProductDetailsDescription, type ProductDetailsErrorState, ProductDetailsGallery, ProductDetailsOption, type ProductDetailsOptionProps, type ProductDetailsOptionState, type ProductDetailsOptionValueState, ProductDetailsOptions, type ProductDetailsPartProps, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, type ProductDetailsRootProps, type ProductDetailsState, type ProductDetailsStatus, ProductDetailsTitle, ProductDetailsVariablesInput, ProductGridVariablesInput, ProductImage, type ProductImageProps, RelatedProducts, type RelatedProductsProps, SKU, SKUAttributeValue, SKUGroup, SKUGroupAttribute, SKUGroupAttributeValue, SKUGroupSKU, SKUPrice, SkuGroupResult, SkuGroupVariables, SkuGroupsResult, SkuGroupsVariables, SkuResult, SkuVariables, type StorefrontConfig, type StorefrontMessage, type StorefrontMessageKey, type StorefrontMessages, StorefrontProductAttribute, StorefrontProductAttributeValue, type StorefrontTheme, StringInFilter, type SwatchValue, addToCart, enUS, formatMessage, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getProductName, getSingleMatchedSku, getSingleMatchedSkuId, getSkuAttributeValueNames, getStartingCompareAt, inSentence, isOnSale, isProductOnSale, money, moneyRange, useCart, useCartItem, useCommerce, useMessage, useMoney, useMoneyRange, useProductCard, useProductDetails, useProductDetailsOption };
|
|
1190
|
+
export { AddToCartButton, type AddToCartButtonProps, AddToCartItemInput, type AddToCartLabelState, type AddedCartItem, ApiError, type CartActionPartProps, CartAddress, CartButton, type CartButtonProps, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, CartDescription, CartDiscount, CartDrawer, CartDrawerRoot, type CartDrawerRootProps, CartEmpty, type CartEmptyProps, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, type CartItemRootProps, type CartItemState, CartItems, type CartItemsProps, CartLineItem, CartLineItemDetails, CartNote, CartNotices, CartOrder, type CartPartProps, CartRoot, type CartRootProps, CartSelectedAddon, CartSelectedAddonValue, CartSelectedOption, CartShippingInfo, type CartState, CartSubtotal, CartSummary, type CartSummaryProps, CartSummaryTotals, CartTax, type CartTextPartProps, CartTitle, Catalog, CatalogEmpty, type CatalogEmptyProps, CatalogHeader, type CatalogHeaderProps, CatalogPagination, type CatalogPartProps, CatalogProducts, type CatalogProductsProps, type CatalogProps, CatalogRoot, type CatalogRootProps, type CatalogState, CatalogSummary, type CommerceContextValue, type CommerceErrorCode, type CommerceEvent, type CommerceOperation, CommerceProvider, type CommerceProviderProps, CommerceStatus, CommerceStorefront, Connection, ConnectionEdge, DraftOrderContext, type GetSwatch, InventoryCount, LabelFilter, LineItemTotals, MediaObject, type MessageValues, Money, OrderTotals, PageInfo, type PartLabel, PriceRange, ProductCard, ProductCardAction, type ProductCardActionLabelState, type ProductCardActionProps, ProductCardBadge, ProductCardDescription, ProductCardImage, type ProductCardPartProps, ProductCardPrice, type ProductCardProps, ProductCardRoot, type ProductCardRootProps, type ProductCardState, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, type ProductDetailsAddToCartProps, ProductDetailsAvailability, ProductDetailsBackLink, type ProductDetailsBackLinkProps, ProductDetailsBadge, ProductDetailsDescription, type ProductDetailsErrorState, ProductDetailsGallery, ProductDetailsOption, type ProductDetailsOptionProps, type ProductDetailsOptionState, type ProductDetailsOptionValueState, ProductDetailsOptions, type ProductDetailsPartProps, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, type ProductDetailsRootProps, type ProductDetailsState, type ProductDetailsStatus, ProductDetailsTitle, ProductDetailsVariablesInput, ProductGridVariablesInput, ProductImage, type ProductImageProps, RelatedProducts, type RelatedProductsProps, SKU, SKUAttributeValue, SKUGroup, SKUGroupAttribute, SKUGroupAttributeValue, SKUGroupSKU, SKUPrice, type SampleProduct, SkuGroupResult, SkuGroupVariables, SkuGroupsResult, SkuGroupsVariables, SkuResult, SkuVariables, type StorefrontConfig, type StorefrontMessage, type StorefrontMessageKey, type StorefrontMessages, StorefrontProductAttribute, StorefrontProductAttributeValue, type StorefrontTheme, StringInFilter, type SwatchValue, addToCart, enUS, formatMessage, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getProductName, getSingleMatchedSku, getSingleMatchedSkuId, getSkuAttributeValueNames, getStartingCompareAt, inSentence, isOnSale, isProductOnSale, money, moneyRange, useCart, useCartItem, useCatalog, useCommerce, useMessage, useMoney, useMoneyRange, useProductCard, useProductDetails, useProductDetailsOption };
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const englishText = {
|
|
|
14
14
|
requestFailed: "Commerce request failed. Please retry.",
|
|
15
15
|
requestFailedStatus: "Commerce request failed ({status}). Please retry.",
|
|
16
16
|
requestTimeout: "Commerce information took too long to load. Please retry.",
|
|
17
|
+
apiUnavailable: "The store could not be reached. Please retry.",
|
|
17
18
|
unknownError: "Something went wrong. Please retry.",
|
|
18
19
|
storageUnavailable: "Your browser cannot save the cart. Keep this page open while shopping.",
|
|
19
20
|
invalidAddItem: "Select a product variant and a positive quantity.",
|
|
@@ -219,8 +220,22 @@ var ApiError = class extends Error {
|
|
|
219
220
|
this.text = options?.text;
|
|
220
221
|
}
|
|
221
222
|
};
|
|
223
|
+
const warnedHtmlPaths = /* @__PURE__ */ new Set();
|
|
224
|
+
function warnHtmlResponse(input) {
|
|
225
|
+
if (process.env.NODE_ENV === "production") return;
|
|
226
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
227
|
+
const path = url.split(/[?#]/)[0] ?? url;
|
|
228
|
+
if (warnedHtmlPaths.has(path)) return;
|
|
229
|
+
warnedHtmlPaths.add(path);
|
|
230
|
+
console.error(`[gd-commerce-storefront] ${path} returned an HTML page instead of JSON. Mount the commerce routes at /api/commerce on this origin, before any static file or single-page app fallback route.`);
|
|
231
|
+
}
|
|
222
232
|
const checkedFetch = async (input, init) => {
|
|
223
233
|
const response = await fetch(input, init);
|
|
234
|
+
if (response.status < 500 && /^\s*text\/html\b/i.test(response.headers.get("Content-Type") ?? "")) {
|
|
235
|
+
warnHtmlResponse(input);
|
|
236
|
+
const text = { key: "apiUnavailable" };
|
|
237
|
+
throw new ApiError(formatMessage(enUS, text.key), 502, { text });
|
|
238
|
+
}
|
|
224
239
|
if (!response.ok) {
|
|
225
240
|
const data = await response.text();
|
|
226
241
|
const parsed = (() => {
|
|
@@ -348,16 +363,50 @@ function getCartSummaryTotals(order, fallbackCurrency = "USD") {
|
|
|
348
363
|
|
|
349
364
|
//#endregion
|
|
350
365
|
//#region src/sample-cart.tsx
|
|
351
|
-
|
|
366
|
+
/** The six packaged samples, "Product 1" to "Product 6", without images. */
|
|
367
|
+
const builtInSamples = Array.from({ length: 6 }, (_, index) => ({
|
|
352
368
|
key: `sample-${index + 1}`,
|
|
353
|
-
|
|
369
|
+
name: `Product ${index + 1}`,
|
|
354
370
|
price: (index + 1) * 1e3
|
|
355
371
|
}));
|
|
372
|
+
const builtInNumbers = new Map(builtInSamples.map((sample, index) => [sample.key, index + 1]));
|
|
373
|
+
const builtInNumber = (sample) => builtInNumbers.get(sample.key);
|
|
374
|
+
const isBuiltInSample = (sample) => builtInNumber(sample) !== void 0;
|
|
356
375
|
/** Sample prices are examples, in this currency whatever the store's. */
|
|
357
376
|
const sampleCurrency = "USD";
|
|
358
|
-
/** A sample's name
|
|
359
|
-
const sampleName = (text, sample) =>
|
|
360
|
-
const
|
|
377
|
+
/** A sample's name. Packaged samples are named in the provider's language, such as "Product 1". */
|
|
378
|
+
const sampleName = (text, sample) => {
|
|
379
|
+
const number = builtInNumber(sample);
|
|
380
|
+
return number === void 0 ? sample.name : text("sampleProductName", { number });
|
|
381
|
+
};
|
|
382
|
+
const validPrice = (price) => Number.isSafeInteger(price) && price >= 0;
|
|
383
|
+
/** The first reason host samples could not be shown or added consistently, or `null` when they can. */
|
|
384
|
+
function sampleProblem(samples) {
|
|
385
|
+
if (!samples.length) return "CatalogRoot sampleProducts needs at least one product, or `true`.";
|
|
386
|
+
const keys = /* @__PURE__ */ new Set();
|
|
387
|
+
for (const [index, sample] of samples.entries()) {
|
|
388
|
+
if (typeof sample !== "object" || sample === null) return `CatalogRoot sampleProducts: entry ${index} is ${String(sample)}, not a sample.`;
|
|
389
|
+
if (typeof sample.key !== "string" || !sample.key || keys.has(sample.key)) return `CatalogRoot sampleProducts need unique, non-empty string keys, not "${String(sample.key)}".`;
|
|
390
|
+
if (builtInNumbers.has(sample.key)) return `CatalogRoot sampleProducts: "${sample.key}" is a packaged sample's key. Use another key.`;
|
|
391
|
+
keys.add(sample.key);
|
|
392
|
+
if (typeof sample.name !== "string" || !sample.name.trim()) return `CatalogRoot sampleProducts: "${sample.key}" needs a non-empty name.`;
|
|
393
|
+
for (const field of ["description", "imageUrl"]) if (sample[field] !== void 0 && typeof sample[field] !== "string") return `CatalogRoot sampleProducts: "${sample.key}" has a ${field} that is not a string.`;
|
|
394
|
+
for (const field of ["price", "compareAtPrice"]) {
|
|
395
|
+
const value = sample[field];
|
|
396
|
+
if ((field === "price" || value !== void 0) && !validPrice(value)) return `CatalogRoot sampleProducts: "${sample.key}" has an invalid ${field}, ${String(value)}. A price must be a whole number of minor units, 0 or more.`;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
const sameSample = (a, b) => {
|
|
402
|
+
if (a === b) return true;
|
|
403
|
+
if (typeof a !== "object" || !a || typeof b !== "object" || !b) return false;
|
|
404
|
+
const fields = Object.keys(a);
|
|
405
|
+
return fields.length === Object.keys(b).length && fields.every((field) => Object.is(a[field], b[field]));
|
|
406
|
+
};
|
|
407
|
+
/** Whether two sample lists have the same samples with the same field values, so an inline list can be reused. */
|
|
408
|
+
const sameSamples = (a, b) => a === b || a.length === b.length && a.every((sample, index) => sameSample(sample, b[index]));
|
|
409
|
+
const noLines = [];
|
|
361
410
|
const SampleCartContext = createContext(null);
|
|
362
411
|
function useSampleCart() {
|
|
363
412
|
const value = useContext(SampleCartContext);
|
|
@@ -371,37 +420,73 @@ function SampleCartProvider({ enabled, messages, onAdd, announce, emit, children
|
|
|
371
420
|
active.current = session;
|
|
372
421
|
const [state, setState] = useState({
|
|
373
422
|
session,
|
|
374
|
-
|
|
423
|
+
lines: noLines
|
|
375
424
|
});
|
|
376
425
|
useEffect(() => {
|
|
377
426
|
active.current = session;
|
|
378
427
|
setState({
|
|
379
428
|
session,
|
|
380
|
-
|
|
429
|
+
lines: noLines
|
|
381
430
|
});
|
|
382
431
|
return () => {
|
|
383
432
|
active.current = null;
|
|
384
433
|
};
|
|
385
434
|
}, [session]);
|
|
386
|
-
const
|
|
387
|
-
const items = useMemo(() =>
|
|
388
|
-
|
|
389
|
-
name: sampleName(text,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
435
|
+
const lines = state.session === session && enabled ? state.lines : noLines;
|
|
436
|
+
const items = useMemo(() => [...lines].sort((a, b) => (builtInNumber(a.sample) ?? Number.POSITIVE_INFINITY) - (builtInNumber(b.sample) ?? Number.POSITIVE_INFINITY)).map(({ sample, quantity }) => ({
|
|
437
|
+
key: sample.key,
|
|
438
|
+
name: sampleName(text, sample),
|
|
439
|
+
price: sample.price,
|
|
440
|
+
quantity,
|
|
441
|
+
sample
|
|
442
|
+
})), [lines, text]);
|
|
443
|
+
const refresh = useCallback((samples) => {
|
|
444
|
+
const byKey = new Map(samples.map((sample) => [sample.key, sample]));
|
|
445
|
+
setState((previous) => {
|
|
446
|
+
if (previous.session !== session) return previous;
|
|
447
|
+
let changed = false;
|
|
448
|
+
const next = previous.lines.map((line) => {
|
|
449
|
+
const current = byKey.get(line.sample.key);
|
|
450
|
+
if (!current || current === line.sample) return line;
|
|
451
|
+
changed = true;
|
|
452
|
+
return {
|
|
453
|
+
...line,
|
|
454
|
+
sample: current
|
|
455
|
+
};
|
|
456
|
+
});
|
|
457
|
+
return changed ? {
|
|
458
|
+
session,
|
|
459
|
+
lines: next
|
|
460
|
+
} : previous;
|
|
461
|
+
});
|
|
462
|
+
}, [session]);
|
|
463
|
+
function update(key, quantity, added) {
|
|
464
|
+
if (!enabled || active.current !== session) return false;
|
|
465
|
+
if (!added && !lines.some((line) => line.sample.key === key)) return false;
|
|
394
466
|
if (quantity !== void 0 && (!Number.isSafeInteger(quantity) || quantity < 0 || quantity > 999)) return false;
|
|
395
467
|
setState((previous) => {
|
|
396
|
-
const current = previous.session === session ? previous.
|
|
397
|
-
const
|
|
468
|
+
const current = previous.session === session ? previous.lines : noLines;
|
|
469
|
+
const line = current.find((candidate) => candidate.sample.key === key);
|
|
470
|
+
const next = quantity ?? (line?.quantity ?? 0) + 1;
|
|
398
471
|
if (next > 999) return previous;
|
|
472
|
+
const kept = current.filter((candidate) => candidate !== line);
|
|
473
|
+
if (next === 0) return {
|
|
474
|
+
session,
|
|
475
|
+
lines: kept
|
|
476
|
+
};
|
|
477
|
+
if (!line && added) return {
|
|
478
|
+
session,
|
|
479
|
+
lines: [...current, {
|
|
480
|
+
sample: added,
|
|
481
|
+
quantity: next
|
|
482
|
+
}]
|
|
483
|
+
};
|
|
399
484
|
return {
|
|
400
485
|
session,
|
|
401
|
-
|
|
402
|
-
...
|
|
403
|
-
|
|
404
|
-
}
|
|
486
|
+
lines: current.map((candidate) => candidate === line ? {
|
|
487
|
+
...candidate,
|
|
488
|
+
quantity: next
|
|
489
|
+
} : candidate)
|
|
405
490
|
};
|
|
406
491
|
});
|
|
407
492
|
return true;
|
|
@@ -409,10 +494,10 @@ function SampleCartProvider({ enabled, messages, onAdd, announce, emit, children
|
|
|
409
494
|
return /* @__PURE__ */ jsx(SampleCartContext.Provider, {
|
|
410
495
|
value: {
|
|
411
496
|
items,
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (!
|
|
415
|
-
onAdd(sampleName(text,
|
|
497
|
+
refresh,
|
|
498
|
+
add: (sample) => {
|
|
499
|
+
if (!update(sample.key, void 0, sample)) return false;
|
|
500
|
+
onAdd(sampleName(text, sample));
|
|
416
501
|
return true;
|
|
417
502
|
},
|
|
418
503
|
changeQuantity: async (key, quantity) => {
|
|
@@ -977,20 +1062,21 @@ function useCart() {
|
|
|
977
1062
|
const { cart, config, connection, hydrating } = useCommerce();
|
|
978
1063
|
const sample = useSampleCart();
|
|
979
1064
|
const isSample = connection === "unbound";
|
|
980
|
-
const sampleItems
|
|
1065
|
+
const sampleItems = sample.items;
|
|
981
1066
|
return useMemo(() => {
|
|
982
1067
|
if (isSample) return {
|
|
983
|
-
items: sampleItems
|
|
1068
|
+
items: sampleItems.map((item) => ({
|
|
984
1069
|
id: item.key,
|
|
985
1070
|
name: item.name,
|
|
986
1071
|
quantity: item.quantity,
|
|
987
|
-
imageUrl: null,
|
|
1072
|
+
imageUrl: item.sample.imageUrl || null,
|
|
988
1073
|
options: [],
|
|
989
1074
|
subtotal: item.price * item.quantity,
|
|
990
|
-
currency: sampleCurrency
|
|
1075
|
+
currency: sampleCurrency,
|
|
1076
|
+
sample: item.sample
|
|
991
1077
|
})),
|
|
992
|
-
itemCount: sampleItems
|
|
993
|
-
subtotal: sampleItems
|
|
1078
|
+
itemCount: sampleItems.reduce((total, item) => total + item.quantity, 0),
|
|
1079
|
+
subtotal: sampleItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
994
1080
|
currency: sampleCurrency,
|
|
995
1081
|
isSample,
|
|
996
1082
|
loading: false
|
|
@@ -1009,7 +1095,8 @@ function useCart() {
|
|
|
1009
1095
|
values: option.values ?? []
|
|
1010
1096
|
})),
|
|
1011
1097
|
subtotal: typeof item.totals?.subTotal?.value === "number" ? item.totals.subTotal.value : null,
|
|
1012
|
-
currency: item.totals?.subTotal?.currencyCode ?? currency
|
|
1098
|
+
currency: item.totals?.subTotal?.currencyCode ?? currency,
|
|
1099
|
+
sample: null
|
|
1013
1100
|
})),
|
|
1014
1101
|
itemCount: lines.reduce((total, item) => total + (item.quantity ?? 0), 0),
|
|
1015
1102
|
subtotal: typeof subtotal === "number" ? subtotal : null,
|
|
@@ -1022,7 +1109,7 @@ function useCart() {
|
|
|
1022
1109
|
config.currencyCode,
|
|
1023
1110
|
hydrating,
|
|
1024
1111
|
isSample,
|
|
1025
|
-
sampleItems
|
|
1112
|
+
sampleItems
|
|
1026
1113
|
]);
|
|
1027
1114
|
}
|
|
1028
1115
|
|
|
@@ -1574,7 +1661,11 @@ function useCartItemInternal(part) {
|
|
|
1574
1661
|
if (!item) throw new Error(`${part} must be rendered inside <CartItemRoot>.`);
|
|
1575
1662
|
return item;
|
|
1576
1663
|
}
|
|
1577
|
-
/**
|
|
1664
|
+
/**
|
|
1665
|
+
* Read the line from inside `CartItemRoot`, for custom item parts. Pass the type of the site's samples, such as
|
|
1666
|
+
* `useCartItem<MySample>()`, to read their own fields from `sample`. The type is not checked: when the cart can hold
|
|
1667
|
+
* samples from more than one catalog, read the site's own fields as possibly missing.
|
|
1668
|
+
*/
|
|
1578
1669
|
function useCartItem() {
|
|
1579
1670
|
return useCartItemInternal("useCartItem");
|
|
1580
1671
|
}
|
|
@@ -1630,13 +1721,15 @@ function CartItemRoot({ children, className }) {
|
|
|
1630
1721
|
/** The line's product image. Renders nothing without one. */
|
|
1631
1722
|
function CartItemImage({ className }) {
|
|
1632
1723
|
const { imageUrl } = useCartItemInternal("CartItemImage");
|
|
1633
|
-
|
|
1724
|
+
const [failedUrl, setFailedUrl] = useState(null);
|
|
1725
|
+
if (!imageUrl || imageUrl === failedUrl) return null;
|
|
1634
1726
|
return /* @__PURE__ */ jsx("div", {
|
|
1635
1727
|
...partProps("cart-item-image", className),
|
|
1636
1728
|
children: /* @__PURE__ */ jsx("img", {
|
|
1637
1729
|
src: imageUrl,
|
|
1638
1730
|
alt: "",
|
|
1639
|
-
className: "h-20 w-20 bg-commerce-tint object-cover"
|
|
1731
|
+
className: "h-20 w-20 bg-commerce-tint object-cover",
|
|
1732
|
+
onError: () => setFailedUrl(imageUrl)
|
|
1640
1733
|
})
|
|
1641
1734
|
});
|
|
1642
1735
|
}
|
|
@@ -1973,7 +2066,7 @@ function ProductImage({ url, name, className = "" }) {
|
|
|
1973
2066
|
}
|
|
1974
2067
|
/** Set by `CatalogProducts` for each entry it renders, so the card template needs no product prop. */
|
|
1975
2068
|
const CatalogItemContext = createContext(null);
|
|
1976
|
-
const titleLink = "title
|
|
2069
|
+
const titleLink = "title";
|
|
1977
2070
|
function cardValue(item, productPath, links, sampleNameText) {
|
|
1978
2071
|
if (item.source === "sample") return {
|
|
1979
2072
|
...links,
|
|
@@ -1999,16 +2092,24 @@ function useProductCardInternal(part) {
|
|
|
1999
2092
|
if (!context) throw new Error(`${part} must be rendered inside <ProductCardRoot>.`);
|
|
2000
2093
|
return context;
|
|
2001
2094
|
}
|
|
2002
|
-
/**
|
|
2095
|
+
/**
|
|
2096
|
+
* Read the card's product or sample, name, and link from inside `ProductCardRoot`. Pass the type of the site's
|
|
2097
|
+
* samples, such as `useProductCard<MySample>()`, to read their own fields from `sample`. The type is not checked:
|
|
2098
|
+
* on a page that also shows the packaged samples, read the site's own fields as possibly missing.
|
|
2099
|
+
*/
|
|
2003
2100
|
function useProductCard() {
|
|
2004
|
-
const
|
|
2101
|
+
const card = useProductCardInternal("useProductCard");
|
|
2102
|
+
const { product, name, href, isSample } = card;
|
|
2103
|
+
const sample = card.source === "sample" ? card.sample : null;
|
|
2005
2104
|
return useMemo(() => ({
|
|
2006
2105
|
product,
|
|
2106
|
+
sample,
|
|
2007
2107
|
name,
|
|
2008
2108
|
href,
|
|
2009
2109
|
isSample
|
|
2010
2110
|
}), [
|
|
2011
2111
|
product,
|
|
2112
|
+
sample,
|
|
2012
2113
|
name,
|
|
2013
2114
|
href,
|
|
2014
2115
|
isSample
|
|
@@ -2057,8 +2158,9 @@ function ProductCardRoot({ children, product, className }) {
|
|
|
2057
2158
|
return catalogItem ? card : /* @__PURE__ */ jsx(StorefrontSurface, { children: card });
|
|
2058
2159
|
}
|
|
2059
2160
|
/**
|
|
2060
|
-
* The product's first image, linked to its page.
|
|
2061
|
-
* the title is the one link keyboard and screen reader users get
|
|
2161
|
+
* The product's first image, linked to its page. A sample shows its `imageUrl`, unlinked, or the packaged samples'
|
|
2162
|
+
* placeholder. With a `ProductCardTitle` in the card, the title is the one link keyboard and screen reader users get,
|
|
2163
|
+
* and a sample's image is hidden from screen readers so its name is not read twice; without one, the image is exposed.
|
|
2062
2164
|
*/
|
|
2063
2165
|
function ProductCardImage({ className }) {
|
|
2064
2166
|
const card = useProductCardInternal("ProductCardImage");
|
|
@@ -2068,23 +2170,30 @@ function ProductCardImage({ className }) {
|
|
|
2068
2170
|
to: card.href,
|
|
2069
2171
|
tabIndex: card.imageLinked ? void 0 : -1,
|
|
2070
2172
|
"aria-hidden": card.imageLinked ? void 0 : "true",
|
|
2071
|
-
className: "block aspect-
|
|
2173
|
+
className: "block aspect-(--commerce-card-image-aspect,1/1) overflow-hidden bg-commerce-tint focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current",
|
|
2072
2174
|
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
2073
2175
|
url: getPrimaryImageUrl(card.product),
|
|
2074
2176
|
name: card.name,
|
|
2075
2177
|
className: "motion-safe:transition-transform motion-safe:duration-500 motion-safe:ease-out motion-safe:group-hover:scale-[1.03]"
|
|
2076
2178
|
})
|
|
2077
|
-
}) : /* @__PURE__ */ jsx("div", {
|
|
2179
|
+
}) : isBuiltInSample(card.sample) ? /* @__PURE__ */ jsx("div", {
|
|
2078
2180
|
"aria-hidden": "true",
|
|
2079
|
-
className: "flex aspect-
|
|
2181
|
+
className: "flex aspect-(--commerce-card-image-aspect,1/1) items-center justify-center bg-commerce-tint",
|
|
2080
2182
|
children: /* @__PURE__ */ jsx("div", { className: "h-16 w-16 rounded-full border border-commerce-line" })
|
|
2183
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
2184
|
+
"aria-hidden": card.imageLinked ? void 0 : "true",
|
|
2185
|
+
className: "aspect-(--commerce-card-image-aspect,1/1) overflow-hidden bg-commerce-tint",
|
|
2186
|
+
children: /* @__PURE__ */ jsx(ProductImage, {
|
|
2187
|
+
url: card.sample.imageUrl,
|
|
2188
|
+
name: card.name
|
|
2189
|
+
})
|
|
2081
2190
|
})
|
|
2082
2191
|
});
|
|
2083
2192
|
}
|
|
2084
2193
|
/** The product name as an H2, linked to its page. */
|
|
2085
2194
|
function ProductCardTitle({ className }) {
|
|
2086
2195
|
const { name, href, register } = useProductCardInternal("ProductCardTitle");
|
|
2087
|
-
useEffect(() =>
|
|
2196
|
+
useEffect(() => register(titleLink), [register]);
|
|
2088
2197
|
return /* @__PURE__ */ jsx("div", {
|
|
2089
2198
|
...partProps("product-card-title", className),
|
|
2090
2199
|
children: /* @__PURE__ */ jsx("h2", {
|
|
@@ -2097,15 +2206,16 @@ function ProductCardTitle({ className }) {
|
|
|
2097
2206
|
})
|
|
2098
2207
|
});
|
|
2099
2208
|
}
|
|
2100
|
-
/** Up to
|
|
2209
|
+
/** Up to two lines of the product or sample description. Renders nothing without one. */
|
|
2101
2210
|
function ProductCardDescription({ className }) {
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2211
|
+
const card = useProductCardInternal("ProductCardDescription");
|
|
2212
|
+
const description = card.source === "sample" ? card.sample.description : card.product.description;
|
|
2213
|
+
if (!description) return null;
|
|
2104
2214
|
return /* @__PURE__ */ jsx("div", {
|
|
2105
2215
|
...partProps("product-card-description", className),
|
|
2106
2216
|
children: /* @__PURE__ */ jsx("p", {
|
|
2107
2217
|
className: "line-clamp-2 text-sm leading-6 text-commerce-muted",
|
|
2108
|
-
children:
|
|
2218
|
+
children: description
|
|
2109
2219
|
})
|
|
2110
2220
|
});
|
|
2111
2221
|
}
|
|
@@ -2117,12 +2227,13 @@ const formatRange = (range, money$1, moneyRange$1) => {
|
|
|
2117
2227
|
};
|
|
2118
2228
|
/**
|
|
2119
2229
|
* "Sale" when some SKU's compare-at price is above that SKU's price. Without SKU prices, as in the catalog list, only
|
|
2120
|
-
* when the lowest compare-at price is above the highest price.
|
|
2230
|
+
* when the lowest compare-at price is above the highest price. For a sample, when its `compareAtPrice` is above its
|
|
2231
|
+
* price. Renders nothing otherwise.
|
|
2121
2232
|
*/
|
|
2122
2233
|
function ProductCardBadge({ className }) {
|
|
2123
|
-
const
|
|
2234
|
+
const card = useProductCardInternal("ProductCardBadge");
|
|
2124
2235
|
const text = useMessage();
|
|
2125
|
-
if (!isProductOnSale(product)) return null;
|
|
2236
|
+
if (card.source === "sample" ? sampleCompareAt(card.sample) === null : !isProductOnSale(card.product)) return null;
|
|
2126
2237
|
return /* @__PURE__ */ jsx("div", {
|
|
2127
2238
|
...partProps("product-card-badge", className),
|
|
2128
2239
|
children: /* @__PURE__ */ jsx("span", {
|
|
@@ -2131,6 +2242,7 @@ function ProductCardBadge({ className }) {
|
|
|
2131
2242
|
})
|
|
2132
2243
|
});
|
|
2133
2244
|
}
|
|
2245
|
+
const sampleCompareAt = (sample) => sample.compareAtPrice !== void 0 && sample.compareAtPrice > sample.price ? sample.compareAtPrice : null;
|
|
2134
2246
|
/** The product's price, or its price range when variants differ. A single price on sale shows its compare-at price. */
|
|
2135
2247
|
function ProductCardPrice({ className }) {
|
|
2136
2248
|
const card = useProductCardInternal("ProductCardPrice");
|
|
@@ -2139,8 +2251,10 @@ function ProductCardPrice({ className }) {
|
|
|
2139
2251
|
const message = useMessage();
|
|
2140
2252
|
const text = card.source === "sample" ? money$1(card.sample.price, sampleCurrency) : formatRange(card.product.priceRange, money$1, moneyRange$1) ?? message("priceUnavailable");
|
|
2141
2253
|
const range = card.source === "live" ? card.product.priceRange : null;
|
|
2142
|
-
|
|
2143
|
-
|
|
2254
|
+
let starting = null;
|
|
2255
|
+
if (card.source === "sample") starting = sampleCompareAt(card.sample);
|
|
2256
|
+
else if (range?.min === range?.max) starting = getStartingCompareAt(card.product);
|
|
2257
|
+
const compareAt = starting === null ? null : money$1(starting, card.source === "sample" ? sampleCurrency : void 0);
|
|
2144
2258
|
return /* @__PURE__ */ jsx("div", {
|
|
2145
2259
|
...partProps("product-card-price", className),
|
|
2146
2260
|
children: /* @__PURE__ */ jsxs("p", {
|
|
@@ -2263,12 +2377,12 @@ function SampleAction({ sample, label }) {
|
|
|
2263
2377
|
type: "button",
|
|
2264
2378
|
className: `${secondaryButtonClass} w-full`,
|
|
2265
2379
|
"aria-label": text("addSampleToCart", { name }),
|
|
2266
|
-
onClick: () => add(sample
|
|
2380
|
+
onClick: () => add(sample),
|
|
2267
2381
|
children: text("addToCart")
|
|
2268
2382
|
}) : /* @__PURE__ */ jsxs("button", {
|
|
2269
2383
|
type: "button",
|
|
2270
2384
|
className: `${secondaryButtonClass} w-full`,
|
|
2271
|
-
onClick: () => add(sample
|
|
2385
|
+
onClick: () => add(sample),
|
|
2272
2386
|
children: [custom, /* @__PURE__ */ jsxs("span", {
|
|
2273
2387
|
className: "sr-only",
|
|
2274
2388
|
children: [" ", text("sampleItemName", { name })]
|
|
@@ -2319,14 +2433,39 @@ function useRootParams(urlState) {
|
|
|
2319
2433
|
//#region src/catalog.tsx
|
|
2320
2434
|
const CatalogContext = createContext(null);
|
|
2321
2435
|
const CatalogEmptyContext = createContext(null);
|
|
2322
|
-
const
|
|
2436
|
+
const builtInCatalog = {
|
|
2437
|
+
source: "sample",
|
|
2438
|
+
samples: builtInSamples
|
|
2439
|
+
};
|
|
2323
2440
|
function useCatalogInternal(part) {
|
|
2324
2441
|
const context = useContext(CatalogContext);
|
|
2325
2442
|
if (!context) throw new Error(`${part} must be rendered inside <CatalogRoot>.`);
|
|
2326
2443
|
return context;
|
|
2327
2444
|
}
|
|
2328
|
-
/**
|
|
2329
|
-
function
|
|
2445
|
+
/** Read how many products the catalog shows, and whether they are samples, for a custom summary. */
|
|
2446
|
+
function useCatalog() {
|
|
2447
|
+
const catalog = useCatalogInternal("useCatalog");
|
|
2448
|
+
let count;
|
|
2449
|
+
let status = "ready";
|
|
2450
|
+
if (catalog.source === "sample") count = catalog.samples.length;
|
|
2451
|
+
else {
|
|
2452
|
+
count = catalog.nodes.length;
|
|
2453
|
+
if (catalog.products.isPending) status = "loading";
|
|
2454
|
+
else if (catalog.products.isError) status = "error";
|
|
2455
|
+
}
|
|
2456
|
+
const isSample = catalog.source === "sample";
|
|
2457
|
+
return useMemo(() => ({
|
|
2458
|
+
count,
|
|
2459
|
+
isSample,
|
|
2460
|
+
status
|
|
2461
|
+
}), [
|
|
2462
|
+
count,
|
|
2463
|
+
isSample,
|
|
2464
|
+
status
|
|
2465
|
+
]);
|
|
2466
|
+
}
|
|
2467
|
+
/** Loads one cursor page of products, or the samples, and provides them to catalog parts. */
|
|
2468
|
+
function CatalogRoot({ children, className, sampleProducts = false, pageSize = 6, urlState = true }) {
|
|
2330
2469
|
const { connection } = useCommerce();
|
|
2331
2470
|
const [custom, setCustom] = useState(false);
|
|
2332
2471
|
const { register, has } = usePartRegistry(useCallback((registered) => setCustom(registered("empty")), []));
|
|
@@ -2338,6 +2477,25 @@ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = f
|
|
|
2338
2477
|
custom
|
|
2339
2478
|
}), [register, custom]);
|
|
2340
2479
|
if (!Number.isSafeInteger(pageSize) || pageSize < 1) throw new Error(`CatalogRoot pageSize must be a whole number of 1 or more, not ${pageSize}.`);
|
|
2480
|
+
const hostSamples = useStableSamples(Array.isArray(sampleProducts) ? sampleProducts : null);
|
|
2481
|
+
const problem = useMemo(() => hostSamples && sampleProblem(hostSamples), [hostSamples]);
|
|
2482
|
+
if (problem && connection === "unbound") throw new Error(problem);
|
|
2483
|
+
useEffect(() => {
|
|
2484
|
+
if (problem && process.env.NODE_ENV !== "production") console.error(`[gd-commerce-storefront] ${problem}`);
|
|
2485
|
+
}, [problem]);
|
|
2486
|
+
const { refresh } = useSampleCart();
|
|
2487
|
+
useEffect(() => {
|
|
2488
|
+
if (hostSamples && !problem && connection === "unbound") refresh(hostSamples);
|
|
2489
|
+
}, [
|
|
2490
|
+
hostSamples,
|
|
2491
|
+
problem,
|
|
2492
|
+
connection,
|
|
2493
|
+
refresh
|
|
2494
|
+
]);
|
|
2495
|
+
const sampleCatalog = useMemo(() => hostSamples ? {
|
|
2496
|
+
source: "sample",
|
|
2497
|
+
samples: hostSamples
|
|
2498
|
+
} : builtInCatalog, [hostSamples]);
|
|
2341
2499
|
let content;
|
|
2342
2500
|
if (connection === "ready") content = /* @__PURE__ */ jsx(CatalogLoader, {
|
|
2343
2501
|
className,
|
|
@@ -2345,7 +2503,7 @@ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = f
|
|
|
2345
2503
|
urlState,
|
|
2346
2504
|
children
|
|
2347
2505
|
});
|
|
2348
|
-
else if (connection === "unbound" && sampleProducts
|
|
2506
|
+
else if (connection === "unbound" && sampleProducts) content = /* @__PURE__ */ jsx(CatalogContext.Provider, {
|
|
2349
2507
|
value: sampleCatalog,
|
|
2350
2508
|
children: /* @__PURE__ */ jsx("section", {
|
|
2351
2509
|
...partProps("catalog-root", className),
|
|
@@ -2362,6 +2520,13 @@ function CatalogRoot({ children, className, sampleProducts: sampleProducts$1 = f
|
|
|
2362
2520
|
children: content
|
|
2363
2521
|
}) });
|
|
2364
2522
|
}
|
|
2523
|
+
/** The previous list while the samples and their fields are unchanged, so an inline list does not rebuild the cards. */
|
|
2524
|
+
function useStableSamples(samples) {
|
|
2525
|
+
const [stable, setStable] = useState(samples);
|
|
2526
|
+
const same = stable === samples || !!stable && !!samples && sameSamples(stable, samples);
|
|
2527
|
+
if (!same) setStable(samples);
|
|
2528
|
+
return same ? stable : samples;
|
|
2529
|
+
}
|
|
2365
2530
|
function CatalogLoader({ children, className, pageSize, urlState }) {
|
|
2366
2531
|
const { config } = useCommerce();
|
|
2367
2532
|
const [params, setParams] = useRootParams(urlState);
|
|
@@ -2451,13 +2616,6 @@ function summary(catalog) {
|
|
|
2451
2616
|
}
|
|
2452
2617
|
/** Two columns on phones, then as many 240px columns as fit. Shared with `RelatedProducts`. */
|
|
2453
2618
|
const catalogGridClass = "grid grid-cols-2 gap-x-4 gap-y-10 sm:gap-x-6 md:grid-cols-[repeat(auto-fill,minmax(240px,1fr))] lg:gap-y-12";
|
|
2454
|
-
const sampleItems = sampleProducts.map((sample) => ({
|
|
2455
|
-
key: sample.key,
|
|
2456
|
-
item: {
|
|
2457
|
-
source: "sample",
|
|
2458
|
-
sample
|
|
2459
|
-
}
|
|
2460
|
-
}));
|
|
2461
2619
|
const packagedCard$1 = /* @__PURE__ */ jsx(ProductCardRoot, {
|
|
2462
2620
|
className: packagedCardClass,
|
|
2463
2621
|
children: /* @__PURE__ */ jsx(PackagedCardParts, {})
|
|
@@ -2471,13 +2629,20 @@ function CatalogProducts({ className, children }) {
|
|
|
2471
2629
|
const customEmpty = useContext(CatalogEmptyContext)?.custom ?? false;
|
|
2472
2630
|
const card = hasChildren(children) ? children : packagedCard$1;
|
|
2473
2631
|
const nodes = catalog.source === "live" ? catalog.nodes : null;
|
|
2632
|
+
const samples = catalog.source === "sample" ? catalog.samples : null;
|
|
2474
2633
|
const items = useMemo(() => nodes ? nodes.map((product) => ({
|
|
2475
2634
|
key: product.id ?? "",
|
|
2476
2635
|
item: {
|
|
2477
2636
|
source: "live",
|
|
2478
2637
|
product
|
|
2479
2638
|
}
|
|
2480
|
-
})) :
|
|
2639
|
+
})) : (samples ?? []).map((sample) => ({
|
|
2640
|
+
key: sample.key,
|
|
2641
|
+
item: {
|
|
2642
|
+
source: "sample",
|
|
2643
|
+
sample
|
|
2644
|
+
}
|
|
2645
|
+
})), [nodes, samples]);
|
|
2481
2646
|
const products = catalog.source === "live" ? catalog.products : null;
|
|
2482
2647
|
const { messages } = useCommerce();
|
|
2483
2648
|
const text = useMessage();
|
|
@@ -2506,7 +2671,7 @@ function CatalogProducts({ className, children }) {
|
|
|
2506
2671
|
"aria-hidden": "true",
|
|
2507
2672
|
className: "motion-safe:animate-pulse",
|
|
2508
2673
|
children: [
|
|
2509
|
-
/* @__PURE__ */ jsx("div", { className: "aspect-
|
|
2674
|
+
/* @__PURE__ */ jsx("div", { className: "aspect-(--commerce-card-image-aspect,1/1) bg-commerce-tint-strong" }),
|
|
2510
2675
|
/* @__PURE__ */ jsxs("div", {
|
|
2511
2676
|
className: "space-y-2 pt-3",
|
|
2512
2677
|
children: [/* @__PURE__ */ jsx("div", { className: "h-4 w-2/3 rounded bg-commerce-tint-strong" }), /* @__PURE__ */ jsx("div", { className: "h-4 w-1/4 rounded bg-commerce-tint-strong" })]
|
|
@@ -2566,9 +2731,9 @@ function CatalogPagination({ className }) {
|
|
|
2566
2731
|
});
|
|
2567
2732
|
}
|
|
2568
2733
|
/** The packaged catalog. Compose `CatalogRoot` and the catalog parts for a custom layout. */
|
|
2569
|
-
function Catalog({ title, description, showHeader = true, sampleProducts
|
|
2734
|
+
function Catalog({ title, description, showHeader = true, sampleProducts, pageSize }) {
|
|
2570
2735
|
return /* @__PURE__ */ jsxs(CatalogRoot, {
|
|
2571
|
-
sampleProducts
|
|
2736
|
+
sampleProducts,
|
|
2572
2737
|
pageSize,
|
|
2573
2738
|
children: [
|
|
2574
2739
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -3494,4 +3659,4 @@ function RelatedProducts({ productId: productIdProp, title, limit = 4, className
|
|
|
3494
3659
|
}
|
|
3495
3660
|
|
|
3496
3661
|
//#endregion
|
|
3497
|
-
export { AddToCartButton, ApiError, CartButton, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, CartDescription, CartDrawer, CartDrawerRoot, CartEmpty, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, CartItems, CartNotices, CartRoot, CartSubtotal, CartSummary, CartTitle, Catalog, CatalogEmpty, CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductCardAction, ProductCardBadge, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsBadge, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, ProductImage, RelatedProducts, enUS, formatMessage, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, moneyRange, useCart, useCartItem, useCommerce, useMessage, useMoney, useMoneyRange, useProductCard, useProductDetails, useProductDetailsOption };
|
|
3662
|
+
export { AddToCartButton, ApiError, CartButton, CartCheckout, CartCheckoutNote, CartClose, CartContinueShopping, CartDescription, CartDrawer, CartDrawerRoot, CartEmpty, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity, CartItemRemove, CartItemRoot, CartItems, CartNotices, CartRoot, CartSubtotal, CartSummary, CartTitle, Catalog, CatalogEmpty, CatalogHeader, CatalogPagination, CatalogProducts, CatalogRoot, CatalogSummary, CommerceProvider, CommerceStatus, CommerceStorefront, ProductCard, ProductCardAction, ProductCardBadge, ProductCardDescription, ProductCardImage, ProductCardPrice, ProductCardRoot, ProductCardSwatches, ProductCardTitle, ProductDetails, ProductDetailsAddToCart, ProductDetailsAvailability, ProductDetailsBackLink, ProductDetailsBadge, ProductDetailsDescription, ProductDetailsGallery, ProductDetailsOption, ProductDetailsOptions, ProductDetailsPrice, ProductDetailsPurchase, ProductDetailsPurchaseStatus, ProductDetailsQuantity, ProductDetailsRoot, ProductDetailsTitle, ProductImage, RelatedProducts, enUS, formatMessage, getAvailableInventoryQuantity, getCartSummaryTotals, getImageUrls, getLabeledSkuOptions, getPrimaryImageUrl, getProductAttributes, getSingleMatchedSku, money, moneyRange, useCart, useCartItem, useCatalog, useCommerce, useMessage, useMoney, useMoneyRange, useProductCard, useProductDetails, useProductDetailsOption };
|
package/dist/messages/fr-FR.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as StorefrontMessages } from "../messages-
|
|
1
|
+
import { a as StorefrontMessages } from "../messages-peJIfMO4.js";
|
|
2
2
|
|
|
3
3
|
//#region src/locales/fr-fr.d.ts
|
|
4
4
|
/** The package's text in French (France). Pass it to `CommerceProvider` as `messages`, with `locale='fr-FR'`. */
|
package/dist/messages/fr-FR.js
CHANGED
|
@@ -20,6 +20,7 @@ const frFR = {
|
|
|
20
20
|
requestFailed: "La requête a échoué. Veuillez réessayer.",
|
|
21
21
|
requestFailedStatus: "La requête a échoué ({status}). Veuillez réessayer.",
|
|
22
22
|
requestTimeout: "Le chargement des informations a pris trop de temps. Veuillez réessayer.",
|
|
23
|
+
apiUnavailable: "La boutique est injoignable. Veuillez réessayer.",
|
|
23
24
|
unknownError: "Un problème est survenu. Veuillez réessayer.",
|
|
24
25
|
storageUnavailable: "Votre navigateur ne peut pas enregistrer le panier. Gardez cette page ouverte pendant vos achats.",
|
|
25
26
|
invalidAddItem: "Sélectionnez une variante du produit et une quantité positive.",
|
package/dist/styles.css
CHANGED
|
@@ -311,6 +311,9 @@
|
|
|
311
311
|
.commerce-storefront .inline-flex {
|
|
312
312
|
display: inline-flex;
|
|
313
313
|
}
|
|
314
|
+
.commerce-storefront .aspect-\(--commerce-card-image-aspect\,1\/1\) {
|
|
315
|
+
aspect-ratio: var(--commerce-card-image-aspect,1/1);
|
|
316
|
+
}
|
|
314
317
|
.commerce-storefront .aspect-square {
|
|
315
318
|
aspect-ratio: 1 / 1;
|
|
316
319
|
}
|
package/docs/server-api.md
CHANGED
|
@@ -9,7 +9,7 @@ Server implementations can use GoDaddy Commerce APIs or their existing integrati
|
|
|
9
9
|
- Requests use same-origin browser credentials. JSON writes set `Content-Type: application/json`.
|
|
10
10
|
- Cart, catalog and checkout requests include `X-Commerce-Scope` from `/config`. Validate that scope against the server's current store/channel binding, especially before writes. Treat it as a stale-binding guard, not authorization. Authenticate and authorize requests independently.
|
|
11
11
|
- Read requests use `cache: no-store`, cancellation, and a 15-second timeout. The initial cart creation/add request is also a write and is never retried automatically.
|
|
12
|
-
- Return a non-2xx response with `{ "error": "A useful customer-facing message" }` for failure. HTML error pages are also handled as failures.
|
|
12
|
+
- Return a non-2xx response with `{ "error": "A useful customer-facing message" }` for failure. HTML error pages are also handled as failures. A `text/html` response below 500, such as a single-page app fallback or an HTML 404 page answering an unmounted route, is reported as "The store could not be reached" with status 502, so it never clears a saved cart.
|
|
13
13
|
- Only cart reads with HTTP 404 or 410 clear an expired saved cart. Network errors, 401/403/409/5xx, and other failures preserve the ID and block writes until hydration succeeds.
|
|
14
14
|
- Resolve prices and availability on the server. The cart does not calculate shipping, taxes, or discounts; it explains that they are calculated at checkout. Browser SKU names and quantities are input, not pricing authority. Protect mutations against CSRF as appropriate for the host application's authentication.
|
|
15
15
|
|
|
@@ -31,7 +31,9 @@ Server implementations can use GoDaddy Commerce APIs or their existing integrati
|
|
|
31
31
|
|
|
32
32
|
`/checkout` is required only when checkout is enabled. `/discounts` is needed if the host uses `applyDiscount`. The standard catalog/detail/cart flow uses the other routes. All product/cart IDs in request paths are encoded.
|
|
33
33
|
|
|
34
|
-
The companion `@godaddy/gd-commerce-server` returns 404 from
|
|
34
|
+
The companion `@godaddy/gd-commerce-server` filters `GET /products` to ACTIVE groups available on the server-configured channel. It returns 404 from `GET /products/:id` when the group is missing, inactive, or unavailable on that channel, including direct product links. Browser callers cannot override the configured channel. This browsing filter does not check product-channel eligibility for `GET /skus/:id`, cart creation, or item addition by SKU ID; hosts must enforce any required restrictions on those operations. See the [server package's channel visibility notes][server-channel-visibility] for stale-channel troubleshooting and upstream lookup costs.
|
|
35
|
+
|
|
36
|
+
[server-channel-visibility]: ../../commerce-server/README.md#product-channel-visibility
|
|
35
37
|
|
|
36
38
|
## Configuration
|
|
37
39
|
|