@pack/react 4.3.3 → 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 +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/pack/render-sections.d.ts.map +1 -1
- package/dist/pack/render-sections.js +36 -5
- package/dist/use-playbook-cart.d.ts +62 -0
- package/dist/use-playbook-cart.d.ts.map +1 -0
- package/dist/use-playbook-cart.js +87 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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, };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-sections.d.ts","sourceRoot":"","sources":["../../src/pack/render-sections.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"render-sections.d.ts","sourceRoot":"","sources":["../../src/pack/render-sections.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwD,MAAM,OAAO,CAAC;AAM7E,UAAU,kBAAkB;IAC1B,sDAAsD;IACtD,OAAO,EAAE,GAAG,CAAC;CACd;AAuCD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAAE,OAAO,EAAE,EAAE,kBAAkB,4BAG5D;AAGD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,GAAG,EAAE,uCAYnD;AAUD,UAAU,mBAAmB;IAC3B,OAAO,CAAC,EAAE,GAAG,CAAC;CACf;AAkED,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,4BAKxD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,mBAAmB,uCAOrD"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useMemo, useEffect } from "react";
|
|
1
|
+
import React, { useMemo, useEffect, useRef } from "react";
|
|
2
2
|
import { usePackContext } from "./pack-context";
|
|
3
3
|
import { sectionMap } from "../register-section";
|
|
4
4
|
import { generateSectionStylesheet } from "../lib/style-utils";
|
|
@@ -57,12 +57,43 @@ function Sections({ sections }) {
|
|
|
57
57
|
}
|
|
58
58
|
function useRenderSections({ content }) {
|
|
59
59
|
const { isPreview, liveContent, setLiveContent } = usePackContext();
|
|
60
|
-
//
|
|
60
|
+
// The loader `content` object we last pushed into liveContent. While
|
|
61
|
+
// liveContent is still that object, nothing has been received from the
|
|
62
|
+
// customizer for this page, so keep following loader data (navigation and
|
|
63
|
+
// ordinary preview links stay fresh). Once the customizer replaces it via
|
|
64
|
+
// SET_PAGE_DATA, a loader update for the *same page* must not win: any
|
|
65
|
+
// React Router action (e.g. a fetcher POST) revalidates every loader and
|
|
66
|
+
// hands us a fresh `content` object holding the persisted page, which used
|
|
67
|
+
// to silently overwrite unsaved customizer changes — a just-shown section
|
|
68
|
+
// rendered for a beat, then vanished.
|
|
69
|
+
//
|
|
70
|
+
// "Same page" is the handle (localized variants share a handle but not an
|
|
71
|
+
// id, see ff67a5a) plus the pathname the customizer payload arrived on.
|
|
72
|
+
// Handles are only unique per content type, so a client-side navigation
|
|
73
|
+
// from /products/hats to /collections/hats changes the pathname and hands
|
|
74
|
+
// ownership back to loader data instead of pinning the stale product page.
|
|
75
|
+
const syncedLoaderContentRef = useRef(null);
|
|
76
|
+
const customizerContentRef = useRef(null);
|
|
77
|
+
const customizerPathnameRef = useRef(null);
|
|
61
78
|
useEffect(() => {
|
|
62
|
-
if (isPreview
|
|
63
|
-
|
|
79
|
+
if (!isPreview || !setLiveContent)
|
|
80
|
+
return;
|
|
81
|
+
if (liveContent === content)
|
|
82
|
+
return;
|
|
83
|
+
const pathname = typeof window === "undefined" ? null : window.location.pathname;
|
|
84
|
+
const fromCustomizer = !!liveContent && liveContent !== syncedLoaderContentRef.current;
|
|
85
|
+
if (fromCustomizer && liveContent !== customizerContentRef.current) {
|
|
86
|
+
customizerContentRef.current = liveContent;
|
|
87
|
+
customizerPathnameRef.current = pathname;
|
|
64
88
|
}
|
|
65
|
-
|
|
89
|
+
const customizerOwnsPage = fromCustomizer &&
|
|
90
|
+
liveContent?.handle === content?.handle &&
|
|
91
|
+
customizerPathnameRef.current === pathname;
|
|
92
|
+
if (customizerOwnsPage)
|
|
93
|
+
return;
|
|
94
|
+
syncedLoaderContentRef.current = content;
|
|
95
|
+
setLiveContent(content);
|
|
96
|
+
}, [content, isPreview, liveContent, setLiveContent]);
|
|
66
97
|
// Smart content selection: prefer current content when it changes (navigation),
|
|
67
98
|
// but allow liveContent to take precedence when it's updated by customizer
|
|
68
99
|
const contentToRender = useMemo(() => {
|
|
@@ -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.
|
|
1
|
+
export const VERSION = "4.4.0";
|