@pack/react 4.3.4 → 4.4.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/dist/index.d.ts CHANGED
@@ -4,7 +4,9 @@ import { usePackContext } from "./pack/pack-context";
4
4
  import { useSiteSettings } from "./use-site-settings";
5
5
  import { useLocale } from "./use-locale";
6
6
  import { useOverlayScript } from "./use-overlay-script";
7
+ import { usePlaybookCart } from "./use-playbook-cart";
7
8
  import { registerSection } from "./register-section";
8
9
  import { registerStorefrontSettingsSchema } from "./register-storefront-settings-schema";
9
- export { PackProvider, PreviewProvider, RenderSection, RenderSections, registerSection, registerStorefrontSettingsSchema, useLocale, useOverlayScript, usePackContext, useSections, useSiteSettings, };
10
+ export { PackProvider, PreviewProvider, RenderSection, RenderSections, registerSection, registerStorefrontSettingsSchema, useLocale, useOverlayScript, usePackContext, usePlaybookCart, useSections, useSiteSettings, };
11
+ export type { PlaybookCartAdd, PlaybookCartInput } from "./use-playbook-cart";
10
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,cAAc,EACd,WAAW,EACZ,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,gCAAgC,EAAE,MAAM,uCAAuC,CAAC;AAEzF,OAAO,EACL,YAAY,EACZ,eAAe,EACf,aAAa,EACb,cAAc,EACd,eAAe,EACf,gCAAgC,EAChC,SAAS,EACT,gBAAgB,EAChB,cAAc,EACd,WAAW,EACX,eAAe,GAChB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,cAAc,EACd,WAAW,EACZ,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,gCAAgC,EAAE,MAAM,uCAAuC,CAAC;AAEzF,OAAO,EACL,YAAY,EACZ,eAAe,EACf,aAAa,EACb,cAAc,EACd,eAAe,EACf,gCAAgC,EAChC,SAAS,EACT,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,WAAW,EACX,eAAe,GAChB,CAAC;AAEF,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { usePackContext } from "./pack/pack-context";
4
4
  import { useSiteSettings } from "./use-site-settings";
5
5
  import { useLocale } from "./use-locale";
6
6
  import { useOverlayScript } from "./use-overlay-script";
7
+ import { usePlaybookCart } from "./use-playbook-cart";
7
8
  import { registerSection } from "./register-section";
8
9
  import { registerStorefrontSettingsSchema } from "./register-storefront-settings-schema";
