@drip-apex/sdk 0.1.0 → 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.
Files changed (245) hide show
  1. package/README.md +5 -0
  2. package/dist/bundle-report.json +584 -226
  3. package/dist/define.cjs +123 -0
  4. package/dist/define.cjs.map +7 -0
  5. package/dist/define.mjs +102 -0
  6. package/dist/define.mjs.map +7 -0
  7. package/dist/drip.cjs +19262 -0
  8. package/dist/drip.cjs.map +7 -0
  9. package/dist/drip.debug.js +1 -1
  10. package/dist/drip.debug.js.map +1 -1
  11. package/dist/drip.heatmap.js +1 -1
  12. package/dist/drip.heatmap.js.map +1 -1
  13. package/dist/drip.js +1 -1
  14. package/dist/drip.js.map +1 -1
  15. package/dist/drip.live.js +1 -1
  16. package/dist/drip.live.js.map +1 -1
  17. package/dist/drip.mjs +19246 -0
  18. package/dist/drip.mjs.map +7 -0
  19. package/dist/drip.replay-recorder.js +2 -0
  20. package/dist/drip.replay-recorder.js.map +1 -0
  21. package/dist/editor.js +147 -142
  22. package/dist/editor.js.map +4 -4
  23. package/dist/flags.cjs +1788 -0
  24. package/dist/flags.cjs.map +7 -0
  25. package/dist/flags.mjs +1765 -0
  26. package/dist/flags.mjs.map +7 -0
  27. package/dist/index.mjs +12 -0
  28. package/dist/index.mjs.map +2 -2
  29. package/dist/metafiles/define.cjs.meta.json +28 -0
  30. package/dist/metafiles/define.mjs.meta.json +32 -0
  31. package/dist/metafiles/drip.cjs.meta.json +3193 -0
  32. package/dist/metafiles/drip.debug.meta.json +2859 -574
  33. package/dist/metafiles/drip.heatmap.meta.json +2732 -460
  34. package/dist/metafiles/drip.live.meta.json +2732 -460
  35. package/dist/metafiles/drip.meta.json +2859 -574
  36. package/dist/metafiles/drip.mjs.meta.json +3235 -0
  37. package/dist/metafiles/drip.replay-recorder.meta.json +82 -0
  38. package/dist/metafiles/editor.meta.json +67 -27
  39. package/dist/metafiles/flags.cjs.meta.json +220 -0
  40. package/dist/metafiles/flags.mjs.meta.json +225 -0
  41. package/dist/metafiles/index.meta.json +2717 -491
  42. package/dist/metafiles/index.mjs.meta.json +14 -4
  43. package/dist/react/index.js +16621 -13539
  44. package/dist/react/index.js.map +4 -4
  45. package/dist/sri.json +20 -15
  46. package/dist/types/activation-runtime.d.ts +91 -0
  47. package/dist/types/analytics-runtime.d.ts +166 -0
  48. package/dist/types/antiflicker.d.ts +9 -3
  49. package/dist/types/assignment-cookie-sync.d.ts +31 -0
  50. package/dist/types/assignment-runtime.d.ts +257 -0
  51. package/dist/types/assignment-store.d.ts +4 -8
  52. package/dist/types/bucket.d.ts +1 -2
  53. package/dist/types/compact-store.d.ts +1 -0
  54. package/dist/types/config-runtime.d.ts +313 -0
  55. package/dist/types/consent-evaluation.d.ts +27 -0
  56. package/dist/types/css.d.ts +5 -3
  57. package/dist/types/debug-loader.d.ts +2 -0
  58. package/dist/types/decide-assignments.d.ts +31 -0
  59. package/dist/types/delivery-timing.d.ts +8 -0
  60. package/dist/types/early-interaction-capture.d.ts +79 -0
  61. package/dist/types/early-reassert.d.ts +32 -0
  62. package/dist/types/edge-runtime.d.ts +11 -0
  63. package/dist/types/editor/api.d.ts +3 -0
  64. package/dist/types/editor/controller.d.ts +3 -1
  65. package/dist/types/editor/publication-result.d.ts +11 -0
  66. package/dist/types/editor/types.d.ts +2 -1
  67. package/dist/types/editor/view-kit.d.ts +1 -1
  68. package/dist/types/endpoint.d.ts +8 -0
  69. package/dist/types/exclusion-groups.d.ts +4 -7
  70. package/dist/types/flags.d.ts +2 -0
  71. package/dist/types/goal-eligibility.d.ts +4 -0
  72. package/dist/types/goals.d.ts +10 -1
  73. package/dist/types/guards.d.ts +7 -7
  74. package/dist/types/handover.d.ts +12 -1
  75. package/dist/types/hash.d.ts +8 -4
  76. package/dist/types/hydration.d.ts +2 -1
  77. package/dist/types/index.d.ts +24 -128
  78. package/dist/types/init-test-helpers.d.ts +48 -0
  79. package/dist/types/inline-bootstrap.d.ts +26 -0
  80. package/dist/types/js.d.ts +3 -1
  81. package/dist/types/money.d.ts +12 -0
  82. package/dist/types/mutate.d.ts +45 -6
  83. package/dist/types/performance.d.ts +2 -0
  84. package/dist/types/persistence-decoders.d.ts +12 -0
  85. package/dist/types/public-api.d.ts +209 -0
  86. package/dist/types/qa-assistant-state.d.ts +34 -0
  87. package/dist/types/react/index.d.ts +28 -3
  88. package/dist/types/reassert-sentinel.d.ts +6 -0
  89. package/dist/types/redirect.d.ts +48 -0
  90. package/dist/types/replay-recorder-entry.d.ts +1 -0
  91. package/dist/types/replay-signal-state.d.ts +6 -0
  92. package/dist/types/replay-signals.d.ts +13 -0
  93. package/dist/types/replay.d.ts +110 -0
  94. package/dist/types/router.d.ts +12 -2
  95. package/dist/types/runtime-state.d.ts +166 -0
  96. package/dist/types/sanitize.d.ts +1 -0
  97. package/dist/types/shadow.d.ts +3 -0
  98. package/dist/types/shopify-cart.d.ts +25 -1
  99. package/dist/types/storefront-vitals.d.ts +24 -0
  100. package/dist/types/targeting-diagnostics.d.ts +1 -0
  101. package/dist/types/targeting.d.ts +16 -2
  102. package/dist/types/track.d.ts +15 -1
  103. package/dist/types/types.d.ts +115 -9
  104. package/dist/types/wait.d.ts +14 -1
  105. package/dist/types/weekday-schedule.d.ts +3 -0
  106. package/package.json +17 -8
  107. package/src/activation-runtime.ts +2737 -0
  108. package/src/analytics-runtime.ts +2175 -0
  109. package/src/antiflicker.test.ts +216 -44
  110. package/src/antiflicker.ts +103 -28
  111. package/src/artifact-concurrency.test.ts +1091 -0
  112. package/src/assignment-cookie-sync.test.ts +202 -0
  113. package/src/assignment-cookie-sync.ts +189 -0
  114. package/src/assignment-runtime.ts +3204 -0
  115. package/src/assignment-store.test.ts +56 -0
  116. package/src/assignment-store.ts +7 -38
  117. package/src/bucket.ts +3 -18
  118. package/src/commerce-analytics-lifecycle.test.ts +75 -15
  119. package/src/commerce-config-lifecycle.test.ts +81 -1
  120. package/src/commerce-price-preserve.test.ts +133 -0
  121. package/src/commerce-price.test.ts +97 -0
  122. package/src/commerce-price.ts +74 -20
  123. package/src/commerce-shipping.test.ts +53 -0
  124. package/src/commerce-shipping.ts +5 -11
  125. package/src/compact-store.ts +15 -0
  126. package/src/config-fetch.test.ts +480 -9
  127. package/src/config-runtime.ts +2070 -0
  128. package/src/consent-evaluation-cookiescript.test.ts +50 -0
  129. package/src/consent-evaluation.ts +138 -0
  130. package/src/css.test.ts +30 -0
  131. package/src/css.ts +120 -6
  132. package/src/cwv.test.ts +60 -77
  133. package/src/debug-loader.test.ts +214 -0
  134. package/src/debug-loader.ts +41 -0
  135. package/src/decide-assignments.test.ts +196 -0
  136. package/src/decide-assignments.ts +204 -0
  137. package/src/delivery-timing.test.ts +104 -0
  138. package/src/delivery-timing.ts +89 -0
  139. package/src/early-interaction-capture.test.ts +451 -0
  140. package/src/early-interaction-capture.ts +230 -0
  141. package/src/early-reassert.test.ts +207 -0
  142. package/src/early-reassert.ts +152 -0
  143. package/src/edge-adoption.test.ts +435 -0
  144. package/src/edge-runtime.ts +30 -0
  145. package/src/editor/api.test.ts +109 -0
  146. package/src/editor/api.ts +34 -3
  147. package/src/editor/controller.test.ts +297 -14
  148. package/src/editor/controller.ts +69 -5
  149. package/src/editor/index.test.ts +2 -0
  150. package/src/editor/index.ts +1 -0
  151. package/src/editor/publication-result.ts +58 -0
  152. package/src/editor/types.ts +10 -1
  153. package/src/editor/view-kit.ts +2 -1
  154. package/src/editor/view.test.ts +108 -0
  155. package/src/editor/view.ts +49 -7
  156. package/src/endpoint.test.ts +15 -0
  157. package/src/endpoint.ts +10 -0
  158. package/src/exclusion-groups.ts +8 -18
  159. package/src/flags.test.ts +88 -1
  160. package/src/flags.ts +29 -29
  161. package/src/global-runtime-reconciler.test.ts +271 -0
  162. package/src/goal-eligibility.test.ts +29 -0
  163. package/src/goal-eligibility.ts +17 -0
  164. package/src/goals.test.ts +144 -1
  165. package/src/goals.ts +97 -18
  166. package/src/guards.test.ts +99 -1
  167. package/src/guards.ts +34 -15
  168. package/src/handover-cache-order.test.ts +6 -3
  169. package/src/handover.test.ts +154 -8
  170. package/src/handover.ts +122 -12
  171. package/src/hash.ts +29 -54
  172. package/src/holdout.test.ts +26 -0
  173. package/src/hydration.test.ts +73 -2
  174. package/src/hydration.ts +18 -9
  175. package/src/index.ts +130 -7287
  176. package/src/init-analytics.test.ts +1694 -0
  177. package/src/init-anti-flicker.test.ts +1211 -0
  178. package/src/init-cart-attributes.test.ts +2003 -0
  179. package/src/init-config-resolution.test.ts +999 -0
  180. package/src/init-consent.test.ts +971 -0
  181. package/src/init-empty.test.ts +11 -5406
  182. package/src/init-integrations.test.ts +846 -0
  183. package/src/init-page-activation.test.ts +5064 -0
  184. package/src/init-qa-loading.test.ts +740 -0
  185. package/src/init-redirects.test.ts +2156 -0
  186. package/src/init-test-helpers.ts +287 -0
  187. package/src/inline-bootstrap.test.ts +184 -0
  188. package/src/inline-bootstrap.ts +73 -0
  189. package/src/js.test.ts +51 -1
  190. package/src/js.ts +86 -2
  191. package/src/money.test.ts +55 -1
  192. package/src/money.ts +104 -2
  193. package/src/money.web-parity.contract.test.ts +85 -0
  194. package/src/mutate.test.ts +474 -10
  195. package/src/mutate.ts +739 -230
  196. package/src/page-trigger.test.ts +218 -8
  197. package/src/performance.ts +10 -0
  198. package/src/persistence-decoders.ts +18 -0
  199. package/src/public-api.ts +1828 -0
  200. package/src/qa-assistant-state.test.ts +249 -3
  201. package/src/qa-assistant-state.ts +146 -6
  202. package/src/qa-assistant-view.test.ts +220 -2
  203. package/src/qa-assistant-view.ts +102 -5
  204. package/src/react/index.test.tsx +37 -0
  205. package/src/react/index.tsx +19 -3
  206. package/src/reassert-sentinel.test.ts +155 -1
  207. package/src/reassert-sentinel.ts +168 -8
  208. package/src/redirect.test.ts +541 -0
  209. package/src/redirect.ts +572 -0
  210. package/src/replay-recorder-entry.ts +115 -0
  211. package/src/replay-signal-state.ts +100 -0
  212. package/src/replay-signals.test.ts +249 -0
  213. package/src/replay-signals.ts +151 -0
  214. package/src/replay.test.ts +764 -0
  215. package/src/replay.ts +514 -0
  216. package/src/router.test.ts +61 -2
  217. package/src/router.ts +50 -12
  218. package/src/runtime-state.ts +507 -0
  219. package/src/runtime.test.ts +174 -7
  220. package/src/runtime.ts +88 -14
  221. package/src/sanitize.test.ts +34 -0
  222. package/src/sanitize.ts +23 -12
  223. package/src/shadow.test.ts +187 -0
  224. package/src/shadow.ts +99 -0
  225. package/src/shopify-cart.test.ts +733 -23
  226. package/src/shopify-cart.ts +402 -49
  227. package/src/storefront-vitals.test.ts +217 -0
  228. package/src/storefront-vitals.ts +206 -0
  229. package/src/surface-signals.ts +6 -0
  230. package/src/targeting-conformance.test.ts +9 -0
  231. package/src/targeting-diagnostics.ts +38 -8
  232. package/src/targeting-parity-corpus.json +559 -0
  233. package/src/targeting-parity-corpus.test.ts +18 -0
  234. package/src/targeting.detailed.test.ts +105 -0
  235. package/src/targeting.test.ts +211 -0
  236. package/src/targeting.ts +88 -207
  237. package/src/track.test.ts +147 -0
  238. package/src/track.ts +125 -26
  239. package/src/types.ts +120 -13
  240. package/src/wait.test.ts +23 -0
  241. package/src/wait.ts +129 -13
  242. package/src/weekday-delivery.test.ts +56 -0
  243. package/src/weekday-schedule-parity-corpus.json +92 -0
  244. package/src/weekday-schedule.test.ts +69 -0
  245. package/src/weekday-schedule.ts +20 -0
