@nevios/storefront-kit 1.3.0 → 1.5.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.js CHANGED
@@ -1,14 +1,14 @@
1
- import * as React19 from 'react';
1
+ import * as React27 from 'react';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
  import { imageSrcSet, StorefrontError } from '@nevios/storefront-js';
4
4
 
5
5
  // src/provider.tsx
6
- var StorefrontContext = React19.createContext(null);
6
+ var StorefrontContext = React27.createContext(null);
7
7
  function NeviosProvider({ client, children }) {
8
8
  return /* @__PURE__ */ jsx(StorefrontContext.Provider, { value: client, children });
9
9
  }
10
10
  function useStorefront() {
11
- const client = React19.useContext(StorefrontContext);
11
+ const client = React27.useContext(StorefrontContext);
12
12
  if (!client) {
13
13
  throw new Error(
14
14
  "useStorefront must be used inside <NeviosProvider>. Wrap your app root with it."
@@ -17,7 +17,7 @@ function useStorefront() {
17
17
  return client;
18
18
  }
19
19
  function useStorefrontOptional() {
20
- return React19.useContext(StorefrontContext);
20
+ return React27.useContext(StorefrontContext);
21
21
  }
22
22
  var DEFAULTS = {
23
23
  brand: "#111111",
@@ -53,7 +53,7 @@ function ThemeProvider({
53
53
  as: Tag = "div"
54
54
  }) {
55
55
  const style = themeToCssVars(theme);
56
- return React19.createElement(
56
+ return React27.createElement(
57
57
  Tag,
58
58
  { "data-nevios-theme": "", style },
59
59
  children
@@ -62,6 +62,16 @@ function ThemeProvider({
62
62
 
63
63
  // src/i18n/en.ts
64
64
  var en = {
65
+ "subscriptions.purchaseOption": "Purchase option",
66
+ "subscriptions.empty": "You have no subscriptions yet.",
67
+ "subscriptions.status.active": "Active",
68
+ "subscriptions.status.paused": "Paused",
69
+ "subscriptions.status.pending": "Awaiting payment",
70
+ "subscriptions.status.cancelled": "Cancelled",
71
+ "subscriptions.title": "Subscription",
72
+ "subscriptions.consent": "I agree to the recurring price, delivery schedule and terms shown above.",
73
+ "subscriptions.eachDelivery": "For each delivery",
74
+ "subscriptions.once": "One-time purchase",
65
75
  // common
66
76
  "common.free": "Free",
67
77
  "common.close": "Close",
@@ -216,6 +226,45 @@ var en = {
216
226
  "error.generic.title": "Something went wrong",
217
227
  "error.generic.body": "Please try again in a moment.",
218
228
  "error.home": "Back to home",
229
+ "subscriptions.mixPrice": "Fixed box price; shipping is separate",
230
+ "subscriptions.locked.inactive": "This subscription is not active",
231
+ "subscriptions.locked.cutoff": "The edit cutoff has passed",
232
+ "subscriptions.locked.in_flight": "This shipment or payment is already being processed",
233
+ "subscriptions.quantity": "Quantity",
234
+ "subscriptions.back": "Back",
235
+ "subscriptions.loading": "Loading subscription\u2026",
236
+ "subscriptions.loadingProducts": "Loading products\u2026",
237
+ "subscriptions.chargeDate": "Scheduled charge",
238
+ "subscriptions.noticeHeld": "The charge is waiting for a confirmed advance notice.",
239
+ "subscriptions.nextDelivery": "Next shipment",
240
+ "subscriptions.onlyNext": "Only in this shipment",
241
+ "subscriptions.toCharge": "Amount to charge",
242
+ "subscriptions.noticeStatus": "Renewal notice",
243
+ "subscriptions.notice.pending": "Pending",
244
+ "subscriptions.notice.queued": "Queued",
245
+ "subscriptions.notice.sent": "Sent",
246
+ "subscriptions.notice.failed": "Needs attention",
247
+ "subscriptions.notice.superseded": "Updated notice pending",
248
+ "subscriptions.editUntil": "Changes available until",
249
+ "subscriptions.changeInterval": "Change delivery frequency",
250
+ "subscriptions.addNext": "Add to next shipment",
251
+ "subscriptions.changeMix": "Change my mix",
252
+ "subscriptions.skip": "Skip next shipment",
253
+ "subscriptions.skipWarning": "This shipment and its one-time additions will be skipped. Following shipments keep their regular contents.",
254
+ "subscriptions.futureOnly": "Applies to future shipments. Existing orders stay unchanged.",
255
+ "subscriptions.nextUnchanged": "Next date",
256
+ "subscriptions.reviewTotal": "Reviewed amount",
257
+ "subscriptions.acceptChange": "I agree to the displayed contents, schedule and price.",
258
+ "subscriptions.reloadBeforeRetry": "Reload before another attempt",
259
+ "subscriptions.confirmChange": "Confirm change",
260
+ "subscriptions.reviewChange": "Review change",
261
+ "subscriptions.unit.week": "Every {count} week(s)",
262
+ "subscriptions.unit.month": "Every {count} month(s)",
263
+ "subscriptions.unit.year": "Every {count} year(s)",
264
+ "subscriptions.searchProducts": "Search products",
265
+ "subscriptions.addProduct": "Add product",
266
+ "subscriptions.mixCount": "{count} selected \xB7 choose {min}\u2013{max} units",
267
+ "subscriptions.addMix": "Add mix to cart",
219
268
  // account
220
269
  "account.title": "Your account",
221
270
  "account.signIn": "Sign in",
@@ -278,6 +327,16 @@ var en = {
278
327
 
279
328
  // src/i18n/cs.ts
280
329
  var cs = {
330
+ "subscriptions.purchaseOption": "Zp\u016Fsob n\xE1kupu",
331
+ "subscriptions.empty": "Zat\xEDm nem\xE1te \u017E\xE1dn\xE9 p\u0159edplatn\xE9.",
332
+ "subscriptions.status.active": "Aktivn\xED",
333
+ "subscriptions.status.paused": "Pozastaveno",
334
+ "subscriptions.status.pending": "\u010Cek\xE1 na platbu",
335
+ "subscriptions.status.cancelled": "Zru\u0161eno",
336
+ "subscriptions.title": "P\u0159edplatn\xE9",
337
+ "subscriptions.consent": "Souhlas\xEDm s uvedenou opakovanou cenou, pl\xE1nem doru\u010Den\xED a podm\xEDnkami.",
338
+ "subscriptions.eachDelivery": "Za ka\u017Ed\xE9 doru\u010Den\xED",
339
+ "subscriptions.once": "Jednor\xE1zov\xFD n\xE1kup",
281
340
  "common.free": "Zdarma",
282
341
  "common.close": "Zav\u0159\xEDt",
283
342
  "common.loadingShort": "\u2026",
@@ -412,6 +471,45 @@ var cs = {
412
471
  "error.generic.title": "N\u011Bco se pokazilo",
413
472
  "error.generic.body": "Zkuste to pros\xEDm za chv\xEDli znovu.",
414
473
  "error.home": "Zp\u011Bt na \xFAvod",
474
+ "subscriptions.mixPrice": "Pevn\xE1 cena mixu; doprava se po\u010D\xEDt\xE1 zvl\xE1\u0161\u0165",
475
+ "subscriptions.locked.inactive": "P\u0159edplatn\xE9 nen\xED aktivn\xED.",
476
+ "subscriptions.locked.cutoff": "Term\xEDn pro \xFApravy t\xE9to z\xE1silky ji\u017E uplynul.",
477
+ "subscriptions.locked.in_flight": "Z\xE1silka nebo jej\xED platba se ji\u017E zpracov\xE1v\xE1.",
478
+ "subscriptions.quantity": "Mno\u017Estv\xED",
479
+ "subscriptions.back": "Zp\u011Bt",
480
+ "subscriptions.loading": "Na\u010D\xEDt\xE1n\xED p\u0159edplatn\xE9ho\u2026",
481
+ "subscriptions.loadingProducts": "Na\u010D\xEDt\xE1n\xED produkt\u016F\u2026",
482
+ "subscriptions.chargeDate": "Pl\xE1novan\xE9 str\u017Een\xED",
483
+ "subscriptions.noticeHeld": "Platba \u010Dek\xE1 na potvrzen\xE9 odesl\xE1n\xED upozorn\u011Bn\xED a sjednan\xFD p\u0159edstih.",
484
+ "subscriptions.nextDelivery": "P\u0159\xED\u0161t\xED z\xE1silka",
485
+ "subscriptions.onlyNext": "Jen v t\xE9to z\xE1silce",
486
+ "subscriptions.toCharge": "\u010C\xE1stka ke str\u017Een\xED",
487
+ "subscriptions.noticeStatus": "Upozorn\u011Bn\xED p\u0159ed platbou",
488
+ "subscriptions.notice.pending": "\u010Cek\xE1 na odesl\xE1n\xED",
489
+ "subscriptions.notice.queued": "Ve front\u011B",
490
+ "subscriptions.notice.sent": "Odesl\xE1no",
491
+ "subscriptions.notice.failed": "Vy\u017Eaduje pozornost",
492
+ "subscriptions.notice.superseded": "\u010Cek\xE1 na aktualizovan\xE9 upozorn\u011Bn\xED",
493
+ "subscriptions.editUntil": "\xDApravy mo\u017En\xE9 do",
494
+ "subscriptions.changeInterval": "Zm\u011Bnit interval doru\u010Den\xED",
495
+ "subscriptions.addNext": "P\u0159idat k p\u0159\xED\u0161t\xED z\xE1silce",
496
+ "subscriptions.changeMix": "Zm\u011Bnit vlastn\xED mix",
497
+ "subscriptions.skip": "P\u0159esko\u010Dit p\u0159\xED\u0161t\xED z\xE1silku",
498
+ "subscriptions.skipWarning": "Tato z\xE1silka se p\u0159esko\u010D\xED v\u010Detn\u011B jednor\xE1zov\u011B p\u0159idan\xFDch produkt\u016F. Dal\u0161\xED z\xE1silky budou m\xEDt b\u011B\u017En\xFD obsah p\u0159edplatn\xE9ho.",
499
+ "subscriptions.futureOnly": "Plat\xED pro budouc\xED z\xE1silky. Existuj\xEDc\xED objedn\xE1vky se nezm\u011Bn\xED.",
500
+ "subscriptions.nextUnchanged": "Nejbli\u017E\u0161\xED term\xEDn",
501
+ "subscriptions.reviewTotal": "Potvrzovan\xE1 \u010D\xE1stka",
502
+ "subscriptions.acceptChange": "Souhlas\xEDm se zobrazen\xFDm obsahem, term\xEDny a cenou.",
503
+ "subscriptions.reloadBeforeRetry": "Obnovit p\u0159ed dal\u0161\xEDm pokusem",
504
+ "subscriptions.confirmChange": "Potvrdit zm\u011Bnu",
505
+ "subscriptions.reviewChange": "Zkontrolovat zm\u011Bnu",
506
+ "subscriptions.unit.week": "Ka\u017Ed\xFDch {count} t\xFDdn\u016F",
507
+ "subscriptions.unit.month": "Ka\u017Ed\xFDch {count} m\u011Bs\xEDc\u016F",
508
+ "subscriptions.unit.year": "Ka\u017Ed\xFDch {count} let",
509
+ "subscriptions.searchProducts": "Hledat produkty",
510
+ "subscriptions.addProduct": "P\u0159idat produkt",
511
+ "subscriptions.mixCount": "Vybr\xE1no {count} \xB7 zvolte {min}\u2013{max} kus\u016F",
512
+ "subscriptions.addMix": "P\u0159idat mix do ko\u0161\xEDku",
415
513
  "account.title": "V\xE1\u0161 \xFA\u010Det",
416
514
  "account.signIn": "P\u0159ihl\xE1sit se",
417
515
  "account.signInBlurb": "Zadejte e-mail a po\u0161leme v\xE1m p\u0159ihla\u0161ovac\xED odkaz.",
@@ -519,13 +617,13 @@ function makeI18n(locale, messages) {
519
617
  return { locale, messages, t };
520
618
  }
521
619
  var DEFAULT_I18N = makeI18n("en", en);
522
- var I18nContext = React19.createContext(null);
620
+ var I18nContext = React27.createContext(null);
523
621
  function I18nProvider({
524
622
  locale,
525
623
  messages,
526
624
  children
527
625
  }) {
528
- const value = React19.useMemo(() => {
626
+ const value = React27.useMemo(() => {
529
627
  const lang = langOf(locale);
530
628
  if (locale && !(lang in BUNDLED) && !messages) {
531
629
  devWarn(
@@ -539,7 +637,7 @@ function I18nProvider({
539
637
  return /* @__PURE__ */ jsx(I18nContext.Provider, { value, children });
540
638
  }
541
639
  function useI18n() {
542
- return React19.useContext(I18nContext) ?? DEFAULT_I18N;
640
+ return React27.useContext(I18nContext) ?? DEFAULT_I18N;
543
641
  }
544
642
  function useT() {
545
643
  return useI18n().t;
@@ -663,7 +761,7 @@ function ProductCard({ product, href, locale }) {
663
761
  var MAIN_WIDTHS = [600, 800, 1e3];
664
762
  var THUMB_WIDTHS = [56, 112];
665
763
  function ProductGallery({ media, alt }) {
666
- const [active, setActive] = React19.useState(0);
764
+ const [active, setActive] = React27.useState(0);
667
765
  const imageBase = useStorefrontOptional()?.imageBase;
668
766
  if (media.length === 0) {
669
767
  return /* @__PURE__ */ jsx(
@@ -751,10 +849,10 @@ function matchVariant(product, selected) {
751
849
  ) ?? null;
752
850
  }
753
851
  function VariantPicker({ product, onChange }) {
754
- const [selected, setSelected] = React19.useState(
852
+ const [selected, setSelected] = React27.useState(
755
853
  () => product.variants[0]?.options ?? []
756
854
  );
757
- React19.useEffect(() => {
855
+ React27.useEffect(() => {
758
856
  onChange?.(matchVariant(product, selected));
759
857
  }, [product, selected]);
760
858
  if (product.options.length === 0) return null;
@@ -795,8 +893,8 @@ function BackInStockForm({
795
893
  }) {
796
894
  const t = useT();
797
895
  const client = useStorefront();
798
- const [email, setEmail] = React19.useState(defaultEmail);
799
- const [state, setState] = React19.useState("idle");
896
+ const [email, setEmail] = React27.useState(defaultEmail);
897
+ const [state, setState] = React27.useState("idle");
800
898
  if (!variant || variant.availability?.status !== "out_of_stock") return null;
801
899
  async function submit(e) {
802
900
  e.preventDefault();
@@ -896,7 +994,7 @@ function ProductDetailPage({
896
994
  locale,
897
995
  addToCart
898
996
  }) {
899
- const [variant, setVariant] = React19.useState(
997
+ const [variant, setVariant] = React27.useState(
900
998
  product.variants[0] ?? null
901
999
  );
902
1000
  return /* @__PURE__ */ jsxs(
@@ -1128,12 +1226,28 @@ function TagFilter({ tags, selected = [], onChange }) {
1128
1226
  }) })
1129
1227
  ] });
1130
1228
  }
1131
- var CartContext = React19.createContext(null);
1132
- function CartProvider({ children }) {
1229
+ function PurchaseOptions({ plans, value, onChange, disabled, locale }) {
1230
+ const t = useT();
1231
+ const loc = useFormatLocale(locale);
1232
+ const name = React27.useId();
1233
+ const options = [{ id: null, name: t("subscriptions.once"), price: void 0 }, ...plans];
1234
+ return /* @__PURE__ */ jsxs("fieldset", { disabled, style: { border: 0, padding: 0, margin: 0, display: "grid", gap: "0.5rem" }, children: [
1235
+ /* @__PURE__ */ jsx("legend", { style: { marginBottom: "0.5rem", fontWeight: 500 }, children: t("subscriptions.purchaseOption") }),
1236
+ options.map((option) => /* @__PURE__ */ jsxs("label", { style: { display: "flex", gap: "0.6rem", alignItems: "center", padding: "0.75rem", border: `1px solid ${value === option.id ? "var(--nv-fg)" : "var(--nv-border)"}`, borderRadius: "var(--nv-radius)", cursor: "pointer" }, children: [
1237
+ /* @__PURE__ */ jsx("input", { type: "radio", name, checked: value === option.id, onChange: () => onChange(option.id) }),
1238
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: option.name }),
1239
+ option.price ? /* @__PURE__ */ jsx("span", { children: formatCents(option.price.amount_cents, option.price.currency, loc) }) : null
1240
+ ] }, option.id ?? "once"))
1241
+ ] });
1242
+ }
1243
+ var CartContext = React27.createContext(null);
1244
+ function CartProvider({
1245
+ children
1246
+ }) {
1133
1247
  const client = useStorefront();
1134
- const [cart, setCart] = React19.useState(null);
1135
- const [loading, setLoading] = React19.useState(false);
1136
- const run = React19.useCallback(
1248
+ const [cart, setCart] = React27.useState(null);
1249
+ const [loading, setLoading] = React27.useState(false);
1250
+ const run = React27.useCallback(
1137
1251
  async (op) => {
1138
1252
  setLoading(true);
1139
1253
  try {
@@ -1146,7 +1260,7 @@ function CartProvider({ children }) {
1146
1260
  },
1147
1261
  []
1148
1262
  );
1149
- const refresh = React19.useCallback(async () => {
1263
+ const refresh = React27.useCallback(async () => {
1150
1264
  setLoading(true);
1151
1265
  try {
1152
1266
  setCart(await client.cart.get());
@@ -1154,16 +1268,22 @@ function CartProvider({ children }) {
1154
1268
  setLoading(false);
1155
1269
  }
1156
1270
  }, [client]);
1157
- React19.useEffect(() => {
1271
+ React27.useEffect(() => {
1158
1272
  void refresh();
1159
1273
  }, [refresh]);
1160
- const value = React19.useMemo(
1274
+ const value = React27.useMemo(
1161
1275
  () => ({
1162
1276
  cart,
1163
1277
  loading,
1164
1278
  itemCount: cart?.totals.item_count ?? 0,
1165
1279
  addLine: (input) => run(() => client.cart.addLine(input)),
1280
+ setSubscriptionMix: (planId, lines, expectedLineIds) => run(
1281
+ () => client.cart.setSubscriptionMix(planId, lines, expectedLineIds, {
1282
+ expectedRevision: cart?.subscription?.revision
1283
+ })
1284
+ ),
1166
1285
  updateLine: (lineId, quantity) => run(() => client.cart.updateLine(lineId, quantity)),
1286
+ clearSubscription: () => run(() => client.cart.clearSubscription()),
1167
1287
  removeLine: (lineId) => run(() => client.cart.removeLine(lineId)),
1168
1288
  refresh
1169
1289
  }),
@@ -1172,7 +1292,7 @@ function CartProvider({ children }) {
1172
1292
  return /* @__PURE__ */ jsx(CartContext.Provider, { value, children });
1173
1293
  }
1174
1294
  function useCart() {
1175
- const ctx = React19.useContext(CartContext);
1295
+ const ctx = React27.useContext(CartContext);
1176
1296
  if (!ctx) {
1177
1297
  throw new Error("useCart must be used inside <CartProvider>.");
1178
1298
  }
@@ -1181,13 +1301,14 @@ function useCart() {
1181
1301
  function AddToCart({
1182
1302
  variant,
1183
1303
  quantity = 1,
1304
+ subscriptionPlanId,
1184
1305
  children,
1185
1306
  onAdded
1186
1307
  }) {
1187
1308
  const t = useT();
1188
1309
  const { addLine } = useCart();
1189
- const [busy, setBusy] = React19.useState(false);
1190
- const [error, setError] = React19.useState(null);
1310
+ const [busy, setBusy] = React27.useState(false);
1311
+ const [error, setError] = React27.useState(null);
1191
1312
  const disabled = variant == null || busy;
1192
1313
  return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "0.5rem" }, children: [
1193
1314
  /* @__PURE__ */ jsx(
@@ -1200,7 +1321,7 @@ function AddToCart({
1200
1321
  setBusy(true);
1201
1322
  setError(null);
1202
1323
  try {
1203
- await addLine({ variant_id: variant.id, quantity });
1324
+ await addLine({ variant_id: variant.id, quantity, ...subscriptionPlanId ? { subscription_plan_id: subscriptionPlanId } : {} });
1204
1325
  onAdded?.();
1205
1326
  } catch (err) {
1206
1327
  const code = err?.code;
@@ -1229,7 +1350,7 @@ function AddToCart({
1229
1350
  function CartLineItem({ line, locale }) {
1230
1351
  const t = useT();
1231
1352
  const loc = useFormatLocale(locale);
1232
- const { updateLine, removeLine } = useCart();
1353
+ const { cart, updateLine, removeLine } = useCart();
1233
1354
  return /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.85rem", alignItems: "flex-start", padding: "0.75rem 0" }, children: [
1234
1355
  /* @__PURE__ */ jsx(
1235
1356
  "div",
@@ -1258,6 +1379,7 @@ function CartLineItem({ line, locale }) {
1258
1379
  /* @__PURE__ */ jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
1259
1380
  /* @__PURE__ */ jsx("div", { style: { fontWeight: 500, lineHeight: 1.3 }, children: line.title }),
1260
1381
  line.variant_title ? /* @__PURE__ */ jsx("div", { style: { color: "var(--nv-muted)", fontSize: "0.875em" }, children: line.variant_title }) : null,
1382
+ line.subscription_plan_id ? /* @__PURE__ */ jsx("div", { style: { color: "var(--nv-muted)", fontSize: "0.875em" }, children: cart?.subscription?.plan?.name ?? t("subscriptions.title") }) : null,
1261
1383
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.75rem", marginTop: "0.5rem" }, children: [
1262
1384
  /* @__PURE__ */ jsxs(
1263
1385
  "div",
@@ -1336,7 +1458,7 @@ function CartSummary({
1336
1458
  const { cart } = useCart();
1337
1459
  if (cart == null) return null;
1338
1460
  const t = cart.totals;
1339
- const empty = t.item_count === 0;
1461
+ const empty = t.item_count === 0 || cart.subscription?.available === false;
1340
1462
  const cta = checkoutLabel ?? tr("cart.checkout");
1341
1463
  const ctaStyle = {
1342
1464
  display: "block",
@@ -1357,6 +1479,7 @@ function CartSummary({
1357
1479
  t.tax_cents > 0 ? /* @__PURE__ */ jsx(Row, { label: tr("cart.tax"), value: formatCents(t.tax_cents, t.currency, loc), muted: true }) : null,
1358
1480
  /* @__PURE__ */ jsx("div", { style: { height: 1, background: "var(--nv-border)" } }),
1359
1481
  /* @__PURE__ */ jsx(Row, { label: tr("cart.total"), value: formatCents(t.total_cents, t.currency, loc), strong: true }),
1482
+ cart.subscription?.error ? /* @__PURE__ */ jsx("p", { role: "alert", style: { margin: 0 }, children: cart.subscription.error.message }) : null,
1360
1483
  checkoutHref && !empty ? /* @__PURE__ */ jsx("a", { href: checkoutHref, style: ctaStyle, children: cta }) : /* @__PURE__ */ jsx("button", { type: "button", disabled: empty, onClick: onCheckout, style: ctaStyle, children: cta })
1361
1484
  ] });
1362
1485
  }
@@ -1463,15 +1586,15 @@ function CartDrawer({
1463
1586
  }
1464
1587
  );
1465
1588
  }
1466
- var CheckoutContext = React19.createContext(null);
1589
+ var CheckoutContext = React27.createContext(null);
1467
1590
  function CheckoutProvider({
1468
1591
  children
1469
1592
  }) {
1470
1593
  const client = useStorefront();
1471
- const [checkout, setCheckout] = React19.useState(null);
1472
- const [loading, setLoading] = React19.useState(false);
1473
- const [error, setError] = React19.useState(null);
1474
- const run = React19.useCallback(
1594
+ const [checkout, setCheckout] = React27.useState(null);
1595
+ const [loading, setLoading] = React27.useState(false);
1596
+ const [error, setError] = React27.useState(null);
1597
+ const run = React27.useCallback(
1475
1598
  async (op, store) => {
1476
1599
  setLoading(true);
1477
1600
  try {
@@ -1489,23 +1612,36 @@ function CheckoutProvider({
1489
1612
  },
1490
1613
  []
1491
1614
  );
1492
- const refresh = React19.useCallback(async () => {
1615
+ const refresh = React27.useCallback(async () => {
1493
1616
  setLoading(true);
1494
1617
  try {
1495
1618
  setCheckout(await client.checkout.get());
1496
1619
  setError(null);
1620
+ } catch (err) {
1621
+ setError(err instanceof Error ? err : new Error(String(err)));
1497
1622
  } finally {
1498
1623
  setLoading(false);
1499
1624
  }
1500
1625
  }, [client]);
1501
- React19.useEffect(() => {
1626
+ React27.useEffect(() => {
1502
1627
  void refresh();
1503
1628
  }, [refresh]);
1504
- const pay = React19.useCallback(
1629
+ const pay = React27.useCallback(
1505
1630
  async (opts = {}) => {
1506
1631
  const co = checkout;
1507
1632
  if (!co) throw new Error("pay() called before a checkout exists");
1508
1633
  const idempotencyKey = opts.idempotencyKey ?? co.token;
1634
+ if (co.subscription) {
1635
+ if (co.subscription.subscription_id) {
1636
+ const payment = await run(() => client.checkout.subscriptionPayment(), () => null);
1637
+ setCheckout(await client.checkout.get());
1638
+ return { kind: "subscription_payment", payment };
1639
+ }
1640
+ if (!opts.subscription) throw new Error("Accept the current subscription terms before paying");
1641
+ const subscription = await run(() => client.checkout.enrollSubscription(opts.subscription), () => null);
1642
+ setCheckout(await client.checkout.get());
1643
+ return { kind: "subscription", subscription };
1644
+ }
1509
1645
  if (co.payment_category === "gateway") {
1510
1646
  const start = await run(
1511
1647
  () => client.checkout.startPayment({ return_url: opts.returnUrl, cancel_url: opts.cancelUrl }),
@@ -1524,11 +1660,18 @@ function CheckoutProvider({
1524
1660
  },
1525
1661
  [checkout, client, run]
1526
1662
  );
1527
- const value = React19.useMemo(
1663
+ const value = React27.useMemo(
1528
1664
  () => ({
1529
1665
  checkout,
1530
1666
  loading,
1531
1667
  error,
1668
+ selectSubscription: (selection) => run(async () => {
1669
+ const quote = await client.checkout.selectSubscription(selection);
1670
+ setCheckout(await client.checkout.get());
1671
+ return quote;
1672
+ }, () => null),
1673
+ clearSubscription: () => run(() => client.checkout.clearSubscription(), (r) => r),
1674
+ reviewSubscription: () => run(() => client.checkout.reviewSubscription(), () => null),
1532
1675
  create: (cartToken, input) => run(() => client.checkout.create(cartToken, input), (r) => r),
1533
1676
  setAddress: (input) => run(() => client.checkout.setAddress(input), (r) => r),
1534
1677
  shippingQuotes: () => run(() => client.checkout.shippingQuotes(), () => null),
@@ -1549,7 +1692,7 @@ function CheckoutProvider({
1549
1692
  return /* @__PURE__ */ jsx(CheckoutContext.Provider, { value, children });
1550
1693
  }
1551
1694
  function useCheckout() {
1552
- const ctx = React19.useContext(CheckoutContext);
1695
+ const ctx = React27.useContext(CheckoutContext);
1553
1696
  if (!ctx) {
1554
1697
  throw new Error("useCheckout must be used inside <CheckoutProvider>.");
1555
1698
  }
@@ -1708,11 +1851,11 @@ function AddressForm({
1708
1851
  }) {
1709
1852
  const t = useT();
1710
1853
  const { checkout, setAddress, loading } = useCheckout();
1711
- const [form, setForm] = React19.useState(
1854
+ const [form, setForm] = React27.useState(
1712
1855
  () => seed(checkout?.email ?? null, checkout?.shipping_address ?? null)
1713
1856
  );
1714
- const touched = React19.useRef(/* @__PURE__ */ new Set());
1715
- React19.useEffect(() => {
1857
+ const touched = React27.useRef(/* @__PURE__ */ new Set());
1858
+ React27.useEffect(() => {
1716
1859
  const prefill = checkout?.customer_prefill;
1717
1860
  const addresses = prefill?.addresses.filter((a) => !a.is_pickup);
1718
1861
  const saved = addresses?.find((a) => a.is_default) ?? addresses?.[0];
@@ -1777,11 +1920,11 @@ function ShippingPicker({
1777
1920
  const loc = useFormatLocale(locale);
1778
1921
  const { checkout, shippingQuotes, setShippingMethod, loading } = useCheckout();
1779
1922
  const heading = title ?? t("checkout.shippingMethod");
1780
- const [quotes, setQuotes] = React19.useState(null);
1781
- const [selected, setSelected] = React19.useState(null);
1782
- const [fetching, setFetching] = React19.useState(true);
1923
+ const [quotes, setQuotes] = React27.useState(null);
1924
+ const [selected, setSelected] = React27.useState(null);
1925
+ const [fetching, setFetching] = React27.useState(true);
1783
1926
  const addressKey = checkout?.shipping_address ? `${checkout.shipping_address.country_code}:${checkout.shipping_address.postal_code}` : null;
1784
- React19.useEffect(() => {
1927
+ React27.useEffect(() => {
1785
1928
  let live = true;
1786
1929
  setFetching(true);
1787
1930
  shippingQuotes().then((qs) => {
@@ -1856,20 +1999,45 @@ function PaymentMethodPicker({
1856
1999
  }
1857
2000
  );
1858
2001
  }
2002
+ function SubscriptionTerms({ quote, checked, onChange, locale }) {
2003
+ const t = useT();
2004
+ const loc = useFormatLocale(locale);
2005
+ return /* @__PURE__ */ jsxs("fieldset", { style: { margin: 0, padding: "1rem", border: "1px solid var(--nv-border)", borderRadius: "var(--nv-radius)" }, children: [
2006
+ /* @__PURE__ */ jsx("legend", { children: t("subscriptions.title") }),
2007
+ /* @__PURE__ */ jsxs("p", { style: { margin: "0 0 0.5rem", fontWeight: 500 }, children: [
2008
+ t("subscriptions.eachDelivery"),
2009
+ ": ",
2010
+ formatCents(quote.recurring_total_cents, quote.currency, loc)
2011
+ ] }),
2012
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 0.75rem", whiteSpace: "pre-wrap" }, children: quote.terms_text }),
2013
+ /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "flex-start", gap: "0.6rem", cursor: "pointer" }, children: [
2014
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked, onChange: (event) => onChange(event.target.checked) }),
2015
+ /* @__PURE__ */ jsx("span", { children: t("subscriptions.consent") })
2016
+ ] })
2017
+ ] });
2018
+ }
1859
2019
  function PaymentSection({
1860
2020
  title,
1861
2021
  returnUrl,
1862
2022
  cancelUrl,
1863
2023
  onOrderPlaced,
1864
- locale
2024
+ locale,
2025
+ onSubscriptionPayment
1865
2026
  }) {
1866
2027
  const t = useT();
1867
2028
  const heading = title ?? t("checkout.payment");
1868
- const { checkout, paymentMethods, setPaymentMethod, pay, loading } = useCheckout();
1869
- const [methods, setMethods] = React19.useState(null);
1870
- const [fetching, setFetching] = React19.useState(true);
2029
+ const { checkout, paymentMethods, setPaymentMethod, pay, loading, refresh } = useCheckout();
2030
+ const [methods, setMethods] = React27.useState(null);
2031
+ const [fetching, setFetching] = React27.useState(true);
1871
2032
  const selected = checkout?.payment_method ?? null;
1872
- React19.useEffect(() => {
2033
+ const [acceptedDigest, setAcceptedDigest] = React27.useState(null);
2034
+ const [actionBusy, setActionBusy] = React27.useState(false);
2035
+ const quote = checkout?.subscription?.quote;
2036
+ const needsConsent = Boolean(checkout?.subscription?.requires_consent);
2037
+ const clickInFlight = React27.useRef(false);
2038
+ const [actionError, setActionError] = React27.useState(null);
2039
+ const accepted = Boolean(quote && acceptedDigest === quote.quote_digest);
2040
+ React27.useEffect(() => {
1873
2041
  let live = true;
1874
2042
  setFetching(true);
1875
2043
  paymentMethods().then((ms) => {
@@ -1882,12 +2050,30 @@ function PaymentSection({
1882
2050
  return () => {
1883
2051
  live = false;
1884
2052
  };
1885
- }, []);
2053
+ }, [checkout?.token, checkout?.shipping_method_id, checkout?.subscription?.selection.plan_id]);
1886
2054
  const selectedMethod = methods?.find((m) => m.method === selected) ?? null;
1887
2055
  const isGateway = selectedMethod?.category === "gateway";
1888
2056
  async function placeOrder() {
1889
- const result = await pay({ returnUrl, cancelUrl });
1890
- if (result.kind === "order") onOrderPlaced?.(result.checkout);
2057
+ if (clickInFlight.current) return;
2058
+ clickInFlight.current = true;
2059
+ setActionBusy(true);
2060
+ setActionError(null);
2061
+ try {
2062
+ const result = await pay({
2063
+ returnUrl,
2064
+ cancelUrl,
2065
+ ...needsConsent && quote && accepted ? { subscription: { quote, consent: true } } : {}
2066
+ });
2067
+ if (result.kind === "subscription") await onSubscriptionPayment?.(result.subscription.payment);
2068
+ if (result.kind === "subscription_payment") await onSubscriptionPayment?.(result.payment);
2069
+ if (result.kind === "subscription" || result.kind === "subscription_payment") await refresh();
2070
+ if (result.kind === "order") onOrderPlaced?.(result.checkout);
2071
+ } catch (err) {
2072
+ setActionError(err instanceof Error ? err.message : String(err));
2073
+ } finally {
2074
+ clickInFlight.current = false;
2075
+ setActionBusy(false);
2076
+ }
1891
2077
  }
1892
2078
  if (fetching && methods == null) {
1893
2079
  return /* @__PURE__ */ jsx(Section, { title: heading, children: /* @__PURE__ */ jsx("p", { style: { color: "var(--nv-muted)", margin: 0 }, children: t("checkout.loadingPayment") }) });
@@ -1907,9 +2093,12 @@ function PaymentSection({
1907
2093
  label: heading
1908
2094
  }
1909
2095
  ),
2096
+ quote && needsConsent ? /* @__PURE__ */ jsx(SubscriptionTerms, { locale, quote, checked: accepted, onChange: (value) => setAcceptedDigest(value && quote ? quote.quote_digest : null) }) : null,
2097
+ checkout?.subscription?.error ? /* @__PURE__ */ jsx("p", { role: "alert", children: checkout.subscription.error.message }) : null,
2098
+ actionError ? /* @__PURE__ */ jsx("p", { role: "alert", children: actionError }) : null,
1910
2099
  /* @__PURE__ */ jsxs("div", { children: [
1911
- /* @__PURE__ */ jsx(Button, { onClick: placeOrder, busy: loading, disabled: selected == null, children: isGateway ? t("checkout.payNow") : t("checkout.placeOrder") }),
1912
- isGateway ? /* @__PURE__ */ jsx("p", { style: { fontSize: "0.8125rem", color: "var(--nv-muted)", marginTop: "0.6rem" }, children: t("checkout.gatewayRedirectNote") }) : null
2100
+ /* @__PURE__ */ jsx(Button, { onClick: placeOrder, busy: loading || actionBusy, disabled: selected == null || Boolean(checkout?.subscription) && (needsConsent && (!accepted || !quote) || !onSubscriptionPayment || !checkout?.subscription?.available), children: isGateway ? t("checkout.payNow") : t("checkout.placeOrder") }),
2101
+ isGateway && !checkout?.subscription ? /* @__PURE__ */ jsx("p", { style: { fontSize: "0.8125rem", color: "var(--nv-muted)", marginTop: "0.6rem" }, children: t("checkout.gatewayRedirectNote") }) : null
1913
2102
  ] })
1914
2103
  ] });
1915
2104
  }
@@ -1997,6 +2186,7 @@ function reachedStep(checkout) {
1997
2186
  }
1998
2187
  function CheckoutFlow({
1999
2188
  cartToken,
2189
+ onSubscriptionPayment,
2000
2190
  onOrderPlaced,
2001
2191
  emptyState,
2002
2192
  renderConfirmation,
@@ -2006,19 +2196,19 @@ function CheckoutFlow({
2006
2196
  }) {
2007
2197
  const t = useT();
2008
2198
  const { checkout, loading, create } = useCheckout();
2009
- const [step, setStep] = React19.useState("address");
2010
- const [bootstrapped, setBootstrapped] = React19.useState(false);
2011
- React19.useEffect(() => {
2199
+ const [step, setStep] = React27.useState("address");
2200
+ const [bootstrapped, setBootstrapped] = React27.useState(false);
2201
+ React27.useEffect(() => {
2012
2202
  if (checkout || !cartToken || bootstrapped) return;
2013
2203
  setBootstrapped(true);
2014
2204
  void create(cartToken).catch(() => {
2015
2205
  });
2016
2206
  }, [checkout, cartToken, bootstrapped, create]);
2017
2207
  const serverStep = reachedStep(checkout);
2018
- React19.useEffect(() => {
2208
+ React27.useEffect(() => {
2019
2209
  setStep((cur) => rank(serverStep) > rank(cur) ? serverStep : cur);
2020
2210
  }, [serverStep]);
2021
- if (checkout?.order_id) {
2211
+ if (checkout?.order_id && (!checkout.subscription || checkout.phase === "success")) {
2022
2212
  return /* @__PURE__ */ jsx(Shell, { summary: /* @__PURE__ */ jsx(OrderSummary, { locale }), children: renderConfirmation ? renderConfirmation(checkout) : /* @__PURE__ */ jsx(DefaultConfirmation, { checkout }) });
2023
2213
  }
2024
2214
  if (!checkout) {
@@ -2032,6 +2222,7 @@ function CheckoutFlow({
2032
2222
  step === "address" ? /* @__PURE__ */ jsx(AddressForm, { onComplete: () => setStep("shipping") }) : step === "shipping" ? /* @__PURE__ */ jsx(ShippingPicker, { onComplete: () => setStep("payment"), locale }) : /* @__PURE__ */ jsx(
2033
2223
  PaymentSection,
2034
2224
  {
2225
+ onSubscriptionPayment,
2035
2226
  returnUrl,
2036
2227
  cancelUrl,
2037
2228
  onOrderPlaced,
@@ -2136,10 +2327,10 @@ function DefaultConfirmation({ checkout }) {
2136
2327
  }
2137
2328
  function useOrderStatus(statusToken, initial) {
2138
2329
  const client = useStorefront();
2139
- const [order, setOrder] = React19.useState(initial ?? null);
2140
- const [loading, setLoading] = React19.useState(false);
2141
- const [error, setError] = React19.useState(null);
2142
- const load = React19.useCallback(async () => {
2330
+ const [order, setOrder] = React27.useState(initial ?? null);
2331
+ const [loading, setLoading] = React27.useState(false);
2332
+ const [error, setError] = React27.useState(null);
2333
+ const load = React27.useCallback(async () => {
2143
2334
  if (!statusToken) return;
2144
2335
  setLoading(true);
2145
2336
  try {
@@ -2151,7 +2342,7 @@ function useOrderStatus(statusToken, initial) {
2151
2342
  setLoading(false);
2152
2343
  }
2153
2344
  }, [client, statusToken]);
2154
- React19.useEffect(() => {
2345
+ React27.useEffect(() => {
2155
2346
  if (initial) return;
2156
2347
  void load();
2157
2348
  }, [load, initial]);
@@ -2159,12 +2350,12 @@ function useOrderStatus(statusToken, initial) {
2159
2350
  }
2160
2351
  function usePayment(token, initial) {
2161
2352
  const client = useStorefront();
2162
- const [payment, setPayment] = React19.useState(initial ?? null);
2163
- const [methods, setMethods] = React19.useState([]);
2164
- const [loading, setLoading] = React19.useState(false);
2165
- const [busy, setBusy] = React19.useState(false);
2166
- const [error, setError] = React19.useState(null);
2167
- const load = React19.useCallback(async () => {
2353
+ const [payment, setPayment] = React27.useState(initial ?? null);
2354
+ const [methods, setMethods] = React27.useState([]);
2355
+ const [loading, setLoading] = React27.useState(false);
2356
+ const [busy, setBusy] = React27.useState(false);
2357
+ const [error, setError] = React27.useState(null);
2358
+ const load = React27.useCallback(async () => {
2168
2359
  if (!token) return;
2169
2360
  setLoading(true);
2170
2361
  try {
@@ -2181,7 +2372,7 @@ function usePayment(token, initial) {
2181
2372
  setLoading(false);
2182
2373
  }
2183
2374
  }, [client, token]);
2184
- React19.useEffect(() => {
2375
+ React27.useEffect(() => {
2185
2376
  if (!token) return;
2186
2377
  if (initial && initial.token === token) {
2187
2378
  setPayment(initial);
@@ -2190,7 +2381,7 @@ function usePayment(token, initial) {
2190
2381
  }
2191
2382
  void load();
2192
2383
  }, [token, load]);
2193
- const selectMethod = React19.useCallback(
2384
+ const selectMethod = React27.useCallback(
2194
2385
  async (method) => {
2195
2386
  if (!token) return null;
2196
2387
  setBusy(true);
@@ -2208,7 +2399,7 @@ function usePayment(token, initial) {
2208
2399
  },
2209
2400
  [client, token]
2210
2401
  );
2211
- const startPayment = React19.useCallback(
2402
+ const startPayment = React27.useCallback(
2212
2403
  async (input) => {
2213
2404
  if (!token) return null;
2214
2405
  setBusy(true);
@@ -2645,8 +2836,8 @@ function Row4({
2645
2836
  }
2646
2837
  function CopyRow({ label, value }) {
2647
2838
  const t = useT();
2648
- const [copied, setCopied] = React19.useState(false);
2649
- React19.useEffect(() => {
2839
+ const [copied, setCopied] = React27.useState(false);
2840
+ React27.useEffect(() => {
2650
2841
  if (!copied) return;
2651
2842
  const id = setTimeout(() => setCopied(false), 1800);
2652
2843
  return () => clearTimeout(id);
@@ -2730,12 +2921,12 @@ function PaymentPage({
2730
2921
  const t = useT();
2731
2922
  const loc = useFormatLocale(locale);
2732
2923
  const { payment, methods, loading, busy, error, reload, selectMethod, startPayment } = usePayment(token, initial);
2733
- const [verifying, setVerifying] = React19.useState(false);
2734
- React19.useEffect(() => {
2924
+ const [verifying, setVerifying] = React27.useState(false);
2925
+ React27.useEffect(() => {
2735
2926
  if (token) setVerifying(consumeHandoff(token));
2736
2927
  }, [token]);
2737
2928
  const settled = payment != null && !payment.can_pay && payment.blocked_reason === "already_paid";
2738
- React19.useEffect(() => {
2929
+ React27.useEffect(() => {
2739
2930
  if (!verifying || !token || settled) return;
2740
2931
  let attempts = 0;
2741
2932
  const id = setInterval(() => {
@@ -2750,7 +2941,7 @@ function PaymentPage({
2750
2941
  }, POLL_MS);
2751
2942
  return () => clearInterval(id);
2752
2943
  }, [verifying, token, settled, reload]);
2753
- React19.useEffect(() => {
2944
+ React27.useEffect(() => {
2754
2945
  if (!settled || !token || !payment) return;
2755
2946
  setVerifying(false);
2756
2947
  forgetHandoff(token);
@@ -2831,13 +3022,13 @@ function Amount({ label, value }) {
2831
3022
  function Link({ href, children }) {
2832
3023
  return /* @__PURE__ */ jsx("a", { href, style: { color: "var(--nv-brand)", fontSize: "0.875rem" }, children });
2833
3024
  }
2834
- var AccountContext = React19.createContext(null);
3025
+ var AccountContext = React27.createContext(null);
2835
3026
  function AccountProvider({ children }) {
2836
3027
  const client = useStorefront();
2837
- const [customer, setCustomer] = React19.useState(null);
2838
- const [loading, setLoading] = React19.useState(true);
2839
- const [error, setError] = React19.useState(false);
2840
- const refresh = React19.useCallback(async () => {
3028
+ const [customer, setCustomer] = React27.useState(null);
3029
+ const [loading, setLoading] = React27.useState(true);
3030
+ const [error, setError] = React27.useState(false);
3031
+ const refresh = React27.useCallback(async () => {
2841
3032
  setLoading(true);
2842
3033
  setError(false);
2843
3034
  try {
@@ -2848,10 +3039,10 @@ function AccountProvider({ children }) {
2848
3039
  setLoading(false);
2849
3040
  }
2850
3041
  }, [client]);
2851
- React19.useEffect(() => {
3042
+ React27.useEffect(() => {
2852
3043
  void refresh();
2853
3044
  }, [refresh]);
2854
- const value = React19.useMemo(
3045
+ const value = React27.useMemo(
2855
3046
  () => ({
2856
3047
  customer,
2857
3048
  signedIn: customer != null,
@@ -2874,7 +3065,7 @@ function AccountProvider({ children }) {
2874
3065
  return /* @__PURE__ */ jsx(AccountContext.Provider, { value, children });
2875
3066
  }
2876
3067
  function useAccount() {
2877
- const ctx = React19.useContext(AccountContext);
3068
+ const ctx = React27.useContext(AccountContext);
2878
3069
  if (!ctx) {
2879
3070
  throw new Error("useAccount must be used inside <AccountProvider>.");
2880
3071
  }
@@ -2882,9 +3073,9 @@ function useAccount() {
2882
3073
  }
2883
3074
  function useMagicLinkVerify(token) {
2884
3075
  const { verify } = useAccount();
2885
- const [status, setStatus] = React19.useState("idle");
2886
- const done = React19.useRef(false);
2887
- React19.useEffect(() => {
3076
+ const [status, setStatus] = React27.useState("idle");
3077
+ const done = React27.useRef(false);
3078
+ React27.useEffect(() => {
2888
3079
  if (!token || done.current) return;
2889
3080
  done.current = true;
2890
3081
  setStatus("verifying");
@@ -2895,11 +3086,11 @@ function useMagicLinkVerify(token) {
2895
3086
  function LoginForm({ verifyPath = "/account/verify", onSent }) {
2896
3087
  const t = useT();
2897
3088
  const { requestMagicLink } = useAccount();
2898
- const [email, setEmail] = React19.useState("");
2899
- const [busy, setBusy] = React19.useState(false);
2900
- const [sent, setSent] = React19.useState(false);
2901
- const [error, setError] = React19.useState(false);
2902
- const [devLink, setDevLink] = React19.useState(null);
3089
+ const [email, setEmail] = React27.useState("");
3090
+ const [busy, setBusy] = React27.useState(false);
3091
+ const [sent, setSent] = React27.useState(false);
3092
+ const [error, setError] = React27.useState(false);
3093
+ const [devLink, setDevLink] = React27.useState(null);
2903
3094
  async function submit(e) {
2904
3095
  e.preventDefault();
2905
3096
  setBusy(true);
@@ -3011,14 +3202,14 @@ function OrderHistory({
3011
3202
  const loc = useFormatLocale(locale);
3012
3203
  const { signedIn } = useAccount();
3013
3204
  const client = useStorefront();
3014
- const [orders, setOrders] = React19.useState(null);
3015
- const [error, setError] = React19.useState(false);
3016
- const [nextCursor, setNextCursor] = React19.useState(null);
3017
- const [pageLoading, setPageLoading] = React19.useState(false);
3018
- const [reload, setReload] = React19.useState(0);
3019
- const [reordering, setReordering] = React19.useState(null);
3020
- const [notice, setNotice] = React19.useState(null);
3021
- React19.useEffect(() => {
3205
+ const [orders, setOrders] = React27.useState(null);
3206
+ const [error, setError] = React27.useState(false);
3207
+ const [nextCursor, setNextCursor] = React27.useState(null);
3208
+ const [pageLoading, setPageLoading] = React27.useState(false);
3209
+ const [reload, setReload] = React27.useState(0);
3210
+ const [reordering, setReordering] = React27.useState(null);
3211
+ const [notice, setNotice] = React27.useState(null);
3212
+ React27.useEffect(() => {
3022
3213
  let live = true;
3023
3214
  if (!signedIn) {
3024
3215
  setOrders([]);
@@ -3129,7 +3320,7 @@ function OrderHistory({
3129
3320
  }
3130
3321
  function AddressForm2({ initial, onSubmit, onCancel, busy }) {
3131
3322
  const t = useT();
3132
- const [form, setForm] = React19.useState(() => ({
3323
+ const [form, setForm] = React27.useState(() => ({
3133
3324
  first_name: initial?.first_name ?? "",
3134
3325
  last_name: initial?.last_name ?? "",
3135
3326
  company: initial?.company ?? "",
@@ -3227,17 +3418,17 @@ function AddressBook() {
3227
3418
  const t = useT();
3228
3419
  const { signedIn } = useAccount();
3229
3420
  const client = useStorefront();
3230
- const [addresses, setAddresses] = React19.useState(null);
3231
- const [mode, setMode] = React19.useState({ kind: "list" });
3232
- const [busy, setBusy] = React19.useState(false);
3233
- const load = React19.useCallback(() => {
3421
+ const [addresses, setAddresses] = React27.useState(null);
3422
+ const [mode, setMode] = React27.useState({ kind: "list" });
3423
+ const [busy, setBusy] = React27.useState(false);
3424
+ const load = React27.useCallback(() => {
3234
3425
  if (!signedIn) {
3235
3426
  setAddresses([]);
3236
3427
  return;
3237
3428
  }
3238
3429
  client.account.addresses().then(setAddresses).catch(() => setAddresses([]));
3239
3430
  }, [signedIn, client]);
3240
- React19.useEffect(() => {
3431
+ React27.useEffect(() => {
3241
3432
  load();
3242
3433
  }, [load]);
3243
3434
  async function run(fn) {
@@ -3291,10 +3482,10 @@ function ProfileEditor() {
3291
3482
  const t = useT();
3292
3483
  const client = useStorefront();
3293
3484
  const { customer, refresh } = useAccount();
3294
- const [editing, setEditing] = React19.useState(false);
3295
- const [busy, setBusy] = React19.useState(false);
3296
- const [error, setError] = React19.useState(false);
3297
- const [form, setForm] = React19.useState({});
3485
+ const [editing, setEditing] = React27.useState(false);
3486
+ const [busy, setBusy] = React27.useState(false);
3487
+ const [error, setError] = React27.useState(false);
3488
+ const [form, setForm] = React27.useState({});
3298
3489
  if (!customer) return null;
3299
3490
  function startEdit() {
3300
3491
  setForm({
@@ -3375,13 +3566,13 @@ function AccountSecurity() {
3375
3566
  const client = useStorefront();
3376
3567
  const account = useAccount();
3377
3568
  const t = useT();
3378
- const [sessions, setSessions] = React19.useState(null);
3379
- const [email, setEmail] = React19.useState("");
3380
- const [busy, setBusy] = React19.useState(false);
3381
- const [notice, setNotice] = React19.useState(null);
3382
- const [confirmClose, setConfirmClose] = React19.useState(false);
3383
- const [retry, setRetry] = React19.useState(0);
3384
- React19.useEffect(() => {
3569
+ const [sessions, setSessions] = React27.useState(null);
3570
+ const [email, setEmail] = React27.useState("");
3571
+ const [busy, setBusy] = React27.useState(false);
3572
+ const [notice, setNotice] = React27.useState(null);
3573
+ const [confirmClose, setConfirmClose] = React27.useState(false);
3574
+ const [retry, setRetry] = React27.useState(0);
3575
+ React27.useEffect(() => {
3385
3576
  let live = true;
3386
3577
  if (account.signedIn) client.account.sessions().then((s) => {
3387
3578
  if (live) setSessions(s);
@@ -3444,6 +3635,662 @@ function AccountSecurity() {
3444
3635
  ] }) : /* @__PURE__ */ jsx(Button, { disabled: busy, onClick: () => setConfirmClose(true), children: t("account.closeAccount") })
3445
3636
  ] });
3446
3637
  }
3638
+ function useCustomerSubscriptions() {
3639
+ const client = useStorefront();
3640
+ const { customer } = useAccount();
3641
+ const [dataOwner, setDataOwner] = React27.useState(
3642
+ void 0
3643
+ );
3644
+ const [subscriptions, setSubscriptions] = React27.useState([]);
3645
+ const [loading, setLoading] = React27.useState(false);
3646
+ const [error, setError] = React27.useState(null);
3647
+ const generation = React27.useRef(0);
3648
+ const owner = customer?.id;
3649
+ const api = client.account.subscriptions;
3650
+ const refresh = React27.useCallback(async () => {
3651
+ const current = ++generation.current;
3652
+ setError(null);
3653
+ if (!owner) {
3654
+ setSubscriptions([]);
3655
+ setLoading(false);
3656
+ return;
3657
+ }
3658
+ setLoading(true);
3659
+ try {
3660
+ const rows = await api.list();
3661
+ if (current === generation.current) {
3662
+ setSubscriptions(rows);
3663
+ setDataOwner(owner);
3664
+ setError(null);
3665
+ }
3666
+ } catch (err) {
3667
+ if (current === generation.current)
3668
+ setError(err instanceof Error ? err : new Error(String(err)));
3669
+ } finally {
3670
+ if (current === generation.current) setLoading(false);
3671
+ }
3672
+ }, [owner, api]);
3673
+ React27.useEffect(() => {
3674
+ setSubscriptions([]);
3675
+ void refresh();
3676
+ return () => {
3677
+ generation.current++;
3678
+ };
3679
+ }, [refresh]);
3680
+ return {
3681
+ subscriptions: dataOwner === owner ? subscriptions : [],
3682
+ loading,
3683
+ error,
3684
+ refresh,
3685
+ api
3686
+ };
3687
+ }
3688
+ function useCustomerSubscription(id) {
3689
+ const client = useStorefront();
3690
+ const { customer } = useAccount();
3691
+ const [readRevision, setReadRevision] = React27.useState(0);
3692
+ const [dataKey, setDataKey] = React27.useState("");
3693
+ const [subscription, setSubscription] = React27.useState(null);
3694
+ const [loading, setLoading] = React27.useState(false);
3695
+ const [error, setError] = React27.useState(null);
3696
+ const generation = React27.useRef(0);
3697
+ const owner = customer?.id;
3698
+ const api = client.account.subscriptions;
3699
+ const refresh = React27.useCallback(async () => {
3700
+ const current = ++generation.current;
3701
+ setError(null);
3702
+ if (!owner || !id) {
3703
+ setSubscription(null);
3704
+ setLoading(false);
3705
+ return;
3706
+ }
3707
+ setLoading(true);
3708
+ try {
3709
+ const row = await api.management(id);
3710
+ if (current === generation.current) {
3711
+ setSubscription(row);
3712
+ setReadRevision((n) => n + 1);
3713
+ setDataKey(`${owner}:${id}`);
3714
+ setError(null);
3715
+ }
3716
+ } catch (err) {
3717
+ if (current === generation.current)
3718
+ setError(err instanceof Error ? err : new Error(String(err)));
3719
+ } finally {
3720
+ if (current === generation.current) setLoading(false);
3721
+ }
3722
+ }, [owner, id, api]);
3723
+ React27.useEffect(() => {
3724
+ setSubscription(null);
3725
+ void refresh();
3726
+ return () => {
3727
+ generation.current++;
3728
+ };
3729
+ }, [refresh]);
3730
+ return {
3731
+ subscription: dataKey === `${owner}:${id}` ? subscription : null,
3732
+ loading,
3733
+ error,
3734
+ refresh,
3735
+ api,
3736
+ readRevision
3737
+ };
3738
+ }
3739
+ function SubscriptionList({ href, locale }) {
3740
+ const t = useT();
3741
+ const { subscriptions, loading, error } = useCustomerSubscriptions();
3742
+ return /* @__PURE__ */ jsxs("section", { style: { fontFamily: "var(--nv-font)", color: "var(--nv-fg)" }, children: [
3743
+ /* @__PURE__ */ jsx("h2", { style: { fontSize: "1.125rem", fontWeight: 500 }, children: t("subscriptions.title") }),
3744
+ loading ? /* @__PURE__ */ jsx("p", { children: t("checkout.loading") }) : error ? /* @__PURE__ */ jsx("p", { role: "alert", children: error.message }) : subscriptions.length === 0 ? /* @__PURE__ */ jsx("p", { children: t("subscriptions.empty") }) : /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: subscriptions.map((sub) => /* @__PURE__ */ jsx("li", { style: { borderBottom: "1px solid var(--nv-border)", padding: "0.8rem 0" }, children: /* @__PURE__ */ jsxs("a", { href: href(sub.id), style: { color: "inherit", display: "flex", justifyContent: "space-between", gap: "1rem", flexWrap: "wrap" }, children: [
3745
+ /* @__PURE__ */ jsxs("span", { children: [
3746
+ "#",
3747
+ sub.id
3748
+ ] }),
3749
+ /* @__PURE__ */ jsx("span", { children: t(`subscriptions.status.${sub.status}`) }),
3750
+ sub.status === "active" && sub.next_at ? /* @__PURE__ */ jsx("time", { dateTime: sub.next_at, children: new Date(sub.next_at).toLocaleDateString(locale) }) : null
3751
+ ] }) }, sub.id)) })
3752
+ ] });
3753
+ }
3754
+ var subscriptionPanel = {
3755
+ fontFamily: "var(--nv-font)",
3756
+ color: "var(--nv-fg)",
3757
+ background: "var(--nv-bg)",
3758
+ border: "1px solid var(--nv-border)",
3759
+ borderRadius: "var(--nv-radius)",
3760
+ padding: "1rem"
3761
+ };
3762
+ function SubscriptionButton({
3763
+ primary = false,
3764
+ style,
3765
+ ...props
3766
+ }) {
3767
+ return /* @__PURE__ */ jsx(
3768
+ "button",
3769
+ {
3770
+ type: "button",
3771
+ ...props,
3772
+ style: {
3773
+ font: "inherit",
3774
+ fontWeight: 500,
3775
+ padding: ".5rem .85rem",
3776
+ borderRadius: "var(--nv-radius)",
3777
+ border: "1px solid var(--nv-border)",
3778
+ background: primary ? "var(--nv-brand)" : "var(--nv-bg)",
3779
+ color: primary ? "var(--nv-brand-foreground)" : "var(--nv-fg)",
3780
+ cursor: props.disabled ? "default" : "pointer",
3781
+ opacity: props.disabled ? 0.55 : 1,
3782
+ ...style
3783
+ }
3784
+ }
3785
+ );
3786
+ }
3787
+ function ProductSelection({
3788
+ load,
3789
+ value,
3790
+ onChange,
3791
+ initialItems = [],
3792
+ locale,
3793
+ maxQuantity = 99
3794
+ }) {
3795
+ const t = useT(), loc = useFormatLocale(locale), [query, setQuery] = React27.useState(""), [page, setPage] = React27.useState(0), [items, setItems] = React27.useState([]), [more, setMore] = React27.useState(false), [busy, setBusy] = React27.useState(false), [error, setError] = React27.useState("");
3796
+ const known = React27.useRef({});
3797
+ for (const item of initialItems) known.current[item.variant_id] = item;
3798
+ React27.useEffect(() => {
3799
+ const controller = new AbortController();
3800
+ setBusy(true);
3801
+ setError("");
3802
+ const timer = setTimeout(() => {
3803
+ void load(query, page * 50, controller.signal).then((result) => {
3804
+ if (controller.signal.aborted) return;
3805
+ for (const item of result.items)
3806
+ known.current[item.variant_id] = item;
3807
+ setItems((old) => page ? [...old, ...result.items] : result.items);
3808
+ setMore(result.more);
3809
+ }).catch((err) => {
3810
+ if (!controller.signal.aborted)
3811
+ setError(err instanceof Error ? err.message : String(err));
3812
+ }).finally(() => {
3813
+ if (!controller.signal.aborted) setBusy(false);
3814
+ });
3815
+ }, 180);
3816
+ return () => {
3817
+ clearTimeout(timer);
3818
+ controller.abort();
3819
+ };
3820
+ }, [load, query, page]);
3821
+ const selected = Object.keys(value).filter((id) => value[id] > 0).map((id) => known.current[id]).filter(Boolean);
3822
+ return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: ".75rem" }, children: [
3823
+ selected.map((p) => /* @__PURE__ */ jsxs(
3824
+ "label",
3825
+ {
3826
+ style: {
3827
+ display: "flex",
3828
+ justifyContent: "space-between",
3829
+ gap: "1rem",
3830
+ alignItems: "center"
3831
+ },
3832
+ children: [
3833
+ /* @__PURE__ */ jsxs("span", { children: [
3834
+ p.title,
3835
+ " ",
3836
+ p.variant_title,
3837
+ /* @__PURE__ */ jsx("small", { style: { display: "block", color: "var(--nv-muted)" }, children: p.sku })
3838
+ ] }),
3839
+ /* @__PURE__ */ jsx(
3840
+ "input",
3841
+ {
3842
+ "aria-label": `${t("subscriptions.quantity")}: ${p.title}`,
3843
+ type: "number",
3844
+ min: 0,
3845
+ max: maxQuantity,
3846
+ value: value[p.variant_id] ?? 0,
3847
+ onChange: (e) => onChange({ ...value, [p.variant_id]: Number(e.target.value) }),
3848
+ style: {
3849
+ font: "inherit",
3850
+ width: "4.5rem",
3851
+ padding: ".5rem",
3852
+ border: "1px solid var(--nv-border)",
3853
+ borderRadius: "var(--nv-radius)",
3854
+ color: "var(--nv-fg)",
3855
+ background: "var(--nv-bg)"
3856
+ }
3857
+ }
3858
+ )
3859
+ ]
3860
+ },
3861
+ p.variant_id
3862
+ )),
3863
+ /* @__PURE__ */ jsx(
3864
+ "input",
3865
+ {
3866
+ "aria-label": t("subscriptions.searchProducts"),
3867
+ placeholder: t("subscriptions.searchProducts"),
3868
+ value: query,
3869
+ onChange: (e) => {
3870
+ setQuery(e.target.value);
3871
+ setPage(0);
3872
+ setItems([]);
3873
+ },
3874
+ style: {
3875
+ font: "inherit",
3876
+ padding: ".65rem",
3877
+ border: "1px solid var(--nv-border)",
3878
+ borderRadius: "var(--nv-radius)",
3879
+ background: "var(--nv-bg)",
3880
+ color: "var(--nv-fg)"
3881
+ }
3882
+ }
3883
+ ),
3884
+ /* @__PURE__ */ jsx(
3885
+ "ul",
3886
+ {
3887
+ style: {
3888
+ listStyle: "none",
3889
+ padding: 0,
3890
+ margin: 0,
3891
+ maxHeight: "18rem",
3892
+ overflowY: "auto"
3893
+ },
3894
+ children: items.filter((p) => !value[p.variant_id]).map((p) => /* @__PURE__ */ jsxs(
3895
+ "li",
3896
+ {
3897
+ style: {
3898
+ display: "flex",
3899
+ alignItems: "center",
3900
+ justifyContent: "space-between",
3901
+ gap: "1rem",
3902
+ padding: ".5rem 0",
3903
+ borderBottom: "1px solid var(--nv-border)"
3904
+ },
3905
+ children: [
3906
+ /* @__PURE__ */ jsxs("span", { children: [
3907
+ p.title,
3908
+ " ",
3909
+ p.variant_title,
3910
+ /* @__PURE__ */ jsx("small", { style: { display: "block", color: "var(--nv-muted)" }, children: formatCents(p.unit_gross, p.currency, loc) })
3911
+ ] }),
3912
+ /* @__PURE__ */ jsx(
3913
+ SubscriptionButton,
3914
+ {
3915
+ "aria-label": `${t("subscriptions.addProduct")}: ${p.title}`,
3916
+ onClick: () => onChange({ ...value, [p.variant_id]: 1 }),
3917
+ children: "+"
3918
+ }
3919
+ )
3920
+ ]
3921
+ },
3922
+ p.variant_id
3923
+ ))
3924
+ }
3925
+ ),
3926
+ busy ? /* @__PURE__ */ jsx("p", { role: "status", children: t("subscriptions.loadingProducts") }) : more ? /* @__PURE__ */ jsx(SubscriptionButton, { onClick: () => setPage((p) => p + 1), children: t("account.loadMore") }) : null,
3927
+ error && /* @__PURE__ */ jsx("p", { role: "alert", children: error })
3928
+ ] });
3929
+ }
3930
+ function SubscriptionManager({
3931
+ subscriptionId,
3932
+ locale
3933
+ }) {
3934
+ const state = useCustomerSubscription(subscriptionId), { customer } = useAccount(), t = useT();
3935
+ if (!customer) return null;
3936
+ if (state.error)
3937
+ return /* @__PURE__ */ jsxs("p", { role: "alert", children: [
3938
+ state.error.message,
3939
+ " ",
3940
+ /* @__PURE__ */ jsx(SubscriptionButton, { onClick: () => void state.refresh(), children: t("account.retry") })
3941
+ ] });
3942
+ if (!state.subscription)
3943
+ return /* @__PURE__ */ jsx("p", { role: "status", children: t("subscriptions.loading") });
3944
+ return /* @__PURE__ */ jsx(
3945
+ Manager,
3946
+ {
3947
+ state,
3948
+ locale
3949
+ },
3950
+ `${customer.id}:${subscriptionId}:${state.subscription.version}:${state.readRevision}`
3951
+ );
3952
+ }
3953
+ function Manager({
3954
+ state,
3955
+ locale
3956
+ }) {
3957
+ const { subscription: s, api, refresh } = state, t = useT(), loc = useFormatLocale(locale);
3958
+ const [mode, setMode] = React27.useState(null);
3959
+ const [option, setOption] = React27.useState("");
3960
+ const [quantities, setQuantities] = React27.useState(
3961
+ {}
3962
+ );
3963
+ const [quote, setQuote] = React27.useState(null);
3964
+ const [busy, setBusy] = React27.useState(false), [locked, setLocked] = React27.useState(false), [error, setError] = React27.useState("");
3965
+ const [consent, setConsent] = React27.useState(false);
3966
+ const upcoming = s?.upcoming, heading = React27.useRef(null);
3967
+ const loader = React27.useCallback(
3968
+ (q, offset, signal) => api.products(s.id, {
3969
+ q,
3970
+ offset,
3971
+ signal,
3972
+ scope: mode === "mix" ? "mix" : "one_time"
3973
+ }),
3974
+ [api, s?.id, mode]
3975
+ );
3976
+ React27.useEffect(() => {
3977
+ if (mode) heading.current?.focus();
3978
+ }, [mode]);
3979
+ if (!s || !upcoming) return null;
3980
+ if (s.status !== "active")
3981
+ return /* @__PURE__ */ jsx("section", { style: subscriptionPanel, children: /* @__PURE__ */ jsx("h2", { children: t(`subscriptions.status.${s.status}`) }) });
3982
+ const money = (n) => formatCents(n, s.currency, loc);
3983
+ const scope = mode === "mix" ? "recurring" : "one_time";
3984
+ const selected = Object.entries(quantities).filter(([, quantity]) => quantity > 0).map(([variant_id, quantity]) => ({ variant_id, quantity }));
3985
+ const changes = mode === "cadence" ? { cadence_option_id: option } : { lines: selected };
3986
+ const amount = quote ? "recurring_total_cents" in quote ? quote.recurring_total_cents : quote.after.total_cents : upcoming.total_cents;
3987
+ const enter = (next) => {
3988
+ setMode(next);
3989
+ setQuote(null);
3990
+ setConsent(false);
3991
+ setError("");
3992
+ setOption("");
3993
+ setQuantities(
3994
+ Object.fromEntries(
3995
+ upcoming.lines.filter(
3996
+ (li) => li.scope === (next === "mix" ? "recurring" : "one_time")
3997
+ ).map((li) => [li.variant_id, li.quantity])
3998
+ )
3999
+ );
4000
+ };
4001
+ return /* @__PURE__ */ jsxs(
4002
+ "section",
4003
+ {
4004
+ style: { ...subscriptionPanel, display: "grid", gap: "1rem" },
4005
+ "aria-busy": busy,
4006
+ children: [
4007
+ /* @__PURE__ */ jsxs("header", { children: [
4008
+ /* @__PURE__ */ jsx("h2", { style: { margin: 0, fontWeight: 500 }, children: t("subscriptions.nextDelivery") }),
4009
+ /* @__PURE__ */ jsxs("p", { children: [
4010
+ t("subscriptions.chargeDate"),
4011
+ ":",
4012
+ " ",
4013
+ /* @__PURE__ */ jsx("time", { dateTime: upcoming.due_at, children: new Date(upcoming.due_at).toLocaleString(loc) })
4014
+ ] })
4015
+ ] }),
4016
+ /* @__PURE__ */ jsx("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: upcoming.lines.map((li) => /* @__PURE__ */ jsxs(
4017
+ "li",
4018
+ {
4019
+ style: {
4020
+ display: "flex",
4021
+ justifyContent: "space-between",
4022
+ gap: "1rem",
4023
+ padding: ".65rem 0",
4024
+ borderBottom: "1px solid var(--nv-border)"
4025
+ },
4026
+ children: [
4027
+ /* @__PURE__ */ jsxs("span", { children: [
4028
+ li.title ?? li.sku,
4029
+ " \xD7 ",
4030
+ li.quantity,
4031
+ li.scope === "one_time" ? /* @__PURE__ */ jsx("small", { style: { display: "block", color: "var(--nv-muted)" }, children: t("subscriptions.onlyNext") }) : null
4032
+ ] }),
4033
+ /* @__PURE__ */ jsx("span", { children: money(li.unit_gross * li.quantity - (li.discount_cents ?? 0)) })
4034
+ ]
4035
+ },
4036
+ li.line_id
4037
+ )) }),
4038
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
4039
+ /* @__PURE__ */ jsx("span", { children: t("totals.shipping") }),
4040
+ /* @__PURE__ */ jsx("span", { children: money(upcoming.shipping_cents) })
4041
+ ] }),
4042
+ /* @__PURE__ */ jsxs(
4043
+ "div",
4044
+ {
4045
+ style: {
4046
+ display: "flex",
4047
+ justifyContent: "space-between",
4048
+ fontWeight: 500
4049
+ },
4050
+ children: [
4051
+ /* @__PURE__ */ jsx("span", { children: t("subscriptions.toCharge") }),
4052
+ /* @__PURE__ */ jsx("span", { children: money(upcoming.total_cents) })
4053
+ ]
4054
+ }
4055
+ ),
4056
+ upcoming.capabilities.reminder?.enabled ? /* @__PURE__ */ jsxs("p", { style: { color: "var(--nv-muted)", margin: 0 }, children: [
4057
+ t("subscriptions.noticeStatus"),
4058
+ ":",
4059
+ " ",
4060
+ t(`subscriptions.notice.${upcoming.reminder?.status ?? "pending"}`)
4061
+ ] }) : null,
4062
+ upcoming.charge_held_for_notice && /* @__PURE__ */ jsx("p", { role: "status", children: t("subscriptions.noticeHeld") }),
4063
+ !upcoming.editable ? /* @__PURE__ */ jsx("p", { role: "status", children: t(
4064
+ `subscriptions.locked.${upcoming.locked_reason_code ?? "in_flight"}`
4065
+ ) }) : /* @__PURE__ */ jsxs("p", { style: { color: "var(--nv-muted)", margin: 0 }, children: [
4066
+ t("subscriptions.editUntil"),
4067
+ ":",
4068
+ " ",
4069
+ new Date(upcoming.edit_cutoff_at).toLocaleString(loc)
4070
+ ] }),
4071
+ !mode ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: ".5rem", flexWrap: "wrap" }, children: [
4072
+ !!upcoming.capabilities.cadence_options?.length && /* @__PURE__ */ jsx(
4073
+ SubscriptionButton,
4074
+ {
4075
+ disabled: !upcoming.editable,
4076
+ onClick: () => enter("cadence"),
4077
+ children: t("subscriptions.changeInterval")
4078
+ }
4079
+ ),
4080
+ upcoming.capabilities.one_time_addons && /* @__PURE__ */ jsx(
4081
+ SubscriptionButton,
4082
+ {
4083
+ disabled: !upcoming.editable,
4084
+ onClick: () => enter("addons"),
4085
+ children: t("subscriptions.addNext")
4086
+ }
4087
+ ),
4088
+ upcoming.capabilities.mix && /* @__PURE__ */ jsx(
4089
+ SubscriptionButton,
4090
+ {
4091
+ disabled: !upcoming.editable,
4092
+ onClick: () => enter("mix"),
4093
+ children: t("subscriptions.changeMix")
4094
+ }
4095
+ ),
4096
+ s.allowed_actions.skip && /* @__PURE__ */ jsx(SubscriptionButton, { onClick: () => enter("skip"), children: t("subscriptions.skip") })
4097
+ ] }) : /* @__PURE__ */ jsx(
4098
+ "form",
4099
+ {
4100
+ noValidate: true,
4101
+ onSubmit: async (event) => {
4102
+ event.preventDefault();
4103
+ if (busy || locked) return;
4104
+ setBusy(true);
4105
+ setError("");
4106
+ try {
4107
+ if (mode === "skip") {
4108
+ if (!consent) return;
4109
+ try {
4110
+ await api.skipNext(s.id, s.version);
4111
+ await refresh();
4112
+ } catch (err) {
4113
+ setLocked(true);
4114
+ throw err;
4115
+ }
4116
+ } else if (!quote) {
4117
+ const q = mode === "addons" ? await api.previewNextItems(s.id, { addons: selected }) : await api.previewChanges(s.id, changes);
4118
+ setQuote(q);
4119
+ setConsent(false);
4120
+ } else {
4121
+ if (!consent) return;
4122
+ try {
4123
+ if (mode === "addons")
4124
+ await api.commitNextItems(
4125
+ s.id,
4126
+ { addons: selected },
4127
+ quote,
4128
+ "Customer explicitly accepted the displayed next-delivery contents and total"
4129
+ );
4130
+ else
4131
+ await api.commitChanges(
4132
+ s.id,
4133
+ changes,
4134
+ quote,
4135
+ "Customer explicitly accepted the displayed future terms, dates and price"
4136
+ );
4137
+ await refresh();
4138
+ } catch (err) {
4139
+ setLocked(true);
4140
+ throw err;
4141
+ }
4142
+ }
4143
+ } catch (err) {
4144
+ setError(err instanceof Error ? err.message : String(err));
4145
+ } finally {
4146
+ setBusy(false);
4147
+ }
4148
+ },
4149
+ style: {
4150
+ borderTop: "1px solid var(--nv-border)",
4151
+ paddingTop: "1rem",
4152
+ display: "grid",
4153
+ gap: "1rem"
4154
+ },
4155
+ children: /* @__PURE__ */ jsxs(
4156
+ "fieldset",
4157
+ {
4158
+ disabled: busy,
4159
+ style: {
4160
+ border: 0,
4161
+ margin: 0,
4162
+ padding: 0,
4163
+ display: "grid",
4164
+ gap: "1rem",
4165
+ minWidth: 0
4166
+ },
4167
+ children: [
4168
+ /* @__PURE__ */ jsx(
4169
+ "h3",
4170
+ {
4171
+ ref: heading,
4172
+ tabIndex: -1,
4173
+ style: { margin: 0, fontWeight: 500 },
4174
+ children: t(
4175
+ mode === "cadence" ? "subscriptions.changeInterval" : mode === "mix" ? "subscriptions.changeMix" : mode === "skip" ? "subscriptions.skip" : "subscriptions.addNext"
4176
+ )
4177
+ }
4178
+ ),
4179
+ mode === "skip" ? /* @__PURE__ */ jsx("p", { children: t("subscriptions.skipWarning") }) : quote ? /* @__PURE__ */ jsxs("div", { role: "status", children: [
4180
+ /* @__PURE__ */ jsx("p", { children: t(
4181
+ mode === "addons" ? "subscriptions.onlyNext" : "subscriptions.futureOnly"
4182
+ ) }),
4183
+ mode === "cadence" ? /* @__PURE__ */ jsxs("p", { children: [
4184
+ t("subscriptions.nextUnchanged"),
4185
+ ":",
4186
+ " ",
4187
+ new Date(
4188
+ String(quote.after.next_at)
4189
+ ).toLocaleString(loc)
4190
+ ] }) : null,
4191
+ "upcoming_dates" in quote && quote.upcoming_dates?.map((date) => /* @__PURE__ */ jsx("p", { children: new Date(date).toLocaleString(loc) }, date)),
4192
+ mode === "addons" && "addons" in quote.after && quote.after.addons.map(
4193
+ (li) => /* @__PURE__ */ jsxs("p", { children: [
4194
+ li.title ?? li.sku ?? li.variant_id,
4195
+ " \xD7 ",
4196
+ li.quantity
4197
+ ] }, li.variant_id)
4198
+ ),
4199
+ "display_items" in quote && quote.display_items?.map((li) => /* @__PURE__ */ jsxs("p", { children: [
4200
+ li.title,
4201
+ " \xD7 ",
4202
+ li.quantity
4203
+ ] }, li.variant_id)),
4204
+ /* @__PURE__ */ jsxs("strong", { children: [
4205
+ t("subscriptions.reviewTotal"),
4206
+ ": ",
4207
+ money(amount)
4208
+ ] })
4209
+ ] }) : mode === "cadence" ? /* @__PURE__ */ jsx(
4210
+ "div",
4211
+ {
4212
+ role: "group",
4213
+ "aria-label": t("subscriptions.changeInterval"),
4214
+ style: { display: "flex", gap: ".5rem", flexWrap: "wrap" },
4215
+ children: upcoming.capabilities.cadence_options?.map((o) => /* @__PURE__ */ jsx(
4216
+ SubscriptionButton,
4217
+ {
4218
+ "aria-pressed": option === o.id,
4219
+ onClick: () => setOption(o.id),
4220
+ children: t(`subscriptions.unit.${o.unit}`, { count: o.count })
4221
+ },
4222
+ o.id
4223
+ ))
4224
+ }
4225
+ ) : /* @__PURE__ */ jsx(
4226
+ ProductSelection,
4227
+ {
4228
+ maxQuantity: mode === "mix" ? upcoming.capabilities.mix?.per_sku_max ?? 99 : 99,
4229
+ load: loader,
4230
+ value: quantities,
4231
+ onChange: setQuantities,
4232
+ initialItems: upcoming.lines.filter((li) => li.scope === scope).map((li) => ({
4233
+ ...li,
4234
+ title: li.title ?? li.sku ?? li.variant_id,
4235
+ currency: s.currency,
4236
+ variant_title: li.variant_title ?? null,
4237
+ sku: li.sku ?? null
4238
+ })),
4239
+ locale: loc
4240
+ }
4241
+ ),
4242
+ (quote || mode === "skip") && /* @__PURE__ */ jsxs(
4243
+ "label",
4244
+ {
4245
+ style: {
4246
+ display: "flex",
4247
+ gap: ".5rem",
4248
+ alignItems: "flex-start"
4249
+ },
4250
+ children: [
4251
+ /* @__PURE__ */ jsx(
4252
+ "input",
4253
+ {
4254
+ type: "checkbox",
4255
+ checked: consent,
4256
+ onChange: (e) => setConsent(e.target.checked)
4257
+ }
4258
+ ),
4259
+ t("subscriptions.acceptChange")
4260
+ ]
4261
+ }
4262
+ ),
4263
+ error && /* @__PURE__ */ jsx("p", { role: "alert", children: error }),
4264
+ locked ? /* @__PURE__ */ jsx(SubscriptionButton, { onClick: () => void refresh(), children: t("subscriptions.reloadBeforeRetry") }) : /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: ".5rem", flexWrap: "wrap" }, children: [
4265
+ /* @__PURE__ */ jsx(
4266
+ SubscriptionButton,
4267
+ {
4268
+ disabled: busy,
4269
+ onClick: () => quote ? (setQuote(null), setConsent(false)) : enter(null),
4270
+ children: t(quote ? "subscriptions.back" : "account.cancel")
4271
+ }
4272
+ ),
4273
+ /* @__PURE__ */ jsx(
4274
+ SubscriptionButton,
4275
+ {
4276
+ type: "submit",
4277
+ primary: true,
4278
+ disabled: busy || (quote || mode === "skip" ? !consent : mode === "cadence" ? !option : mode === "mix" ? !selected.length : false),
4279
+ children: busy ? t("subscriptions.loading") : t(
4280
+ quote || mode === "skip" ? "subscriptions.confirmChange" : "subscriptions.reviewChange"
4281
+ )
4282
+ }
4283
+ )
4284
+ ] })
4285
+ ]
4286
+ }
4287
+ )
4288
+ }
4289
+ )
4290
+ ]
4291
+ }
4292
+ );
4293
+ }
3447
4294
  function serializeJsonLd(data) {
3448
4295
  return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
3449
4296
  }
@@ -3547,7 +4394,119 @@ function OrganizationJsonLd({
3547
4394
  });
3548
4395
  return /* @__PURE__ */ jsx(JsonLd, { data });
3549
4396
  }
4397
+ function Mix({
4398
+ plan,
4399
+ marketId,
4400
+ locale,
4401
+ onAdded
4402
+ }) {
4403
+ const client = useStorefront(), cart = useCart(), t = useT(), [value, setValue] = React27.useState({}), [busy, setBusy] = React27.useState(false), [error, setError] = React27.useState(""), [locked, setLocked] = React27.useState(false);
4404
+ const load = React27.useCallback(
4405
+ (q, offset, signal) => client.subscriptions.mixProducts(plan.id, {
4406
+ q,
4407
+ offset,
4408
+ marketId,
4409
+ signal
4410
+ }),
4411
+ [client, plan.id, marketId]
4412
+ );
4413
+ const loc = useFormatLocale(locale);
4414
+ const mix = plan.capabilities?.mix;
4415
+ if (!mix) return null;
4416
+ const units = Object.values(value).reduce((a, b) => a + b, 0), lines = Object.entries(value).filter(([, q]) => q > 0).map(([variant_id, quantity]) => ({ variant_id, quantity }));
4417
+ const valid = units >= mix.min_units && units <= mix.max_units && lines.every(
4418
+ (li) => Number.isInteger(li.quantity) && li.quantity <= (mix.per_sku_max ?? 99)
4419
+ ) && (mix.groups ?? []).every((g) => {
4420
+ const n = lines.filter((li) => g.variant_ids.includes(li.variant_id)).reduce((a, li) => a + li.quantity, 0);
4421
+ return n >= g.min_units && n <= g.max_units;
4422
+ });
4423
+ return /* @__PURE__ */ jsxs("section", { style: { ...subscriptionPanel, display: "grid", gap: "1rem" }, children: [
4424
+ /* @__PURE__ */ jsx("h2", { style: { fontWeight: 500, margin: 0 }, children: plan.name }),
4425
+ plan.price && /* @__PURE__ */ jsxs("p", { children: [
4426
+ formatCents(plan.price.amount_cents, plan.price.currency, loc),
4427
+ " \xB7",
4428
+ " ",
4429
+ t("subscriptions.mixPrice")
4430
+ ] }),
4431
+ /* @__PURE__ */ jsx("p", { role: "status", children: t("subscriptions.mixCount", {
4432
+ count: units,
4433
+ min: mix.min_units,
4434
+ max: mix.max_units
4435
+ }) }),
4436
+ mix.groups?.map((g) => /* @__PURE__ */ jsxs("p", { children: [
4437
+ g.name,
4438
+ ": ",
4439
+ g.min_units,
4440
+ "\u2013",
4441
+ g.max_units
4442
+ ] }, g.name)),
4443
+ /* @__PURE__ */ jsx(
4444
+ "fieldset",
4445
+ {
4446
+ disabled: busy || locked,
4447
+ style: { border: 0, padding: 0, margin: 0, minWidth: 0 },
4448
+ children: /* @__PURE__ */ jsx(
4449
+ ProductSelection,
4450
+ {
4451
+ maxQuantity: mix.per_sku_max ?? 99,
4452
+ load,
4453
+ value,
4454
+ onChange: (v) => {
4455
+ setValue(v);
4456
+ setError("");
4457
+ },
4458
+ locale
4459
+ }
4460
+ )
4461
+ }
4462
+ ),
4463
+ error && /* @__PURE__ */ jsx("p", { role: "alert", children: error }),
4464
+ locked ? /* @__PURE__ */ jsx(
4465
+ SubscriptionButton,
4466
+ {
4467
+ onClick: async () => {
4468
+ await cart.refresh();
4469
+ setLocked(false);
4470
+ },
4471
+ children: t("subscriptions.reloadBeforeRetry")
4472
+ }
4473
+ ) : /* @__PURE__ */ jsx(
4474
+ SubscriptionButton,
4475
+ {
4476
+ primary: true,
4477
+ disabled: !valid || busy || cart.loading,
4478
+ onClick: async () => {
4479
+ setBusy(true);
4480
+ setError("");
4481
+ try {
4482
+ await cart.setSubscriptionMix(
4483
+ plan.id,
4484
+ lines,
4485
+ cart.cart?.line_items.filter((li) => li.subscription_plan_id).map((li) => li.id) ?? []
4486
+ );
4487
+ onAdded?.();
4488
+ } catch (err) {
4489
+ setLocked(true);
4490
+ setError(err instanceof Error ? err.message : String(err));
4491
+ } finally {
4492
+ setBusy(false);
4493
+ }
4494
+ },
4495
+ children: t("subscriptions.addMix")
4496
+ }
4497
+ )
4498
+ ] });
4499
+ }
4500
+ function SubscriptionMix(props) {
4501
+ return /* @__PURE__ */ jsx(
4502
+ Mix,
4503
+ {
4504
+ ...props
4505
+ },
4506
+ `${props.plan.id}:${props.plan.version}:${props.marketId ?? ""}`
4507
+ );
4508
+ }
3550
4509
 
3551
- export { AccountDashboard, AccountProvider, AccountSecurity, AddToCart, AddressBook, AddressForm, AvailabilityFilter, BackInStockForm, BreadcrumbJsonLd, CartDrawer, CartLineItem, CartProvider, CartSummary, CheckoutFlow, CheckoutProvider, CollectionPage, I18nProvider, JsonLd, LoginForm, MarketSwitcher, NeviosProvider, OrderConfirmation, OrderHistory, OrderLines, OrderStatusPage, OrderSummary, OrderTotals, OrganizationJsonLd, PaymentMethodPicker, PaymentPage, PaymentSection, PhaseTimeline, PriceMoney, PriceRangeFilter, ProductCard, ProductDetailPage, ProductGallery, ProductJsonLd, ProfileEditor, ShippingPicker, SortSelect, StatusBanner, StockBadge, TagFilter, ThemeProvider, TransferInstructions, VariantPicker, absoluteUrl, bundledMessages, cs, en, formatCents, formatMoney, isTerminal, serializeJsonLd, stripHtml, themeToCssVars, useAccount, useCart, useCheckout, useI18n, useMagicLinkVerify, useOrderStatus, usePayment, useStorefront, useStorefrontOptional, useT };
4510
+ export { AccountDashboard, AccountProvider, AccountSecurity, AddToCart, AddressBook, AddressForm, AvailabilityFilter, BackInStockForm, BreadcrumbJsonLd, CartDrawer, CartLineItem, CartProvider, CartSummary, CheckoutFlow, CheckoutProvider, CollectionPage, I18nProvider, JsonLd, LoginForm, MarketSwitcher, NeviosProvider, OrderConfirmation, OrderHistory, OrderLines, OrderStatusPage, OrderSummary, OrderTotals, OrganizationJsonLd, PaymentMethodPicker, PaymentPage, PaymentSection, PhaseTimeline, PriceMoney, PriceRangeFilter, ProductCard, ProductDetailPage, ProductGallery, ProductJsonLd, ProfileEditor, PurchaseOptions, ShippingPicker, SortSelect, StatusBanner, StockBadge, SubscriptionList, SubscriptionManager, SubscriptionMix, SubscriptionTerms, TagFilter, ThemeProvider, TransferInstructions, VariantPicker, absoluteUrl, bundledMessages, cs, en, formatCents, formatMoney, isTerminal, serializeJsonLd, stripHtml, themeToCssVars, useAccount, useCart, useCheckout, useCustomerSubscription, useCustomerSubscriptions, useI18n, useMagicLinkVerify, useOrderStatus, usePayment, useStorefront, useStorefrontOptional, useT };
3552
4511
  //# sourceMappingURL=index.js.map
3553
4512
  //# sourceMappingURL=index.js.map