@lime-bundles/react 3.0.0 → 4.1.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 CHANGED
@@ -113,6 +113,49 @@ useEffect(() => {
113
113
 
114
114
  Full CSS variable reference: [css-variables.md](./docs/css-variables.md).
115
115
 
116
+ ## Markets & B2B
117
+
118
+ `<FixedBundle>`, `<VolumeBundle>`, `<MixMatchBundle>`, and `useBundleData` accept four optional props for Shopify Markets and B2B catalogs:
119
+
120
+ - **`country`** — ISO-3166 alpha-2 (e.g. `"US"`). Passed as Storefront `@inContext(country:)`. Drives Markets pricing and currency.
121
+ - **`language`** — ISO-639-1 (e.g. `"EN"`). Passed as `@inContext(language:)`.
122
+ - **`buyer`** — B2B buyer identity. `BuyerInput` object or `() => Promise<BuyerInput>` callback.
123
+ - **`marketId`** — Current visitor's Market GID. When the bundle is `marketVisibility: "specific"`, it's hidden unless this market is in its allow-list.
124
+
125
+ ```tsx
126
+ <FixedBundle
127
+ shopDomain="acme.myshopify.com"
128
+ storefrontAccessToken={token}
129
+ bundleGid={gid}
130
+ onAddToCart={addToCart}
131
+ country="US"
132
+ language="EN"
133
+ buyer={async () => fetchBuyerToken()} // resolved per request
134
+ marketId="gid://shopify/Market/1"
135
+ />
136
+ ```
137
+
138
+ **Security — B2B `customerAccessToken`:**
139
+
140
+ - Prefer the callback form (`() => Promise<BuyerInput>`) so the token doesn't sit in static prop trees, React DevTools snapshots, or Sentry/LogRocket traces.
141
+ - Never pass via URL query parameters (leaks to Referer headers and server logs).
142
+ - Never store in localStorage shared with third-party scripts.
143
+ - The Customer Account API PKCE flow is the canonical source. See https://shopify.dev/docs/api/customer/latest.
144
+
145
+ **Caching:**
146
+
147
+ Buyer-contextual responses MUST be `Cache-Control: private` — never share across users. `@lime-bundles/core` exports `getCacheKey()` for SWR / TanStack Query consumers; it hashes the buyer token (never includes the raw value) so per-buyer caches don't leak tokens.
148
+
149
+ ```tsx
150
+ import { getCacheKey } from "@lime-bundles/core";
151
+ useQuery({
152
+ queryKey: getCacheKey("Bundle", { id }, { shopDomain, country, buyer }),
153
+ queryFn: () => fetchBundleData({ ... }),
154
+ });
155
+ ```
156
+
157
+ **Filtering vs throwing:** `fetchBundleData` throws `BundleParseError` when the bundle is hidden by the market gate. Use `fetchBundleDataWithWarnings` instead to receive `{ bundle, warnings }` and decide whether to render alternative content.
158
+
116
159
  ## Version policy
117
160
 
118
161
  `BundleComponentProps`, `UseBundleDataResult`, and `UseBundlesForProductResult` are locked at v2.0.0. All three packages (`core`, `react`, `widget`) bump majors together.
package/dist/index.cjs CHANGED
@@ -80,14 +80,29 @@ var import_core3 = require("@lime-bundles/core");
80
80
  // src/fetchBundleData.ts
81
81
  var import_core = require("@lime-bundles/core");
82
82
  async function fetchBundleData(options) {
83
- const client = (0, import_core.createStorefrontClient)({
83
+ const result = await fetchBundleDataWithWarnings(options);
84
+ if (!result.bundle) {
85
+ throw new import_core.BundleParseError(
86
+ `Bundle hidden by market gate: ${options.bundleGid}`,
87
+ "not_found"
88
+ );
89
+ }
90
+ return result.bundle;
91
+ }
92
+ async function fetchBundleDataWithWarnings(options) {
93
+ const config = {
84
94
  shopDomain: options.shopDomain,
85
95
  accessToken: options.storefrontAccessToken,
86
96
  buyerIp: options.buyerIp,
87
- apiVersion: options.apiVersion
88
- });
97
+ apiVersion: options.apiVersion,
98
+ country: options.country,
99
+ language: options.language,
100
+ buyer: options.buyer
101
+ };
102
+ const client = (0, import_core.createStorefrontClient)(config);
103
+ const query = (0, import_core.hasInContext)(config) ? (0, import_core.withInContext)(import_core.BUNDLE_METAOBJECT_QUERY) : import_core.BUNDLE_METAOBJECT_QUERY;
89
104
  const data = await client.query(
90
- import_core.BUNDLE_METAOBJECT_QUERY,
105
+ query,
91
106
  { id: options.bundleGid },
92
107
  { signal: options.signal }
93
108
  );
@@ -97,10 +112,24 @@ async function fetchBundleData(options) {
97
112
  "not_found"
98
113
  );
99
114
  }
100
- return (0, import_core.parseMetaobjectBundleStrict)(
115
+ const bundle = (0, import_core.parseMetaobjectBundleStrict)(
101
116
  data.metaobject.id,
102
117
  data.metaobject.fields
103
118
  );
119
+ if (bundle.marketVisibility === "specific" && (!options.marketId || !bundle.marketIds.includes(options.marketId))) {
120
+ return {
121
+ bundle: null,
122
+ warnings: [
123
+ {
124
+ bundleGid: options.bundleGid,
125
+ reason: "market_mismatch",
126
+ currentMarketId: options.marketId ?? null,
127
+ allowedMarketIds: bundle.marketIds
128
+ }
129
+ ]
130
+ };
131
+ }
132
+ return { bundle, warnings: [] };
104
133
  }
105
134
 
106
135
  // src/fetchShopCustomCss.ts
@@ -146,7 +175,11 @@ function useBundleData(options) {
146
175
  shopDomain: options.shopDomain,
147
176
  storefrontAccessToken: options.storefrontAccessToken,
148
177
  bundleGid: options.bundleGid,
149
- signal: controller.signal
178
+ signal: controller.signal,
179
+ country: options.country,
180
+ language: options.language,
181
+ buyer: options.buyer,
182
+ marketId: options.marketId
150
183
  }).then((bundle) => {
151
184
  if (controller.signal.aborted) return;
152
185
  setState({ status: "success", bundle, error: null });
@@ -173,7 +206,11 @@ function useBundleData(options) {
173
206
  }, [
174
207
  options.shopDomain,
175
208
  options.storefrontAccessToken,
176
- options.bundleGid
209
+ options.bundleGid,
210
+ options.country,
211
+ options.language,
212
+ options.buyer,
213
+ options.marketId
177
214
  ]);
178
215
  return state;
179
216
  }