9
- export { PackProvider, PreviewProvider, RenderSection, RenderSections, registerSection, registerStorefrontSettingsSchema, useLocale, useOverlayScript, usePackContext, useSections, useSiteSettings, };
10
+ export { PackProvider, PreviewProvider, RenderSection, RenderSections, registerSection, registerStorefrontSettingsSchema, useLocale, useOverlayScript, usePackContext, usePlaybookCart, useSections, useSiteSettings, };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Bridges a storefront's own cart to Playbook's collection-grid "Add to cart".
3
+ *
4
+ * WHY A BRIDGE AND NOT A DIRECT WRITE. Playbook's storefront SDK adds a line on
5
+ * a Liquid theme through Shopify's AJAX Cart API (`/cart/add.js`). Hydrogen has
6
+ * no such endpoint, and the one thing Playbook must not do is write the line
7
+ * behind the storefront's back: it could add it through the Storefront API, but
8
+ * a Hydrogen cart's UI is rendered from React state that no third-party script
9
+ * can reach, so the line would land in Shopify while the cart icon still read
10
+ * zero and the drawer never opened. The shopper would click again. A correct
11
+ * backend behind a frontend that looks broken is worse than an honest link to
12
+ * the product page, which is what Playbook falls back to when no bridge exists.
13
+ *
14
+ * So the storefront — the only code that CAN update its own cart UI — supplies
15
+ * the write, and Playbook calls it. Registering this also wins on Liquid, where
16
+ * Playbook prefers a registered bridge over its own event fan-out, because the
17
+ * store's real drawer always beats a simulated one.
18
+ *
19
+ * OPT-IN, ALWAYS. Nothing here runs unless a storefront calls this hook. It is
20
+ * a `window` global, and a package installed by 50+ storefronts must never
21
+ * assign one on their behalf.
22
+ *
23
+ * const { linesAdd } = useCart();
24
+ * usePlaybookCart(async ({ variantId, quantity }) => {
25
+ * await linesAdd([{ merchandiseId: variantId, quantity }]);
26
+ * });
27
+ *
28
+ * THE OTHER HALF OF THIS CONTRACT LIVES IN PLAYBOOK, not here, and nothing in
29
+ * this repo can verify it: `sdk/experience/src/cart-add.ts` in
30
+ * `packdigital/playbook` defines the global's name, decides whether the button
31
+ * renders at all (`canAddToCart`), and treats a rejection as the shopper-facing
32
+ * failure state. Read that file before changing the shape below.
33
+ */
34
+ /**
35
+ * What Playbook passes to the bridge.
36
+ *
37
+ * Deliberately OPEN: Playbook owns this payload, and the bridge forwards every
38
+ * field it receives so a later addition there (a selling plan, line attributes)
39
+ * reaches the storefront without a release here.
40
+ */
41
+ export interface PlaybookCartInput {
42
+ /** Numeric variant id as Playbook sends it; normalized to a GID before the
43
+ * storefront's callback sees it. */
44
+ variantId: string;
45
+ quantity: number;
46
+ [key: string]: unknown;
47
+ }
48
+ /** The storefront's own add-to-cart. Resolve = added; throw = failed. */
49
+ export type PlaybookCartAdd = (input: PlaybookCartInput) => Promise<unknown> | unknown;
50
+ /**
51
+ * Accept either form and always hand the callback a variant GID. Playbook sends
52
+ * the legacy numeric id (its Liquid path needs that for `/cart/add.js`), which
53
+ * every Hydrogen cart API rejects.
54
+ *
55
+ * This is a globally-callable entry point, so it validates rather than coercing:
56
+ * an empty id would otherwise build the bare prefix `gid://shopify/…/`, and a
57
+ * PRODUCT gid would sail through as if it were a variant. Both fail far away
58
+ * from here, inside the cart mutation, with a message that doesn't name Playbook.
59
+ */
60
+ export declare const toVariantGid: (variantId: string) => string;
61
+ export declare const usePlaybookCart: (addToCart: PlaybookCartAdd | null | undefined) => void;
62
+ //# sourceMappingURL=use-playbook-cart.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-playbook-cart.d.ts","sourceRoot":"","sources":["../src/use-playbook-cart.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC;yCACqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,yEAAyE;AACzE,MAAM,MAAM,eAAe,GAAG,CAC5B,KAAK,EAAE,iBAAiB,KACrB,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;AAmBhC;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,cAAe,MAAM,KAAG,MAahD,CAAC;AAYF,eAAO,MAAM,eAAe,cACf,eAAe,GAAG,IAAI,GAAG,SAAS,KAC5C,IAqDF,CAAC"}
@@ -0,0 +1,87 @@
1
+ import { useEffect, useRef } from "react";
2
+ /** Shopify variant GID prefix — the form every Hydrogen cart API expects. */
3
+ const VARIANT_GID_PREFIX = "gid://shopify/ProductVariant/";
4
+ /**
5
+ * Accept either form and always hand the callback a variant GID. Playbook sends
6
+ * the legacy numeric id (its Liquid path needs that for `/cart/add.js`), which
7
+ * every Hydrogen cart API rejects.
8
+ *
9
+ * This is a globally-callable entry point, so it validates rather than coercing:
10
+ * an empty id would otherwise build the bare prefix `gid://shopify/…/`, and a
11
+ * PRODUCT gid would sail through as if it were a variant. Both fail far away
12
+ * from here, inside the cart mutation, with a message that doesn't name Playbook.
13
+ */
14
+ export const toVariantGid = (variantId) => {
15
+ const id = String(variantId ?? "").trim();
16
+ if (!id)
17
+ throw new Error("[playbook] add-to-cart called with no variantId");
18
+ if (id.startsWith("gid://")) {
19
+ if (!id.startsWith(VARIANT_GID_PREFIX)) {
20
+ throw new Error(`[playbook] expected a ProductVariant gid, got "${id}"`);
21
+ }
22
+ return id;
23
+ }
24
+ if (!/^\d+$/.test(id)) {
25
+ throw new Error(`[playbook] variantId must be numeric or a gid, got "${id}"`);
26
+ }
27
+ return `${VARIANT_GID_PREFIX}${id}`;
28
+ };
29
+ /** Quantity arrives from a third-party script; a cart mutation should never see
30
+ * a fraction, a negative, or `NaN`. */
31
+ const toQuantity = (quantity) => {
32
+ const n = Number(quantity);
33
+ if (!Number.isInteger(n) || n < 1) {
34
+ throw new Error(`[playbook] quantity must be a positive integer, got "${quantity}"`);
35
+ }
36
+ return n;
37
+ };
38
+ export const usePlaybookCart = (addToCart) => {
39
+ // The documented usage passes an INLINE arrow, so `addToCart` is a new
40
+ // identity every render. Keeping it in a ref means the global is registered
41
+ // once and the callback stays current, instead of the handler being torn down
42
+ // and reassigned on every render of the component that mounts the bridge.
43
+ const latest = useRef(addToCart);
44
+ useEffect(() => {
45
+ latest.current = addToCart;
46
+ }, [addToCart]);
47
+ const enabled = Boolean(addToCart);
48
+ useEffect(() => {
49
+ if (!enabled)
50
+ return;
51
+ const host = window;
52
+ if (host.playbookAddToCart) {
53
+ // Two bridges racing means one silently wins and the other's cart is
54
+ // never used — worth a word in dev, where a storefront author can act on
55
+ // it. Not an throw: the page still works, and the later mount wins
56
+ // consistently.
57
+ console.warn("[playbook] window.playbookAddToCart was already registered; the most recent usePlaybookCart wins.");
58
+ }
59
+ // `async` so a SYNCHRONOUS throw from the storefront's callback (or from
60
+ // validation above) reaches Playbook as a REJECTION. Playbook awaits this
61
+ // and renders the shopper a failure state; a sync throw would escape that
62
+ // path entirely and the button would sit there having done nothing.
63
+ const handler = async (input) => {
64
+ const add = latest.current;
65
+ if (!add)
66
+ return;
67
+ // Spread FIRST so any field Playbook adds later rides along untouched;
68
+ // only the two we normalize are overridden.
69
+ return add({
70
+ ...input,
71
+ variantId: toVariantGid(input.variantId),
72
+ quantity: toQuantity(input.quantity),
73
+ });
74
+ };
75
+ host.playbookAddToCart = handler;
76
+ return () => {
77
+ // Clear ONLY our own registration, by identity. React can mount the next
78
+ // instance before unmounting the previous one (a route transition, or
79
+ // StrictMode's double-invoke in dev), so an unconditional delete would
80
+ // remove the NEWER handler and leave the storefront with no bridge at
81
+ // all — the button silently degrading to navigation, on a store that had
82
+ // opted in.
83
+ if (host.playbookAddToCart === handler)
84
+ delete host.playbookAddToCart;
85
+ };
86
+ }, [enabled]);
87
+ };
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "4.3.4";
1
+ export const VERSION = "4.4.0";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pack/react",
3
3
  "description": "React",
4
- "version": "4.3.4",
4
+ "version": "4.4.0",
5
5
  "exports": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "engines": {