@@ -1,5 +1,9 @@
1
1
  import type { Assignment, CommercePriceSettings } from "./types";
2
- import { formatCentsLike, parseDisplayedCents } from "./money";
2
+ import {
3
+ currencyExponent,
4
+ formatCentsLike,
5
+ parseDisplayedCents,
6
+ } from "./money";
3
7
 
4
8
  type PriceAssignmentAuthorization = (assignment: Assignment) => boolean;
5
9
 
@@ -23,8 +27,6 @@ type ShopifyWindow = Window & {
23
27
  };
24
28
  };
25
29
 
26
- const ZERO_DECIMAL =
27
- /^(BIF|CLP|DJF|GNF|ISK|JPY|KMF|KRW|PYG|RWF|UGX|VND|VUV|XAF|XOF|XPF)$/;
28
30
  let observer: MutationObserver | null = null;
29
31
  let rootObserver: MutationObserver | null = null;
30
32
  let variantForm: HTMLFormElement | null = null;
@@ -147,6 +149,13 @@ function activeExperiments(
147
149
  )
148
150
  return [];
149
151
  const currency = w.Shopify.currency?.active;
152
+ if (typeof currency !== "string") return [];
153
+ let minorUnits: number;
154
+ try {
155
+ minorUnits = currencyExponent(currency);
156
+ } catch {
157
+ return [];
158
+ }
150
159
  const pageVariantIds = new Set(
151
160
  meta.product.variants
152
161
  .filter((variant) => variant.id != null)
@@ -176,7 +185,7 @@ function activeExperiments(
176
185
  adjustments &&
177
186
  targetsCurrentProduct
178
187
  ) {
179
- active.push({ experiment, assignment, currency });
188
+ active.push({ experiment, assignment, currency, minorUnits });
180
189
  }
181
190
  }
