@lime-bundles/react 7.3.0 → 7.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/README.md CHANGED
@@ -124,10 +124,14 @@ Full CSS variable reference: [css-variables.md](./docs/css-variables.md).
124
124
 
125
125
  `<FixedBundle>`, `<VolumeBundle>`, `<MixMatchBundle>`, and `useBundleData` accept four optional props for Shopify Markets and B2B catalogs:
126
126
 
127
- - **`country`** — ISO-3166 alpha-2 (e.g. `"US"`). Passed as Storefront `@inContext(country:)`. Drives Markets pricing and currency.
127
+ - **`country`** — ISO-3166 alpha-2 (e.g. `"US"`). Passed as Storefront `@inContext(country:)`. Drives Markets pricing and currency, and matches the bundle's projected country codes when it is restricted to specific markets.
128
128
  - **`language`** — ISO-639-1 (e.g. `"EN"`). Passed as `@inContext(language:)`.
129
- - **`buyer`** — B2B buyer identity. `BuyerInput` object or `() => Promise<BuyerInput>` callback.
130
- - **`marketId`** — Current visitor's Market GID. When the bundle is `marketVisibility: "specific"`, it's hidden unless this market is in its allow-list.
129
+ - **`buyer`** — B2B buyer identity. `BuyerInput` object or `() => Promise<BuyerInput>` callback. Its `companyLocationId` also matches the bundle's projected company locations when it is restricted to a B2B market.
130
+ - **`marketId`** — Current visitor's Market GID. Legacy market-gate signal; prefer `country` + `buyer` Shopify has deprecated market IDs for buyer targeting, and a B2B buyer's company-location market has no storefront market context.
131
+
132
+ A market-restricted bundle is shown when **any** signal matches: buyer country vs the bundle's projected `countryCodes`, buyer company location vs its projected `companyLocationIds`, or the legacy market-id vs `marketIds`. With no matching signal the fetcher returns `bundle: null` plus a `BundleVisibilityWarning` describing both sides of the mismatch.
133
+
134
+ **`currencyRate`** — store→presentment currency rate, accepted by the same components and by `fetchBundleData` / `useBundleData` / `useBundlesForProduct`. Merchant-configured amounts (fixed-amount discounts, flat prices, volume tier amounts) are stored in the shop's store currency, while `@inContext(country:)` prices arrive in the buyer's presentment currency; the checkout-side discount function converts configured amounts by `presentmentCurrencyRate`, so pass the same rate here (Hydrogen: the cart's `presentmentCurrencyRate`; Liquid-adjacent pages: `window.Shopify.currency.rate`) or the quoted sale price drifts from what checkout charges. Defaults to `1` (no conversion); invalid or non-positive values fall back to `1`. Percentages are unaffected.
131
135
 
132
136
  ```tsx
133
137
  <FixedBundle
package/dist/index.cjs CHANGED
@@ -93,6 +93,7 @@ async function fetchBundleData(options) {
93
93
  return result.bundle;
94
94
  }
95
95
  async function fetchBundleDataWithWarnings(options) {
96
+ const buyer = typeof options.buyer === "function" ? await options.buyer() : options.buyer;
96
97
  const config = {
97
98
  shopDomain: options.shopDomain,
98
99
  accessToken: options.storefrontAccessToken,
@@ -100,7 +101,7 @@ async function fetchBundleDataWithWarnings(options) {
100
101
  apiVersion: options.apiVersion,
101
102
  country: options.country,
102
103
  language: options.language,
103
- buyer: options.buyer
104
+ buyer
104
105
  };
105
106
  const client = (0, import_core.createStorefrontClient)(config);
106
107
  const query = (0, import_core.hasInContext)(config) ? (0, import_core.withInContext)(import_core.BUNDLE_METAOBJECT_QUERY) : import_core.BUNDLE_METAOBJECT_QUERY;
@@ -115,11 +116,16 @@ async function fetchBundleDataWithWarnings(options) {
115
116
  "not_found"
116
117
  );
117
118
  }
118
- const bundle = (0, import_core.parseMetaobjectBundleStrict)(
119
- data.metaobject.id,
120
- data.metaobject.fields
119
+ const bundle = (0, import_core.convertBundleToPresentment)(
120
+ (0, import_core.parseMetaobjectBundleStrict)(data.metaobject.id, data.metaobject.fields),
121
+ options.currencyRate
121
122
  );
122
- if (!(0, import_core.isVisibleInMarket)(bundle, options.marketId)) {
123
+ const buyerContext = {
124
+ marketId: options.marketId,
125
+ countryCode: options.country,
126
+ companyLocationId: buyer?.companyLocationId
127
+ };
128
+ if (!(0, import_core.isVisibleToBuyer)(bundle, buyerContext)) {
123
129
  return {
124
130
  bundle: null,
125
131
  warnings: [
@@ -127,7 +133,11 @@ async function fetchBundleDataWithWarnings(options) {
127
133
  bundleGid: options.bundleGid,
128
134
  reason: "market_mismatch",
129
135
  currentMarketId: options.marketId ?? null,
130
- allowedMarketIds: bundle.marketIds
136
+ currentCountryCode: options.country ?? null,
137
+ currentCompanyLocationId: buyer?.companyLocationId ?? null,
138
+ allowedMarketIds: bundle.marketIds,
139
+ allowedCountryCodes: bundle.countryCodes,
140
+ allowedCompanyLocationIds: bundle.companyLocationIds
131
141
  }
132
142
  ]
133
143
  };
@@ -194,7 +204,8 @@ function useBundleData(options) {
194
204
  country: options.country,
195
205
  language: options.language,
196
206
  buyer: options.buyer,
197
- marketId: options.marketId
207
+ marketId: options.marketId,
208
+ currencyRate: options.currencyRate
198
209
  }).then((bundle) => {
199
210
  if (controller.signal.aborted) return;
200
211
  setState({ status: "success", bundle, error: null });
@@ -225,7 +236,8 @@ function useBundleData(options) {
225
236
  options.country,
226
237
  options.language,
227
238
  options.buyer,
228
- options.marketId
239
+ options.marketId,
240
+ options.currencyRate
229
241
  ]);
230
242
  return state;
231
243
  }
@@ -749,12 +761,22 @@ function FixedBundle(props) {
749
761
  analyticsEnabled,
750
762
  onAddToCart,
751
763
  onError,
752
- className
764
+ className,
765
+ country,
766
+ language,
767
+ buyer,
768
+ marketId,
769
+ currencyRate
753
770
  } = props;
754
771
  const result = useBundleData({
755
772
  shopDomain,
756
773
  storefrontAccessToken,
757
- bundleGid
774
+ bundleGid,
775
+ country,
776
+ language,
777
+ buyer,
778
+ marketId,
779
+ currencyRate
758
780
  });
759
781
  const { elementRef, trackAddToCart } = useAnalytics({
760
782
  shopDomain,
@@ -1810,12 +1832,22 @@ function MixMatchBundle(props) {
1810
1832
  onAddToCart,
1811
1833
  onError,
1812
1834
  className,
1813
- productHandle
1835
+ productHandle,
1836
+ country,
1837
+ language,
1838
+ buyer,
1839
+ marketId,
1840
+ currencyRate
1814
1841
  } = props;
1815
1842
  const result = useBundleData({
1816
1843
  shopDomain,
1817
1844
  storefrontAccessToken,
1818
- bundleGid
1845
+ bundleGid,
1846
+ country,
1847
+ language,
1848
+ buyer,
1849
+ marketId,
1850
+ currencyRate
1819
1851
  });
1820
1852
  const { elementRef, trackAddToCart } = useAnalytics({
1821
1853
  shopDomain,
@@ -2341,12 +2373,22 @@ function VolumeBundle(props) {
2341
2373
  onAddToCart,
2342
2374
  onError,
2343
2375
  className,
2344
- productId
2376
+ productId,
2377
+ country,
2378
+ language,
2379
+ buyer,
2380
+ marketId,
2381
+ currencyRate
2345
2382
  } = props;
2346
2383
  const result = useBundleData({
2347
2384
  shopDomain,
2348
2385
  storefrontAccessToken,
2349
- bundleGid
2386
+ bundleGid,
2387
+ country,
2388
+ language,
2389
+ buyer,
2390
+ marketId,
2391
+ currencyRate
2350
2392
  });
2351
2393
  const { elementRef, trackAddToCart } = useAnalytics({
2352
2394
  shopDomain,
@@ -2722,12 +2764,22 @@ function BogoBundle(props) {
2722
2764
  analyticsEnabled,
2723
2765
  onAddToCart,
2724
2766
  onError,
2725
- className
2767
+ className,
2768
+ country,
2769
+ language,
2770
+ buyer,
2771
+ marketId,
2772
+ currencyRate
2726
2773
  } = props;
2727
2774
  const result = useBundleData({
2728
2775
  shopDomain,
2729
2776
  storefrontAccessToken,
2730
- bundleGid
2777
+ bundleGid,
2778
+ country,
2779
+ language,
2780
+ buyer,
2781
+ marketId,
2782
+ currencyRate
2731
2783
  });
2732
2784
  const { elementRef, trackAddToCart } = useAnalytics({
2733
2785
  shopDomain,
@@ -3066,6 +3118,8 @@ function normalizeText2(str) {
3066
3118
  return str.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
3067
3119
  }
3068
3120
  var FILTERS_ALL2 = "__all__";
3121
+ var STEP_CASCADE_MS = 500;
3122
+ var AUTO_ADVANCE_HOLD_MS = 160;
3069
3123
  function MultiStepPicker({
3070
3124
  bundle,
3071
3125
  currency,
@@ -3089,6 +3143,8 @@ function MultiStepPicker({
3089
3143
  const closeBtnRef = (0, import_react15.useRef)(null);
3090
3144
  const listRef = (0, import_react15.useRef)(null);
3091
3145
  const mouseDownTarget = (0, import_react15.useRef)(null);
3146
+ const cascadeTimer = (0, import_react15.useRef)(null);
3147
+ const autoAdvanceTimer = (0, import_react15.useRef)(null);
3092
3148
  const [open, setOpen] = (0, import_react15.useState)(false);
3093
3149
  const [stepIndex, setStepIndex] = (0, import_react15.useState)(
3094
3150
  () => Math.max(0, Math.min(steps.length - 1, initialStep))
@@ -3124,6 +3180,14 @@ function MultiStepPicker({
3124
3180
  const id = requestAnimationFrame(() => setOpen(true));
3125
3181
  return () => cancelAnimationFrame(id);
3126
3182
  }, []);
3183
+ (0, import_react15.useEffect)(() => {
3184
+ return () => {
3185
+ if (cascadeTimer.current !== null) clearTimeout(cascadeTimer.current);
3186
+ if (autoAdvanceTimer.current !== null) {
3187
+ clearTimeout(autoAdvanceTimer.current);
3188
+ }
3189
+ };
3190
+ }, []);
3127
3191
  useScrollLock(true);
3128
3192
  useFocusTrap({
3129
3193
  active: true,
@@ -3152,12 +3216,36 @@ function MultiStepPicker({
3152
3216
  lastSeen.current = { step: stepIndex, units: unitsInStep };
3153
3217
  if (!autoAdvance || isLastStep || prev.step !== stepIndex) return;
3154
3218
  if (unitsInStep <= prev.units) return;
3155
- if ((0, import_core13.stepHeadroom)(step?.maxQuantity ?? null, unitsInStep) === 0) {
3156
- goToStep(stepIndex + 1);
3219
+ if ((0, import_core13.stepHeadroom)(step?.maxQuantity ?? null, unitsInStep) !== 0) return;
3220
+ const from = stepIndex;
3221
+ if (autoAdvanceTimer.current !== null) {
3222
+ clearTimeout(autoAdvanceTimer.current);
3157
3223
  }
3224
+ autoAdvanceTimer.current = setTimeout(() => {
3225
+ autoAdvanceTimer.current = null;
3226
+ goToStep(from + 1);
3227
+ }, AUTO_ADVANCE_HOLD_MS);
3158
3228
  });
3159
3229
  function goToStep(i) {
3160
3230
  const clamped = Math.max(0, Math.min(steps.length - 1, i));
3231
+ if (autoAdvanceTimer.current !== null) {
3232
+ clearTimeout(autoAdvanceTimer.current);
3233
+ autoAdvanceTimer.current = null;
3234
+ }
3235
+ const dialog = dialogRef.current;
3236
+ if (dialog && clamped !== stepIndex) {
3237
+ if (cascadeTimer.current !== null) clearTimeout(cascadeTimer.current);
3238
+ dialog.removeAttribute("data-step-transition");
3239
+ void dialog.offsetHeight;
3240
+ dialog.setAttribute(
3241
+ "data-step-transition",
3242
+ clamped > stepIndex ? "forward" : "back"
3243
+ );
3244
+ cascadeTimer.current = setTimeout(() => {
3245
+ cascadeTimer.current = null;
3246
+ dialog.removeAttribute("data-step-transition");
3247
+ }, STEP_CASCADE_MS);
3248
+ }
3161
3249
  setStepIndex(clamped);
3162
3250
  setQuery("");
3163
3251
  setActiveType(FILTERS_ALL2);
@@ -3742,12 +3830,22 @@ function MultiStepBundle(props) {
3742
3830
  analyticsEnabled,
3743
3831
  onAddToCart,
3744
3832
  onError,
3745
- className
3833
+ className,
3834
+ country,
3835
+ language,
3836
+ buyer,
3837
+ marketId,
3838
+ currencyRate
3746
3839
  } = props;
3747
3840
  const result = useBundleData({
3748
3841
  shopDomain,
3749
3842
  storefrontAccessToken,
3750
- bundleGid
3843
+ bundleGid,
3844
+ country,
3845
+ language,
3846
+ buyer,
3847
+ marketId,
3848
+ currencyRate
3751
3849
  });
3752
3850
  const { elementRef, trackAddToCart } = useAnalytics({
3753
3851
  shopDomain,
@@ -4353,7 +4451,8 @@ function useBundlesForProduct(options) {
4353
4451
  storefrontAccessToken: options.storefrontAccessToken,
4354
4452
  productHandle: options.productHandle,
4355
4453
  buyerIp: options.buyerIp,
4356
- signal: controller.signal
4454
+ signal: controller.signal,
4455
+ currencyRate: options.currencyRate
4357
4456
  }).then((bundles) => {
4358
4457
  if (controller.signal.aborted) return;
4359
4458
  setState({ status: "success", bundles, error: null });
@@ -4381,7 +4480,8 @@ function useBundlesForProduct(options) {
4381
4480
  options.shopDomain,
4382
4481
  options.storefrontAccessToken,
4383
4482
  options.productHandle,
4384
- options.buyerIp
4483
+ options.buyerIp,
4484
+ options.currencyRate
4385
4485
  ]);
4386
4486
  return state;
4387
4487
  }