@godaddy/gd-commerce-storefront 0.1.2 → 0.2.0
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 +43 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +213 -28
- package/dist/styles.css +0 -2
- package/docs/server-api.md +37 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ A connection failure leaves the surrounding application and its state mounted. C
|
|
|
84
84
|
|
|
85
85
|
The stylesheet includes all required utilities and scopes them to the package's surfaces. The build removes CSS layer wrappers in their declared order, so the exported CSS can pass through a host Tailwind v3 PostCSS pipeline without `@tailwind` directives. Import it directly; consumers do not need to copy or rewrite the CSS. It does not add a global reset or require dependency scanning by a host Tailwind build. The `theme` prop reaches the drawer even though it is portalled into `document.body`.
|
|
86
86
|
|
|
87
|
-
By default, `.commerce-storefront` wrappers
|
|
87
|
+
By default, `.commerce-storefront` wrappers do not paint a background and inherit the host's text color. Set page and section backgrounds on the host application's layout elements. `--commerce-surface` colors controls and the opaque cart drawer, and supplies the base for muted colors; it does not color component wrappers. Set `--commerce-surface` and `--commerce-text` together to adapt controls and the drawer to a dark theme. Avoid setting wrapper backgrounds through CSS or the `theme` prop's `background`, `backgroundColor`, or `backgroundImage` properties, which can still override the transparent default. Descendants inherit the surface text and body font instead of global heading styles. Keep text, controls and focus indicators accessible when changing colors. Utility class names and internal markup are not a customization API.
|
|
88
88
|
|
|
89
89
|
The first release uses English UI text and `en-US` currency formatting. Catalog title and description are configurable. Full localization and arbitrary component slots are outside this initial API.
|
|
90
90
|
|
|
@@ -144,3 +144,45 @@ Build before testing: artifact tests check the compiled JavaScript and shipped C
|
|
|
144
144
|
## License
|
|
145
145
|
|
|
146
146
|
[MIT](LICENSE.md).
|
|
147
|
+
|
|
148
|
+
### Before a store is connected
|
|
149
|
+
|
|
150
|
+
A site can mount the same components before it has a connected store:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
<CommerceStorefront theme={theme}>
|
|
154
|
+
<Header><CartButton /></Header>
|
|
155
|
+
<Catalog sampleProducts />
|
|
156
|
+
</CommerceStorefront>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`Catalog.sampleProducts` defaults to `false`. It allows six built-in sample products
|
|
160
|
+
(Product 1–6, $10–$60 USD) **only** when `/api/commerce/config` reports `state: 'unbound'`.
|
|
161
|
+
When a store is connected, the server reports a different state and the components show real
|
|
162
|
+
products. You do not need to change props or the component tree. The prop has no effect while
|
|
163
|
+
a store is connected. Real empty catalogs and errors never fall back to samples.
|
|
164
|
+
|
|
165
|
+
The sample cart supports adding items, changing quantities (1–999), removing items, and an
|
|
166
|
+
example subtotal. It uses the existing drawer and theme, keeps its items during navigation under
|
|
167
|
+
the shared provider, and resets on reload, unmount, or any change away from `unbound`. It uses no
|
|
168
|
+
live cart storage and makes no product, cart, checkout, or upstream Commerce requests.
|
|
169
|
+
Configuration reads continue. Sample entries are not SKUs or draft orders and never move into a
|
|
170
|
+
real cart. Checkout is unavailable, including direct `useCommerce().checkout()` calls.
|
|
171
|
+
|
|
172
|
+
`useCommerce().connection` now includes `unbound` and `connecting` in addition to `loading`,
|
|
173
|
+
`ready`, and `error`. Only `ready` means a usable store connection. The public `cart` and live
|
|
174
|
+
mutation methods apply only to the connected store; sample cart data is internal. A catalog
|
|
175
|
+
without `sampleProducts`, and direct product-detail visits, show the unconnected state.
|
|
176
|
+
Sample grids and drawers have the attribute `data-commerce-source="sample"`.
|
|
177
|
+
|
|
178
|
+
The provider reads configuration on mount and focus, and every five seconds while the page is
|
|
179
|
+
visible and the state is `unbound` or `connecting`. The first failed refresh hides samples and
|
|
180
|
+
clears the sample cart; a later `unbound` response starts an empty sample cart. While
|
|
181
|
+
connecting, refresh failures keep the connecting state for up to 60 seconds before showing
|
|
182
|
+
the error. Polling stops
|
|
183
|
+
when the state is `ready`; focus and explicit retry still refresh configuration.
|
|
184
|
+
|
|
185
|
+
Use a server that reports connection state before you enable `sampleProducts`. Successful
|
|
186
|
+
config responses without `state` still mean ready. A server that does not report `unbound`
|
|
187
|
+
never shows samples. See the [server contract](docs/server-api.md) for the host's
|
|
188
|
+
responsibilities.
|
package/dist/index.d.ts
CHANGED
|
@@ -325,12 +325,14 @@ interface CatalogProps {
|
|
|
325
325
|
title?: string;
|
|
326
326
|
description?: string;
|
|
327
327
|
showHeader?: boolean;
|
|
328
|
+
/** Permit built-in examples only while the host confirms this site is unbound. */
|
|
329
|
+
sampleProducts?: boolean;
|
|
328
330
|
}
|
|
329
331
|
declare function Catalog(props: CatalogProps): ReactElement;
|
|
330
332
|
//#endregion
|
|
331
333
|
//#region src/commerce-provider.d.ts
|
|
332
334
|
interface CommerceContextValue {
|
|
333
|
-
connection: 'loading' | 'ready' | 'error';
|
|
335
|
+
connection: 'loading' | 'unbound' | 'connecting' | 'ready' | 'error';
|
|
334
336
|
connectionError: string | null;
|
|
335
337
|
retryConnection: () => void;
|
|
336
338
|
theme: StorefrontTheme;
|
package/dist/index.js
CHANGED
|
@@ -148,6 +148,82 @@ function getProductAttributes(skuGroup) {
|
|
|
148
148
|
}) ?? [];
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/sample-cart.tsx
|
|
153
|
+
const sampleProducts = Array.from({ length: 6 }, (_, index) => ({
|
|
154
|
+
key: `sample-${index + 1}`,
|
|
155
|
+
name: `Product ${index + 1}`,
|
|
156
|
+
price: (index + 1) * 1e3
|
|
157
|
+
}));
|
|
158
|
+
const SampleCartContext = createContext(null);
|
|
159
|
+
function useSampleCart() {
|
|
160
|
+
const value = useContext(SampleCartContext);
|
|
161
|
+
if (!value) throw new Error("Mount CommerceProvider around sample products.");
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
function SampleCartProvider({ enabled, onAdd, announce, children }) {
|
|
165
|
+
const session = useMemo(() => ({ enabled }), [enabled]);
|
|
166
|
+
const active = useRef(session);
|
|
167
|
+
active.current = session;
|
|
168
|
+
const [state, setState] = useState({
|
|
169
|
+
session,
|
|
170
|
+
quantities: {}
|
|
171
|
+
});
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
active.current = session;
|
|
174
|
+
setState({
|
|
175
|
+
session,
|
|
176
|
+
quantities: {}
|
|
177
|
+
});
|
|
178
|
+
return () => {
|
|
179
|
+
active.current = null;
|
|
180
|
+
};
|
|
181
|
+
}, [session]);
|
|
182
|
+
const quantities = state.session === session && enabled ? state.quantities : {};
|
|
183
|
+
function update(key, quantity) {
|
|
184
|
+
if (!enabled || active.current !== session || !sampleProducts.some((product) => product.key === key)) return false;
|
|
185
|
+
if (quantity !== void 0 && (!Number.isSafeInteger(quantity) || quantity < 0 || quantity > 999)) return false;
|
|
186
|
+
setState((previous) => {
|
|
187
|
+
const current = previous.session === session ? previous.quantities : {};
|
|
188
|
+
const next = quantity ?? (current[key] ?? 0) + 1;
|
|
189
|
+
if (next > 999) return previous;
|
|
190
|
+
return {
|
|
191
|
+
session,
|
|
192
|
+
quantities: {
|
|
193
|
+
...current,
|
|
194
|
+
[key]: next
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return /* @__PURE__ */ jsx(SampleCartContext.Provider, {
|
|
201
|
+
value: {
|
|
202
|
+
items: sampleProducts.flatMap((product) => quantities[product.key] ? [{
|
|
203
|
+
...product,
|
|
204
|
+
quantity: quantities[product.key]
|
|
205
|
+
}] : []),
|
|
206
|
+
add: (key) => {
|
|
207
|
+
const product = sampleProducts.find((product$1) => product$1.key === key);
|
|
208
|
+
if (!product || !update(key)) return false;
|
|
209
|
+
onAdd(product.name);
|
|
210
|
+
return true;
|
|
211
|
+
},
|
|
212
|
+
changeQuantity: async (key, quantity) => {
|
|
213
|
+
const updated = quantity >= 1 && update(key, quantity);
|
|
214
|
+
if (updated) announce("Sample cart quantity updated.");
|
|
215
|
+
return updated;
|
|
216
|
+
},
|
|
217
|
+
remove: async (key) => {
|
|
218
|
+
const updated = update(key, 0);
|
|
219
|
+
if (updated) announce("Sample item removed from cart.");
|
|
220
|
+
return updated;
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
children
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
151
227
|
//#endregion
|
|
152
228
|
//#region src/commerce-provider.tsx
|
|
153
229
|
const CommerceContext = createContext(null);
|
|
@@ -156,10 +232,19 @@ function useCommerce() {
|
|
|
156
232
|
if (!context) throw new Error("Mount CommerceProvider once around the header and page outlet, inside the router.");
|
|
157
233
|
return context;
|
|
158
234
|
}
|
|
235
|
+
const CONNECTING_FAILURE_GRACE_MS = 6e4;
|
|
159
236
|
async function loadConfig(signal) {
|
|
160
237
|
const config = await request("/config", { signal });
|
|
161
|
-
if (
|
|
162
|
-
|
|
238
|
+
if (config?.state === "unbound" || config?.state === "connecting") {
|
|
239
|
+
if ("cartScope" in config || "currencyCode" in config) throw new Error("The store returned invalid configuration.");
|
|
240
|
+
return { state: config.state };
|
|
241
|
+
}
|
|
242
|
+
if (!config || config.state !== void 0 && config.state !== "ready" || typeof config.cartScope !== "string" || !config.cartScope.trim() || typeof config.currencyCode !== "string" || !/^[A-Z]{3}$/.test(config.currencyCode)) throw new Error("The store returned invalid configuration.");
|
|
243
|
+
return {
|
|
244
|
+
state: "ready",
|
|
245
|
+
cartScope: config.cartScope,
|
|
246
|
+
currencyCode: config.currencyCode
|
|
247
|
+
};
|
|
163
248
|
}
|
|
164
249
|
function CommerceProvider({ children, catalogPath = "/shop", productPath = "/products", checkoutSuccessPath, theme = {} }) {
|
|
165
250
|
const config = useQuery({
|
|
@@ -167,19 +252,25 @@ function CommerceProvider({ children, catalogPath = "/shop", productPath = "/pro
|
|
|
167
252
|
queryKey: ["commerce", "configuration"],
|
|
168
253
|
queryFn: ({ signal }) => loadConfig(signal),
|
|
169
254
|
staleTime: 3e4,
|
|
170
|
-
|
|
255
|
+
refetchOnMount: "always",
|
|
256
|
+
refetchOnWindowFocus: "always",
|
|
257
|
+
refetchInterval: (query) => query.state.data?.state === "unbound" || query.state.data?.state === "connecting" ? 5e3 : false,
|
|
258
|
+
refetchIntervalInBackground: false
|
|
171
259
|
});
|
|
260
|
+
const holdConnecting = config.isError && config.data?.state === "connecting" && config.errorUpdatedAt - config.dataUpdatedAt < CONNECTING_FAILURE_GRACE_MS;
|
|
261
|
+
let connection = config.data?.state ?? "loading";
|
|
262
|
+
if (config.isError) connection = holdConnecting ? "connecting" : "error";
|
|
172
263
|
return /* @__PURE__ */ jsx(BoundCommerceProvider, {
|
|
173
264
|
config: {
|
|
174
265
|
cartScope: "",
|
|
175
266
|
currencyCode: "USD",
|
|
176
|
-
...config.data,
|
|
267
|
+
...config.data?.state === "ready" ? config.data : {},
|
|
177
268
|
catalogPath,
|
|
178
269
|
productPath,
|
|
179
270
|
checkoutSuccessPath
|
|
180
271
|
},
|
|
181
|
-
connection
|
|
182
|
-
connectionError:
|
|
272
|
+
connection,
|
|
273
|
+
connectionError: connection === "error" ? message(config.error) : null,
|
|
183
274
|
retryConnection: () => {
|
|
184
275
|
config.refetch();
|
|
185
276
|
},
|
|
@@ -193,8 +284,8 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
193
284
|
const ready = connection === "ready";
|
|
194
285
|
const session = useMemo(() => ({
|
|
195
286
|
cartIdStorage,
|
|
196
|
-
|
|
197
|
-
}), [cartIdStorage,
|
|
287
|
+
connection
|
|
288
|
+
}), [cartIdStorage, connection]);
|
|
198
289
|
const currentSession = useRef(session);
|
|
199
290
|
currentSession.current = session;
|
|
200
291
|
const readyRef = useRef(ready);
|
|
@@ -299,6 +390,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
299
390
|
setOpen(false);
|
|
300
391
|
setError(null);
|
|
301
392
|
setStorageWarning(null);
|
|
393
|
+
setAnnouncement("");
|
|
302
394
|
setPending(false);
|
|
303
395
|
setHydrating(ready);
|
|
304
396
|
if (ready) refresh();
|
|
@@ -315,7 +407,7 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
315
407
|
window.removeEventListener("storage", onStorage);
|
|
316
408
|
window.removeEventListener("focus", onFocus);
|
|
317
409
|
};
|
|
318
|
-
}, [cartIdStorage,
|
|
410
|
+
}, [cartIdStorage, connection]);
|
|
319
411
|
function addItem(item) {
|
|
320
412
|
rememberOpener();
|
|
321
413
|
return run(async () => {
|
|
@@ -426,7 +518,16 @@ function BoundCommerceProvider({ config, children, connection, connectionError,
|
|
|
426
518
|
refresh,
|
|
427
519
|
checkout
|
|
428
520
|
},
|
|
429
|
-
children: [
|
|
521
|
+
children: [/* @__PURE__ */ jsx(SampleCartProvider, {
|
|
522
|
+
enabled: connection === "unbound",
|
|
523
|
+
announce: setAnnouncement,
|
|
524
|
+
onAdd: (name) => {
|
|
525
|
+
rememberOpener();
|
|
526
|
+
setAnnouncement(`${name} added to your sample cart.`);
|
|
527
|
+
setOpen(true);
|
|
528
|
+
},
|
|
529
|
+
children
|
|
530
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
430
531
|
className: "commerce-sr-only",
|
|
431
532
|
role: "status",
|
|
432
533
|
"aria-live": "polite",
|
|
@@ -448,7 +549,10 @@ function StorefrontSurface({ children, className = "" }) {
|
|
|
448
549
|
function CommerceStatus() {
|
|
449
550
|
const { connection, connectionError, retryConnection } = useCommerce();
|
|
450
551
|
if (connection === "ready") return null;
|
|
451
|
-
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "
|
|
552
|
+
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "unbound" ? /* @__PURE__ */ jsx("p", {
|
|
553
|
+
role: "status",
|
|
554
|
+
children: "Connect a store to show products."
|
|
555
|
+
}) : connection === "loading" || connection === "connecting" ? /* @__PURE__ */ jsx("p", {
|
|
452
556
|
role: "status",
|
|
453
557
|
children: "Connecting to the store…"
|
|
454
558
|
}) : /* @__PURE__ */ jsxs("div", {
|
|
@@ -468,13 +572,14 @@ const buttonClass = "inline-flex min-h-11 items-center justify-center gap-2 roun
|
|
|
468
572
|
const inputClass = "min-h-11 rounded-lg border border-neutral-400 bg-white px-3 py-2 text-neutral-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-900";
|
|
469
573
|
function CartButton() {
|
|
470
574
|
const { cart, setOpen, open, connection } = useCommerce();
|
|
471
|
-
const
|
|
575
|
+
const sample = useSampleCart();
|
|
576
|
+
const count = connection === "unbound" ? sample.items.reduce((total, item) => total + item.quantity, 0) : cart?.lineItems?.reduce((total, item) => total + (item.quantity ?? 0), 0) ?? 0;
|
|
472
577
|
return /* @__PURE__ */ jsx(StorefrontSurface, {
|
|
473
578
|
className: "commerce-inline",
|
|
474
579
|
children: /* @__PURE__ */ jsxs("button", {
|
|
475
580
|
type: "button",
|
|
476
581
|
className: buttonClass,
|
|
477
|
-
disabled: connection !== "ready",
|
|
582
|
+
disabled: connection !== "ready" && connection !== "unbound",
|
|
478
583
|
"aria-haspopup": "dialog",
|
|
479
584
|
"aria-expanded": open,
|
|
480
585
|
onClick: () => setOpen(true),
|
|
@@ -521,13 +626,34 @@ function AddToCartButton({ sku, name, quantity = 1 }) {
|
|
|
521
626
|
});
|
|
522
627
|
}
|
|
523
628
|
function CartDrawer() {
|
|
524
|
-
const { config, cart, open, setOpen, restoreFocus, pending, hydrating, error, storageWarning, changeQuantity, removeItem, refresh, checkout } = useCommerce();
|
|
629
|
+
const { config, connection, cart, open, setOpen, restoreFocus, pending, hydrating, error, storageWarning, changeQuantity: changeLiveQuantity, removeItem: removeLiveItem, refresh, checkout } = useCommerce();
|
|
630
|
+
const sample = useSampleCart();
|
|
631
|
+
const isSample = connection === "unbound";
|
|
632
|
+
const changeQuantity = isSample ? sample.changeQuantity : changeLiveQuantity;
|
|
633
|
+
const removeItem = isSample ? sample.remove : removeLiveItem;
|
|
525
634
|
const [drawerAction, setDrawerAction] = useState(null);
|
|
526
635
|
const checkingOut = drawerAction === "checkout";
|
|
527
636
|
const locked = pending || hydrating || drawerAction !== null;
|
|
528
|
-
const items =
|
|
637
|
+
const items = isSample ? sample.items.map((item) => ({
|
|
638
|
+
id: item.key,
|
|
639
|
+
name: item.name,
|
|
640
|
+
quantity: item.quantity,
|
|
641
|
+
image: void 0,
|
|
642
|
+
options: void 0,
|
|
643
|
+
subtotal: item.price * item.quantity,
|
|
644
|
+
currency: "USD"
|
|
645
|
+
})) : (cart?.lineItems ?? []).map((item) => ({
|
|
646
|
+
id: item.id,
|
|
647
|
+
name: item.name,
|
|
648
|
+
quantity: item.quantity,
|
|
649
|
+
image: item.details?.productAssetUrl,
|
|
650
|
+
options: item.details?.selectedOptions,
|
|
651
|
+
subtotal: item.totals?.subTotal?.value,
|
|
652
|
+
currency: item.totals?.subTotal?.currencyCode
|
|
653
|
+
}));
|
|
529
654
|
const summary = getCartSummaryTotals(cart);
|
|
530
|
-
const currency = cart?.totals?.total?.currencyCode ?? config.currencyCode;
|
|
655
|
+
const currency = isSample ? "USD" : cart?.totals?.total?.currencyCode ?? config.currencyCode;
|
|
656
|
+
const subtotal = isSample ? sample.items.reduce((sum, item) => sum + item.price * item.quantity, 0) : summary.subtotal;
|
|
531
657
|
async function handleAction(action, operation) {
|
|
532
658
|
if (locked) return;
|
|
533
659
|
setDrawerAction(action);
|
|
@@ -541,6 +667,7 @@ function CartDrawer() {
|
|
|
541
667
|
open,
|
|
542
668
|
onOpenChange: setOpen,
|
|
543
669
|
children: /* @__PURE__ */ jsx(Dialog.Portal, { children: /* @__PURE__ */ jsxs(StorefrontSurface, { children: [/* @__PURE__ */ jsx(Dialog.Overlay, { className: "fixed inset-0 z-40 bg-neutral-950/40" }), /* @__PURE__ */ jsxs(Dialog.Content, {
|
|
670
|
+
"data-commerce-source": isSample ? "sample" : void 0,
|
|
544
671
|
className: "fixed inset-y-0 right-0 z-50 flex w-full max-w-lg flex-col bg-white text-neutral-900 shadow-xl",
|
|
545
672
|
onCloseAutoFocus: (event) => {
|
|
546
673
|
event.preventDefault();
|
|
@@ -551,10 +678,10 @@ function CartDrawer() {
|
|
|
551
678
|
className: "flex items-start justify-between border-b border-neutral-200 p-6",
|
|
552
679
|
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Dialog.Title, {
|
|
553
680
|
className: "text-2xl font-semibold",
|
|
554
|
-
children: "Your cart"
|
|
681
|
+
children: isSample ? "Sample cart" : "Your cart"
|
|
555
682
|
}), /* @__PURE__ */ jsx(Dialog.Description, {
|
|
556
683
|
className: "mt-1 text-sm text-neutral-600",
|
|
557
|
-
children: "Review your items before checkout."
|
|
684
|
+
children: isSample ? "Example products and prices in USD. Nothing can be purchased." : "Review your items before checkout."
|
|
558
685
|
})] }), /* @__PURE__ */ jsx(Dialog.Close, {
|
|
559
686
|
className: "min-h-11 min-w-11 rounded-lg border border-neutral-400 bg-white px-3 text-neutral-900 focus-visible:outline focus-visible:outline-2",
|
|
560
687
|
"aria-label": "Close cart",
|
|
@@ -595,7 +722,7 @@ function CartDrawer() {
|
|
|
595
722
|
}),
|
|
596
723
|
/* @__PURE__ */ jsx("p", {
|
|
597
724
|
className: "mt-2 text-neutral-600",
|
|
598
|
-
children: "Find something you love in the shop."
|
|
725
|
+
children: isSample ? "Add a sample product to try the cart." : "Find something you love in the shop."
|
|
599
726
|
}),
|
|
600
727
|
/* @__PURE__ */ jsx(Link, {
|
|
601
728
|
className: `${buttonClass} mt-6`,
|
|
@@ -612,8 +739,8 @@ function CartDrawer() {
|
|
|
612
739
|
"data-testid": "cart-item",
|
|
613
740
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
614
741
|
className: "flex gap-4",
|
|
615
|
-
children: [item.
|
|
616
|
-
src: item.
|
|
742
|
+
children: [item.image && /* @__PURE__ */ jsx("img", {
|
|
743
|
+
src: item.image,
|
|
617
744
|
alt: "",
|
|
618
745
|
className: "h-20 w-20 rounded-lg bg-neutral-100 object-cover"
|
|
619
746
|
}), /* @__PURE__ */ jsxs("div", {
|
|
@@ -623,7 +750,7 @@ function CartDrawer() {
|
|
|
623
750
|
className: "font-semibold",
|
|
624
751
|
children: item.name
|
|
625
752
|
}),
|
|
626
|
-
item.
|
|
753
|
+
item.options?.map((option) => /* @__PURE__ */ jsxs("p", {
|
|
627
754
|
className: "text-sm text-neutral-600",
|
|
628
755
|
children: [
|
|
629
756
|
option.attribute,
|
|
@@ -633,7 +760,7 @@ function CartDrawer() {
|
|
|
633
760
|
}, `${option.attribute}:${option.values?.join(",")}`)),
|
|
634
761
|
/* @__PURE__ */ jsx("p", {
|
|
635
762
|
className: "mt-1 text-sm",
|
|
636
|
-
children: typeof item.
|
|
763
|
+
children: typeof item.subtotal === "number" ? money(item.subtotal, item.currency ?? currency) : "Price unavailable"
|
|
637
764
|
})
|
|
638
765
|
]
|
|
639
766
|
})]
|
|
@@ -690,16 +817,19 @@ function CartDrawer() {
|
|
|
690
817
|
children: [
|
|
691
818
|
/* @__PURE__ */ jsx("dl", {
|
|
692
819
|
className: "space-y-2 text-sm",
|
|
693
|
-
children: typeof cart?.totals?.subTotal?.value === "number" && /* @__PURE__ */ jsxs("div", {
|
|
820
|
+
children: (isSample || typeof cart?.totals?.subTotal?.value === "number") && /* @__PURE__ */ jsxs("div", {
|
|
694
821
|
className: "flex justify-between",
|
|
695
|
-
children: [/* @__PURE__ */ jsx("dt", { children: "Subtotal" }), /* @__PURE__ */ jsx("dd", { children: money(
|
|
822
|
+
children: [/* @__PURE__ */ jsx("dt", { children: isSample ? "Sample subtotal" : "Subtotal" }), /* @__PURE__ */ jsx("dd", { children: money(subtotal, isSample ? "USD" : cart?.totals?.subTotal?.currencyCode ?? currency) })]
|
|
696
823
|
})
|
|
697
824
|
}),
|
|
698
|
-
/* @__PURE__ */ jsx("p", {
|
|
825
|
+
!isSample && /* @__PURE__ */ jsx("p", {
|
|
699
826
|
className: "commerce-cart-checkout-adjustments-note mt-3 text-sm text-neutral-600",
|
|
700
827
|
children: "Shipping, taxes, and discounts are calculated at checkout."
|
|
701
828
|
}),
|
|
702
|
-
|
|
829
|
+
isSample ? /* @__PURE__ */ jsx("p", {
|
|
830
|
+
className: "text-sm text-neutral-600",
|
|
831
|
+
children: "Example total in USD. Checkout is not set up yet."
|
|
832
|
+
}) : config.checkoutSuccessPath ? /* @__PURE__ */ jsx("button", {
|
|
703
833
|
type: "button",
|
|
704
834
|
className: `${buttonClass} w-full`,
|
|
705
835
|
disabled: checkingOut || hydrating || cart?.totals?.total?.value == null,
|
|
@@ -800,7 +930,7 @@ function ProductCardContent({ product }) {
|
|
|
800
930
|
}
|
|
801
931
|
function Catalog(props) {
|
|
802
932
|
const { connection } = useCommerce();
|
|
803
|
-
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" ? /* @__PURE__ */ jsx(CatalogContent, { ...props }) : /* @__PURE__ */ jsx(CommerceStatus, {}) });
|
|
933
|
+
return /* @__PURE__ */ jsx(StorefrontSurface, { children: connection === "ready" ? /* @__PURE__ */ jsx(CatalogContent, { ...props }) : connection === "unbound" && props.sampleProducts ? /* @__PURE__ */ jsx(SampleCatalog, { ...props }) : /* @__PURE__ */ jsx(CommerceStatus, {}) });
|
|
804
934
|
}
|
|
805
935
|
function CatalogContent({ title = "Shop all products", description = "Explore the collection and find your favorites.", showHeader = true }) {
|
|
806
936
|
const { config } = useCommerce();
|
|
@@ -902,6 +1032,61 @@ function CatalogContent({ title = "Shop all products", description = "Explore th
|
|
|
902
1032
|
})
|
|
903
1033
|
] });
|
|
904
1034
|
}
|
|
1035
|
+
function SampleCatalog({ title = "Shop all products", description = "Explore the collection and find your favorites.", showHeader = true }) {
|
|
1036
|
+
const { add } = useSampleCart();
|
|
1037
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
1038
|
+
"data-commerce-source": "sample",
|
|
1039
|
+
children: [
|
|
1040
|
+
showHeader && /* @__PURE__ */ jsxs("header", {
|
|
1041
|
+
className: "border-b border-neutral-200 pb-10 pt-4 sm:pb-14 sm:pt-8",
|
|
1042
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
1043
|
+
className: "max-w-3xl text-4xl font-semibold leading-tight sm:text-5xl lg:text-6xl",
|
|
1044
|
+
children: title
|
|
1045
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1046
|
+
className: "mt-5 max-w-xl text-base leading-7 sm:text-lg",
|
|
1047
|
+
children: description
|
|
1048
|
+
})]
|
|
1049
|
+
}),
|
|
1050
|
+
/* @__PURE__ */ jsx("p", {
|
|
1051
|
+
className: "border-b border-neutral-200 py-6 text-sm",
|
|
1052
|
+
role: "status",
|
|
1053
|
+
children: "Sample products · Example prices in USD"
|
|
1054
|
+
}),
|
|
1055
|
+
/* @__PURE__ */ jsx("div", {
|
|
1056
|
+
className: "grid grid-cols-1 gap-6 py-8 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 lg:py-10",
|
|
1057
|
+
children: sampleProducts.map((product) => /* @__PURE__ */ jsxs("article", {
|
|
1058
|
+
className: "flex min-w-0 flex-col overflow-hidden rounded-lg border border-neutral-200",
|
|
1059
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1060
|
+
"aria-hidden": "true",
|
|
1061
|
+
className: "flex aspect-square items-center justify-center bg-neutral-100",
|
|
1062
|
+
children: /* @__PURE__ */ jsx("div", { className: "h-20 w-20 rounded-lg border border-neutral-300" })
|
|
1063
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1064
|
+
className: "flex flex-1 flex-col p-6",
|
|
1065
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
1066
|
+
className: "break-words text-2xl font-semibold leading-tight",
|
|
1067
|
+
children: product.name
|
|
1068
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1069
|
+
className: "mt-auto pt-6",
|
|
1070
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1071
|
+
className: "flex flex-wrap items-end justify-between gap-4 border-t border-neutral-200 pt-5",
|
|
1072
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1073
|
+
className: "text-lg font-semibold leading-7",
|
|
1074
|
+
children: money(product.price, "USD")
|
|
1075
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1076
|
+
type: "button",
|
|
1077
|
+
className: buttonClass,
|
|
1078
|
+
"aria-label": `Add ${product.name} to sample cart`,
|
|
1079
|
+
onClick: () => add(product.key),
|
|
1080
|
+
children: "Add to cart"
|
|
1081
|
+
})]
|
|
1082
|
+
})
|
|
1083
|
+
})]
|
|
1084
|
+
})]
|
|
1085
|
+
}, product.key))
|
|
1086
|
+
})
|
|
1087
|
+
]
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
905
1090
|
|
|
906
1091
|
//#endregion
|
|
907
1092
|
//#region src/commerce-storefront.tsx
|
package/dist/styles.css
CHANGED
|
@@ -65,7 +65,6 @@
|
|
|
65
65
|
}
|
|
66
66
|
.commerce-storefront {
|
|
67
67
|
color: var(--commerce-text, inherit);
|
|
68
|
-
background-color: var(--commerce-surface, transparent);
|
|
69
68
|
font-family: inherit;
|
|
70
69
|
line-height: 1.5;
|
|
71
70
|
}
|
|
@@ -108,7 +107,6 @@
|
|
|
108
107
|
}
|
|
109
108
|
.commerce-inline {
|
|
110
109
|
display: inline-block;
|
|
111
|
-
background-color: transparent;
|
|
112
110
|
}
|
|
113
111
|
.commerce-sr-only {
|
|
114
112
|
position: absolute;
|
package/docs/server-api.md
CHANGED
|
@@ -101,3 +101,40 @@ The client refreshes the cart before checkout and sends its ID, an absolute cata
|
|
|
101
101
|
When using `@godaddy/gd-commerce-server`, configure the router's `checkoutReturnUrls.returnUrls` with the absolute catalog return URL and `checkoutReturnUrls.successUrls` with the absolute success-page URL. The package matches these exact destinations and permits an additional `orderId` parameter on success URLs. Missing policy disables HTTP checkout. Public HTTP requests accept only cart or SKU checkout; non-catalog amounts belong in a trusted server handler.
|
|
102
102
|
|
|
103
103
|
Do not treat `checkoutSuccessPath`, a client-provided URL, the redirect itself, or a draft-order response as proof that payment succeeded. Verify payment using the checkout/payment service or trusted webhook state. Expire or reject paid/closed drafts on subsequent cart reads so returning customers cannot reuse a completed cart.
|
|
104
|
+
|
|
105
|
+
## Connection state
|
|
106
|
+
|
|
107
|
+
A host may return one of these uncached config responses:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{ "state": "unbound" }
|
|
111
|
+
{ "state": "connecting" }
|
|
112
|
+
{ "state": "ready", "cartScope": "opaque-cart-scope", "currencyCode": "USD" }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Only an explicit `unbound` response allows `Catalog sampleProducts`. `connecting` suspends all
|
|
116
|
+
catalog and cart actions. Loading, malformed state, HTTP errors, and missing routes never allow
|
|
117
|
+
samples. A successful `{ cartScope, currencyCode }` response without `state` still means ready.
|
|
118
|
+
|
|
119
|
+
`@godaddy/gd-commerce-server` accepts an optional synchronous
|
|
120
|
+
`CommerceConfiguration.readConnectionState(): 'unbound' | 'connecting' | 'ready' | undefined`.
|
|
121
|
+
Return `undefined` to keep the existing config behavior. Return `unbound` only when the host has
|
|
122
|
+
confirmed that no store is connected. Throw when the state is unknown or cannot be read.
|
|
123
|
+
`unbound` and `connecting` responses need no credentials. `ready` still requires `read()` to
|
|
124
|
+
succeed. Data and checkout handlers reject any state other than `ready`, even if credentials
|
|
125
|
+
remain. Do not infer `unbound` from missing credentials or configuration.
|
|
126
|
+
|
|
127
|
+
The host owns the connection lifecycle. Report `ready` only when the store connection is
|
|
128
|
+
complete, and keep failures in a state other than `unbound`. After a store connects, missing
|
|
129
|
+
configuration must never bring samples back. Report `unbound` again only after the store is
|
|
130
|
+
deliberately disconnected. State changes must be visible to the next request. Pages switch to
|
|
131
|
+
live data only when the deployed server reports a new state. If a host's deployments read
|
|
132
|
+
state from deployment configuration, a deployed site keeps showing samples until that
|
|
133
|
+
configuration is updated, for example by publishing again.
|
|
134
|
+
|
|
135
|
+
The storefront polls every five seconds while the page is visible and the state is `unbound` or
|
|
136
|
+
`connecting`, and refreshes on mount and focus. A failed refresh immediately hides samples, without
|
|
137
|
+
automatic request retries. While the last response was `connecting`, refresh failures keep the
|
|
138
|
+
connecting state for up to 60 seconds, so a host restart during connection does not flash an
|
|
139
|
+
error. Polling continues, and a longer failure reports the error. Aborted configuration requests cannot overwrite newer responses.
|
|
140
|
+
Release server and client support before using the `sampleProducts` prop. No new endpoint is required.
|