182
191
  return active;
@@ -275,6 +284,50 @@ function treatmentUnits(value: string, minorUnits: number): number | null {
275
284
  return Number.isSafeInteger(units) ? units : null;
276
285
  }
277
286
 
287
+ function shopifyStoredMinorUnits(
288
+ value: number,
289
+ minorUnits: number,
290
+ shopifyStoredScale: number,
291
+ ): number | null {
292
+ if (
293
+ !Number.isFinite(value) ||
294
+ value < 0 ||
295
+ !Number.isSafeInteger(minorUnits) ||
296
+ minorUnits < 0 ||
297
+ !Number.isSafeInteger(shopifyStoredScale) ||
298
+ shopifyStoredScale <= 0
299
+ ) {
300
+ return null;
301
+ }
302
+ const match = /^(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/i.exec(value.toString());
303
+ if (!match) return null;
304
+ const fraction = match[2] ?? "";
305
+ const exponent = Number(match[3] ?? 0);
306
+ if (!Number.isSafeInteger(exponent)) return null;
307
+ const digits = `${match[1]}${fraction}`;
308
+ const decimalScale = fraction.length - exponent;
309
+ const scaleDelta = minorUnits - decimalScale;
310
+ let scaledDigits: string;
311
+ if (scaleDelta >= 0) {
312
+ scaledDigits = digits + "0".repeat(scaleDelta);
313
+ } else {
314
+ const discardedLength = -scaleDelta;
315
+ if (discardedLength > digits.length) {
316
+ if (/[^0]/.test(digits)) return null;
317
+ scaledDigits = "0";
318
+ } else {
319
+ const discarded = digits.slice(digits.length - discardedLength);
320
+ if (/[^0]/.test(discarded)) return null;
321
+ scaledDigits = digits.slice(0, digits.length - discardedLength) || "0";
322
+ }
323
+ }
324
+ const scaled = Number(scaledDigits);
325
+ if (!Number.isSafeInteger(scaled) || scaled % shopifyStoredScale !== 0) {
326
+ return null;
327
+ }
328
+ return scaled / shopifyStoredScale;
329
+ }
330
+
278
331
  function replaceNumericText(
279
332
  value: string,
280
333
  targets: Map<number, number>,
@@ -443,15 +496,14 @@ function swapPass(): void {
443
496
  const variants = (window as ShopifyWindow).ShopifyAnalytics?.meta?.product
444
497
  ?.variants;
445
498
  if (!active.length || !Array.isArray(variants)) return;
446
- const currency = active[0]!.currency;
447
- const minorUnits = ZERO_DECIMAL.test(currency) ? 0 : 2;
448
- // ShopifyAnalytics keeps prices on Shopify's two-decimal stored scale even
449
- // when the storefront formats a zero-decimal currency such as JPY.
450
- const shopifyStoredScale = minorUnits ? 1 : 100;
499
+ const minorUnits = active[0]!.minorUnits;
500
+ // ShopifyAnalytics uses Shopify's fixed two-decimal stored scale. This
501
+ // scale is independent of the display currency's ISO-4217 exponent.
502
+ const shopifyStoredScale = 100;
451
503
  const published = new Map<string, number>();
452
504
  for (const variant of variants) {
453
505
  const price = Number(variant.price);
454
- if (variant.id != null && Number.isSafeInteger(price) && price >= 0)
506
+ if (variant.id != null && Number.isFinite(price) && price >= 0)
455
507
  published.set(String(variant.id), price);
456
508
  }
457
509
  const selectedByVariant = new Map<string, string>();
@@ -514,16 +566,17 @@ function swapPass(): void {
514
566
  fixedPricePerUnit,
515
567
  minorUnits,
516
568
  );
569
+ const sourceDisplayAmount = source == null
570
+ ? null
571
+ : shopifyStoredMinorUnits(source, minorUnits, shopifyStoredScale);
517
572
  // The display plane must mirror the Cart Transform's decrease-only
518
573
  // invariant. Invalid, equal, or higher treatment prices fail closed.
519
574
  if (
520
- source == null ||
575
+ sourceDisplayAmount == null ||
521
576
  treatmentDisplayAmount == null ||
522
- treatmentDisplayAmount * shopifyStoredScale >= source
577
+ treatmentDisplayAmount >= sourceDisplayAmount
523
578
  )
524
579
  continue;
525
- if (source % shopifyStoredScale) continue;
526
- const sourceDisplayAmount = source / shopifyStoredScale;
527
580
  const existing = targets.get(sourceDisplayAmount);
528
581
  targets.set(
529
582
  sourceDisplayAmount,
@@ -539,10 +592,9 @@ function swapPass(): void {
539
592
  if (compare.mode !== "set") continue;
540
593
  const source = published.get(id);
541
594
  const compareAmount = treatmentUnits(compare.amount, minorUnits);
542
- const sourceAmount =
543
- source != null && source % shopifyStoredScale === 0
544
- ? source / shopifyStoredScale
545
- : null;
595
+ const sourceAmount = source == null
596
+ ? null
597
+ : shopifyStoredMinorUnits(source, minorUnits, shopifyStoredScale);
546
598
  const displayedAmount =
547
599
  sourceAmount == null ? null : targets.get(sourceAmount) || sourceAmount;
548
600
  if (
@@ -610,9 +662,12 @@ function swapPass(): void {
610
662
  const source = selectedVariantId
611
663
  ? published.get(selectedVariantId)
612
664
  : undefined;
665
+ const sourceDisplayAmount = source == null
666
+ ? null
667
+ : shopifyStoredMinorUnits(source, minorUnits, shopifyStoredScale);
613
668
  // The current price is read from Shopify and is never replaced for a
614
669
  // compare-at-only arm. Only add merchant-authored reference framing.
615
- if (amount != null && source != null) {
670
+ if (amount != null && sourceDisplayAmount != null) {
616
671
  const eligible = containers.filter(
617
672
  (container) =>
618
673
  !isElementLinkedToDifferentProduct(container, productHandle),
@@ -636,7 +691,6 @@ function swapPass(): void {
636
691
  container.parentElement?.insertBefore(node, container);
637
692
  compareAtNodes.push(node);
638
693
  }
639
- const sourceDisplayAmount = source / shopifyStoredScale;
640
694
  const currentText = container.textContent ?? "";
641
695
  const parsedCurrent = parseDisplayedCents(currentText, minorUnits);
642
696
  // A node whose text still holds several amounts is not safe to
@@ -500,6 +500,59 @@ describe("commerce shipping messaging", () => {
500
500
  ).toBe("fixed");
501
501
  });
502
502
 
503
+ it("cancels queued placement checks and cannot re-arm them after teardown", async () => {
504
+ vi.useFakeTimers();
505
+ applyCommerceShipping(settings("banner"), assignments);
506
+ await vi.runAllTimersAsync();
507
+ expect(document.querySelector("[data-apex-shipping]")).not.toBeNull();
508
+
509
+ document.body.append(document.createElement("div"));
510
+ await Promise.resolve();
511
+ expect(vi.getTimerCount()).toBeGreaterThan(0);
512
+
513
+ cleanupCommerceShipping();
514
+ expect(vi.getTimerCount()).toBe(0);
515
+ await vi.advanceTimersByTimeAsync(100);
516
+ expect(document.querySelector("[data-apex-shipping]")).toBeNull();
517
+
518
+ document.body.append(document.createElement("div"));
519
+ await Promise.resolve();
520
+ expect(vi.getTimerCount()).toBe(0);
521
+ });
522
+
523
+ it("ignores a placement-observer callback delivered after teardown", async () => {
524
+ vi.useFakeTimers();
525
+ const callbacks: MutationCallback[] = [];
526
+ vi.stubGlobal(
527
+ "MutationObserver",
528
+ class {
529
+ constructor(callback: MutationCallback) {
530
+ callbacks.push(callback);
531
+ }
532
+ observe() {}
533
+ disconnect() {}
534
+ takeRecords() {
535
+ return [];
536
+ }
537
+ },
538
+ );
539
+ applyCommerceShipping(settings("banner"), assignments);
540
+ await vi.runAllTimersAsync();
541
+ expect(document.querySelector("[data-apex-shipping]")).not.toBeNull();
542
+ expect(callbacks.length).toBeGreaterThan(0);
543
+
544
+ cleanupCommerceShipping();
545
+ expect(vi.getTimerCount()).toBe(0);
546
+ // Simulate a mutation record queued before disconnect() but delivered
547
+ // after teardown: the stale callback must not re-arm placement work.
548
+ for (const callback of callbacks) {
549
+ callback([], {} as MutationObserver);
550
+ }
551
+ expect(vi.getTimerCount()).toBe(0);
552
+ await vi.advanceTimersByTimeAsync(100);
553
+ expect(document.querySelector("[data-apex-shipping]")).toBeNull();
554
+ });
555
+
503
556
  it("re-reads after cart mutations without replacing their response", async () => {
504
557
  applyCommerceShipping(settings(), assignments);
505
558
  await flush();
@@ -4,6 +4,7 @@ import {
4
4
  findCartDrawerContainer,
5
5
  installCartDrawerOpenListener,
6
6
  } from "./surface-signals";
7
+ import { currencyExponent } from "./money";
7
8
 
8
9
  type ShopifyWindow = Window & {
9
10
  Shopify?: {
@@ -108,17 +109,7 @@ export function isShippingCartEligibilityConfirmed(
108
109
 
109
110
  function currencyMinorUnits(currency: string): number | null {
110
111
  try {
111
- const units = new Intl.NumberFormat("en", {
112
- style: "currency",
113
- currency,
114
- }).resolvedOptions().maximumFractionDigits;
115
- return typeof units === "number" &&
116
- Number.isInteger(units) &&
117
- units >= 0 &&
118
- // ISO 4217 tops out at exponent 4 (CLF, UYW).
119
- units <= 4
120
- ? units
121
- : null;
112
+ return currencyExponent(currency);
122
113
  } catch {
123
114
  return null;
124
115
  }
@@ -386,6 +377,9 @@ async function refreshCart(
386
377
 
387
378
  function installPlacementObserver(active: ActiveShipping): void {
388
379
  placementObserver = new MutationObserver(() => {
380
+ // A mutation record may already be queued when teardown disconnects the
381
+ // observer. Do not let that stale callback re-arm placement work.
382
+ if (current !== active) return;
389
383
  if (placementCheckTimer !== null) return;
390
384
  placementCheckTimer = window.setTimeout(() => {
391
385
  placementCheckTimer = null;
@@ -0,0 +1,15 @@
1
+ export function decodeCompactStore(raw: string | null, width: 2 | 3): Record<string, unknown[]> {
2
+ if (!raw) return {};
3
+ try {
4
+ const parsed = JSON.parse(raw) as unknown;
5
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
6
+ const out: Record<string, unknown[]> = {};
7
+ for (const [key, value] of Object.entries(parsed)) {
8
+ if (!Array.isArray(value) || value.length < width || typeof value[0] !== "string" || typeof value[1] !== "number" || (width === 3 && typeof value[2] !== "number")) continue;
9
+ out[key] = value;
10
+ }
11
+ return out;
12
+ } catch {
13
+ return {};
14
+ }
15
+ }