@nevios/storefront-kit 1.4.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.cjs CHANGED
@@ -248,6 +248,45 @@ var en = {
248
248
  "error.generic.title": "Something went wrong",
249
249
  "error.generic.body": "Please try again in a moment.",
250
250
  "error.home": "Back to home",
251
+ "subscriptions.mixPrice": "Fixed box price; shipping is separate",
252
+ "subscriptions.locked.inactive": "This subscription is not active",
253
+ "subscriptions.locked.cutoff": "The edit cutoff has passed",
254
+ "subscriptions.locked.in_flight": "This shipment or payment is already being processed",
255
+ "subscriptions.quantity": "Quantity",
256
+ "subscriptions.back": "Back",
257
+ "subscriptions.loading": "Loading subscription\u2026",
258
+ "subscriptions.loadingProducts": "Loading products\u2026",
259
+ "subscriptions.chargeDate": "Scheduled charge",
260
+ "subscriptions.noticeHeld": "The charge is waiting for a confirmed advance notice.",
261
+ "subscriptions.nextDelivery": "Next shipment",
262
+ "subscriptions.onlyNext": "Only in this shipment",
263
+ "subscriptions.toCharge": "Amount to charge",
264
+ "subscriptions.noticeStatus": "Renewal notice",
265
+ "subscriptions.notice.pending": "Pending",
266
+ "subscriptions.notice.queued": "Queued",
267
+ "subscriptions.notice.sent": "Sent",
268
+ "subscriptions.notice.failed": "Needs attention",
269
+ "subscriptions.notice.superseded": "Updated notice pending",
270
+ "subscriptions.editUntil": "Changes available until",
271
+ "subscriptions.changeInterval": "Change delivery frequency",
272
+ "subscriptions.addNext": "Add to next shipment",
273
+ "subscriptions.changeMix": "Change my mix",
274
+ "subscriptions.skip": "Skip next shipment",
275
+ "subscriptions.skipWarning": "This shipment and its one-time additions will be skipped. Following shipments keep their regular contents.",
276
+ "subscriptions.futureOnly": "Applies to future shipments. Existing orders stay unchanged.",
277
+ "subscriptions.nextUnchanged": "Next date",
278
+ "subscriptions.reviewTotal": "Reviewed amount",
279
+ "subscriptions.acceptChange": "I agree to the displayed contents, schedule and price.",
280
+ "subscriptions.reloadBeforeRetry": "Reload before another attempt",
281
+ "subscriptions.confirmChange": "Confirm change",
282
+ "subscriptions.reviewChange": "Review change",
283
+ "subscriptions.unit.week": "Every {count} week(s)",
284
+ "subscriptions.unit.month": "Every {count} month(s)",
285
+ "subscriptions.unit.year": "Every {count} year(s)",
286
+ "subscriptions.searchProducts": "Search products",
287
+ "subscriptions.addProduct": "Add product",
288
+ "subscriptions.mixCount": "{count} selected \xB7 choose {min}\u2013{max} units",
289
+ "subscriptions.addMix": "Add mix to cart",
251
290
  // account
252
291
  "account.title": "Your account",
253
292
  "account.signIn": "Sign in",
@@ -454,6 +493,45 @@ var cs = {
454
493
  "error.generic.title": "N\u011Bco se pokazilo",
455
494
  "error.generic.body": "Zkuste to pros\xEDm za chv\xEDli znovu.",
456
495
  "error.home": "Zp\u011Bt na \xFAvod",
496
+ "subscriptions.mixPrice": "Pevn\xE1 cena mixu; doprava se po\u010D\xEDt\xE1 zvl\xE1\u0161\u0165",
497
+ "subscriptions.locked.inactive": "P\u0159edplatn\xE9 nen\xED aktivn\xED.",
498
+ "subscriptions.locked.cutoff": "Term\xEDn pro \xFApravy t\xE9to z\xE1silky ji\u017E uplynul.",
499
+ "subscriptions.locked.in_flight": "Z\xE1silka nebo jej\xED platba se ji\u017E zpracov\xE1v\xE1.",
500
+ "subscriptions.quantity": "Mno\u017Estv\xED",
501
+ "subscriptions.back": "Zp\u011Bt",
502
+ "subscriptions.loading": "Na\u010D\xEDt\xE1n\xED p\u0159edplatn\xE9ho\u2026",
503
+ "subscriptions.loadingProducts": "Na\u010D\xEDt\xE1n\xED produkt\u016F\u2026",
504
+ "subscriptions.chargeDate": "Pl\xE1novan\xE9 str\u017Een\xED",
505
+ "subscriptions.noticeHeld": "Platba \u010Dek\xE1 na potvrzen\xE9 odesl\xE1n\xED upozorn\u011Bn\xED a sjednan\xFD p\u0159edstih.",
506
+ "subscriptions.nextDelivery": "P\u0159\xED\u0161t\xED z\xE1silka",
507
+ "subscriptions.onlyNext": "Jen v t\xE9to z\xE1silce",
508
+ "subscriptions.toCharge": "\u010C\xE1stka ke str\u017Een\xED",
509
+ "subscriptions.noticeStatus": "Upozorn\u011Bn\xED p\u0159ed platbou",
510
+ "subscriptions.notice.pending": "\u010Cek\xE1 na odesl\xE1n\xED",
511
+ "subscriptions.notice.queued": "Ve front\u011B",
512
+ "subscriptions.notice.sent": "Odesl\xE1no",
513
+ "subscriptions.notice.failed": "Vy\u017Eaduje pozornost",
514
+ "subscriptions.notice.superseded": "\u010Cek\xE1 na aktualizovan\xE9 upozorn\u011Bn\xED",
515
+ "subscriptions.editUntil": "\xDApravy mo\u017En\xE9 do",
516
+ "subscriptions.changeInterval": "Zm\u011Bnit interval doru\u010Den\xED",
517
+ "subscriptions.addNext": "P\u0159idat k p\u0159\xED\u0161t\xED z\xE1silce",
518
+ "subscriptions.changeMix": "Zm\u011Bnit vlastn\xED mix",
519
+ "subscriptions.skip": "P\u0159esko\u010Dit p\u0159\xED\u0161t\xED z\xE1silku",
520
+ "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.",
521
+ "subscriptions.futureOnly": "Plat\xED pro budouc\xED z\xE1silky. Existuj\xEDc\xED objedn\xE1vky se nezm\u011Bn\xED.",
522
+ "subscriptions.nextUnchanged": "Nejbli\u017E\u0161\xED term\xEDn",
523
+ "subscriptions.reviewTotal": "Potvrzovan\xE1 \u010D\xE1stka",
524
+ "subscriptions.acceptChange": "Souhlas\xEDm se zobrazen\xFDm obsahem, term\xEDny a cenou.",
525
+ "subscriptions.reloadBeforeRetry": "Obnovit p\u0159ed dal\u0161\xEDm pokusem",
526
+ "subscriptions.confirmChange": "Potvrdit zm\u011Bnu",
527
+ "subscriptions.reviewChange": "Zkontrolovat zm\u011Bnu",
528
+ "subscriptions.unit.week": "Ka\u017Ed\xFDch {count} t\xFDdn\u016F",
529
+ "subscriptions.unit.month": "Ka\u017Ed\xFDch {count} m\u011Bs\xEDc\u016F",
530
+ "subscriptions.unit.year": "Ka\u017Ed\xFDch {count} let",
531
+ "subscriptions.searchProducts": "Hledat produkty",
532
+ "subscriptions.addProduct": "P\u0159idat produkt",
533
+ "subscriptions.mixCount": "Vybr\xE1no {count} \xB7 zvolte {min}\u2013{max} kus\u016F",
534
+ "subscriptions.addMix": "P\u0159idat mix do ko\u0161\xEDku",
457
535
  "account.title": "V\xE1\u0161 \xFA\u010Det",
458
536
  "account.signIn": "P\u0159ihl\xE1sit se",
459
537
  "account.signInBlurb": "Zadejte e-mail a po\u0161leme v\xE1m p\u0159ihla\u0161ovac\xED odkaz.",
@@ -1185,7 +1263,9 @@ function PurchaseOptions({ plans, value, onChange, disabled, locale }) {
1185
1263
  ] });
1186
1264
  }
1187
1265
  var CartContext = React27__namespace.createContext(null);
1188
- function CartProvider({ children }) {
1266
+ function CartProvider({
1267
+ children
1268
+ }) {
1189
1269
  const client = useStorefront();
1190
1270
  const [cart, setCart] = React27__namespace.useState(null);
1191
1271
  const [loading, setLoading] = React27__namespace.useState(false);
@@ -1219,6 +1299,11 @@ function CartProvider({ children }) {
1219
1299
  loading,
1220
1300
  itemCount: cart?.totals.item_count ?? 0,
1221
1301
  addLine: (input) => run(() => client.cart.addLine(input)),
1302
+ setSubscriptionMix: (planId, lines, expectedLineIds) => run(
1303
+ () => client.cart.setSubscriptionMix(planId, lines, expectedLineIds, {
1304
+ expectedRevision: cart?.subscription?.revision
1305
+ })
1306
+ ),
1222
1307
  updateLine: (lineId, quantity) => run(() => client.cart.updateLine(lineId, quantity)),
1223
1308
  clearSubscription: () => run(() => client.cart.clearSubscription()),
1224
1309
  removeLine: (lineId) => run(() => client.cart.removeLine(lineId)),
@@ -3575,7 +3660,9 @@ function AccountSecurity() {
3575
3660
  function useCustomerSubscriptions() {
3576
3661
  const client = useStorefront();
3577
3662
  const { customer } = useAccount();
3578
- const [dataOwner, setDataOwner] = React27__namespace.useState(void 0);
3663
+ const [dataOwner, setDataOwner] = React27__namespace.useState(
3664
+ void 0
3665
+ );
3579
3666
  const [subscriptions, setSubscriptions] = React27__namespace.useState([]);
3580
3667
  const [loading, setLoading] = React27__namespace.useState(false);
3581
3668
  const [error, setError] = React27__namespace.useState(null);
@@ -3599,7 +3686,8 @@ function useCustomerSubscriptions() {
3599
3686
  setError(null);
3600
3687
  }
3601
3688
  } catch (err) {
3602
- if (current === generation.current) setError(err instanceof Error ? err : new Error(String(err)));
3689
+ if (current === generation.current)
3690
+ setError(err instanceof Error ? err : new Error(String(err)));
3603
3691
  } finally {
3604
3692
  if (current === generation.current) setLoading(false);
3605
3693
  }
@@ -3611,11 +3699,18 @@ function useCustomerSubscriptions() {
3611
3699
  generation.current++;
3612
3700
  };
3613
3701
  }, [refresh]);
3614
- return { subscriptions: dataOwner === owner ? subscriptions : [], loading, error, refresh, api };
3702
+ return {
3703
+ subscriptions: dataOwner === owner ? subscriptions : [],
3704
+ loading,
3705
+ error,
3706
+ refresh,
3707
+ api
3708
+ };
3615
3709
  }
3616
3710
  function useCustomerSubscription(id) {
3617
3711
  const client = useStorefront();
3618
3712
  const { customer } = useAccount();
3713
+ const [readRevision, setReadRevision] = React27__namespace.useState(0);
3619
3714
  const [dataKey, setDataKey] = React27__namespace.useState("");
3620
3715
  const [subscription, setSubscription] = React27__namespace.useState(null);
3621
3716
  const [loading, setLoading] = React27__namespace.useState(false);
@@ -3636,11 +3731,13 @@ function useCustomerSubscription(id) {
3636
3731
  const row = await api.management(id);
3637
3732
  if (current === generation.current) {
3638
3733
  setSubscription(row);
3734
+ setReadRevision((n) => n + 1);
3639
3735
  setDataKey(`${owner}:${id}`);
3640
3736
  setError(null);
3641
3737
  }
3642
3738
  } catch (err) {
3643
- if (current === generation.current) setError(err instanceof Error ? err : new Error(String(err)));
3739
+ if (current === generation.current)
3740
+ setError(err instanceof Error ? err : new Error(String(err)));
3644
3741
  } finally {
3645
3742
  if (current === generation.current) setLoading(false);
3646
3743
  }
@@ -3652,7 +3749,14 @@ function useCustomerSubscription(id) {
3652
3749
  generation.current++;
3653
3750
  };
3654
3751
  }, [refresh]);
3655
- return { subscription: dataKey === `${owner}:${id}` ? subscription : null, loading, error, refresh, api };
3752
+ return {
3753
+ subscription: dataKey === `${owner}:${id}` ? subscription : null,
3754
+ loading,
3755
+ error,
3756
+ refresh,
3757
+ api,
3758
+ readRevision
3759
+ };
3656
3760
  }
3657
3761
  function SubscriptionList({ href, locale }) {
3658
3762
  const t = useT();
@@ -3669,6 +3773,546 @@ function SubscriptionList({ href, locale }) {
3669
3773
  ] }) }, sub.id)) })
3670
3774
  ] });
3671
3775
  }
3776
+ var subscriptionPanel = {
3777
+ fontFamily: "var(--nv-font)",
3778
+ color: "var(--nv-fg)",
3779
+ background: "var(--nv-bg)",
3780
+ border: "1px solid var(--nv-border)",
3781
+ borderRadius: "var(--nv-radius)",
3782
+ padding: "1rem"
3783
+ };
3784
+ function SubscriptionButton({
3785
+ primary = false,
3786
+ style,
3787
+ ...props
3788
+ }) {
3789
+ return /* @__PURE__ */ jsxRuntime.jsx(
3790
+ "button",
3791
+ {
3792
+ type: "button",
3793
+ ...props,
3794
+ style: {
3795
+ font: "inherit",
3796
+ fontWeight: 500,
3797
+ padding: ".5rem .85rem",
3798
+ borderRadius: "var(--nv-radius)",
3799
+ border: "1px solid var(--nv-border)",
3800
+ background: primary ? "var(--nv-brand)" : "var(--nv-bg)",
3801
+ color: primary ? "var(--nv-brand-foreground)" : "var(--nv-fg)",
3802
+ cursor: props.disabled ? "default" : "pointer",
3803
+ opacity: props.disabled ? 0.55 : 1,
3804
+ ...style
3805
+ }
3806
+ }
3807
+ );
3808
+ }
3809
+ function ProductSelection({
3810
+ load,
3811
+ value,
3812
+ onChange,
3813
+ initialItems = [],
3814
+ locale,
3815
+ maxQuantity = 99
3816
+ }) {
3817
+ const t = useT(), loc = useFormatLocale(locale), [query, setQuery] = React27__namespace.useState(""), [page, setPage] = React27__namespace.useState(0), [items, setItems] = React27__namespace.useState([]), [more, setMore] = React27__namespace.useState(false), [busy, setBusy] = React27__namespace.useState(false), [error, setError] = React27__namespace.useState("");
3818
+ const known = React27__namespace.useRef({});
3819
+ for (const item of initialItems) known.current[item.variant_id] = item;
3820
+ React27__namespace.useEffect(() => {
3821
+ const controller = new AbortController();
3822
+ setBusy(true);
3823
+ setError("");
3824
+ const timer = setTimeout(() => {
3825
+ void load(query, page * 50, controller.signal).then((result) => {
3826
+ if (controller.signal.aborted) return;
3827
+ for (const item of result.items)
3828
+ known.current[item.variant_id] = item;
3829
+ setItems((old) => page ? [...old, ...result.items] : result.items);
3830
+ setMore(result.more);
3831
+ }).catch((err) => {
3832
+ if (!controller.signal.aborted)
3833
+ setError(err instanceof Error ? err.message : String(err));
3834
+ }).finally(() => {
3835
+ if (!controller.signal.aborted) setBusy(false);
3836
+ });
3837
+ }, 180);
3838
+ return () => {
3839
+ clearTimeout(timer);
3840
+ controller.abort();
3841
+ };
3842
+ }, [load, query, page]);
3843
+ const selected = Object.keys(value).filter((id) => value[id] > 0).map((id) => known.current[id]).filter(Boolean);
3844
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "grid", gap: ".75rem" }, children: [
3845
+ selected.map((p) => /* @__PURE__ */ jsxRuntime.jsxs(
3846
+ "label",
3847
+ {
3848
+ style: {
3849
+ display: "flex",
3850
+ justifyContent: "space-between",
3851
+ gap: "1rem",
3852
+ alignItems: "center"
3853
+ },
3854
+ children: [
3855
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
3856
+ p.title,
3857
+ " ",
3858
+ p.variant_title,
3859
+ /* @__PURE__ */ jsxRuntime.jsx("small", { style: { display: "block", color: "var(--nv-muted)" }, children: p.sku })
3860
+ ] }),
3861
+ /* @__PURE__ */ jsxRuntime.jsx(
3862
+ "input",
3863
+ {
3864
+ "aria-label": `${t("subscriptions.quantity")}: ${p.title}`,
3865
+ type: "number",
3866
+ min: 0,
3867
+ max: maxQuantity,
3868
+ value: value[p.variant_id] ?? 0,
3869
+ onChange: (e) => onChange({ ...value, [p.variant_id]: Number(e.target.value) }),
3870
+ style: {
3871
+ font: "inherit",
3872
+ width: "4.5rem",
3873
+ padding: ".5rem",
3874
+ border: "1px solid var(--nv-border)",
3875
+ borderRadius: "var(--nv-radius)",
3876
+ color: "var(--nv-fg)",
3877
+ background: "var(--nv-bg)"
3878
+ }
3879
+ }
3880
+ )
3881
+ ]
3882
+ },
3883
+ p.variant_id
3884
+ )),
3885
+ /* @__PURE__ */ jsxRuntime.jsx(
3886
+ "input",
3887
+ {
3888
+ "aria-label": t("subscriptions.searchProducts"),
3889
+ placeholder: t("subscriptions.searchProducts"),
3890
+ value: query,
3891
+ onChange: (e) => {
3892
+ setQuery(e.target.value);
3893
+ setPage(0);
3894
+ setItems([]);
3895
+ },
3896
+ style: {
3897
+ font: "inherit",
3898
+ padding: ".65rem",
3899
+ border: "1px solid var(--nv-border)",
3900
+ borderRadius: "var(--nv-radius)",
3901
+ background: "var(--nv-bg)",
3902
+ color: "var(--nv-fg)"
3903
+ }
3904
+ }
3905
+ ),
3906
+ /* @__PURE__ */ jsxRuntime.jsx(
3907
+ "ul",
3908
+ {
3909
+ style: {
3910
+ listStyle: "none",
3911
+ padding: 0,
3912
+ margin: 0,
3913
+ maxHeight: "18rem",
3914
+ overflowY: "auto"
3915
+ },
3916
+ children: items.filter((p) => !value[p.variant_id]).map((p) => /* @__PURE__ */ jsxRuntime.jsxs(
3917
+ "li",
3918
+ {
3919
+ style: {
3920
+ display: "flex",
3921
+ alignItems: "center",
3922
+ justifyContent: "space-between",
3923
+ gap: "1rem",
3924
+ padding: ".5rem 0",
3925
+ borderBottom: "1px solid var(--nv-border)"
3926
+ },
3927
+ children: [
3928
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
3929
+ p.title,
3930
+ " ",
3931
+ p.variant_title,
3932
+ /* @__PURE__ */ jsxRuntime.jsx("small", { style: { display: "block", color: "var(--nv-muted)" }, children: formatCents(p.unit_gross, p.currency, loc) })
3933
+ ] }),
3934
+ /* @__PURE__ */ jsxRuntime.jsx(
3935
+ SubscriptionButton,
3936
+ {
3937
+ "aria-label": `${t("subscriptions.addProduct")}: ${p.title}`,
3938
+ onClick: () => onChange({ ...value, [p.variant_id]: 1 }),
3939
+ children: "+"
3940
+ }
3941
+ )
3942
+ ]
3943
+ },
3944
+ p.variant_id
3945
+ ))
3946
+ }
3947
+ ),
3948
+ busy ? /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: t("subscriptions.loadingProducts") }) : more ? /* @__PURE__ */ jsxRuntime.jsx(SubscriptionButton, { onClick: () => setPage((p) => p + 1), children: t("account.loadMore") }) : null,
3949
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: error })
3950
+ ] });
3951
+ }
3952
+ function SubscriptionManager({
3953
+ subscriptionId,
3954
+ locale
3955
+ }) {
3956
+ const state = useCustomerSubscription(subscriptionId), { customer } = useAccount(), t = useT();
3957
+ if (!customer) return null;
3958
+ if (state.error)
3959
+ return /* @__PURE__ */ jsxRuntime.jsxs("p", { role: "alert", children: [
3960
+ state.error.message,
3961
+ " ",
3962
+ /* @__PURE__ */ jsxRuntime.jsx(SubscriptionButton, { onClick: () => void state.refresh(), children: t("account.retry") })
3963
+ ] });
3964
+ if (!state.subscription)
3965
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: t("subscriptions.loading") });
3966
+ return /* @__PURE__ */ jsxRuntime.jsx(
3967
+ Manager,
3968
+ {
3969
+ state,
3970
+ locale
3971
+ },
3972
+ `${customer.id}:${subscriptionId}:${state.subscription.version}:${state.readRevision}`
3973
+ );
3974
+ }
3975
+ function Manager({
3976
+ state,
3977
+ locale
3978
+ }) {
3979
+ const { subscription: s, api, refresh } = state, t = useT(), loc = useFormatLocale(locale);
3980
+ const [mode, setMode] = React27__namespace.useState(null);
3981
+ const [option, setOption] = React27__namespace.useState("");
3982
+ const [quantities, setQuantities] = React27__namespace.useState(
3983
+ {}
3984
+ );
3985
+ const [quote, setQuote] = React27__namespace.useState(null);
3986
+ const [busy, setBusy] = React27__namespace.useState(false), [locked, setLocked] = React27__namespace.useState(false), [error, setError] = React27__namespace.useState("");
3987
+ const [consent, setConsent] = React27__namespace.useState(false);
3988
+ const upcoming = s?.upcoming, heading = React27__namespace.useRef(null);
3989
+ const loader = React27__namespace.useCallback(
3990
+ (q, offset, signal) => api.products(s.id, {
3991
+ q,
3992
+ offset,
3993
+ signal,
3994
+ scope: mode === "mix" ? "mix" : "one_time"
3995
+ }),
3996
+ [api, s?.id, mode]
3997
+ );
3998
+ React27__namespace.useEffect(() => {
3999
+ if (mode) heading.current?.focus();
4000
+ }, [mode]);
4001
+ if (!s || !upcoming) return null;
4002
+ if (s.status !== "active")
4003
+ return /* @__PURE__ */ jsxRuntime.jsx("section", { style: subscriptionPanel, children: /* @__PURE__ */ jsxRuntime.jsx("h2", { children: t(`subscriptions.status.${s.status}`) }) });
4004
+ const money = (n) => formatCents(n, s.currency, loc);
4005
+ const scope = mode === "mix" ? "recurring" : "one_time";
4006
+ const selected = Object.entries(quantities).filter(([, quantity]) => quantity > 0).map(([variant_id, quantity]) => ({ variant_id, quantity }));
4007
+ const changes = mode === "cadence" ? { cadence_option_id: option } : { lines: selected };
4008
+ const amount = quote ? "recurring_total_cents" in quote ? quote.recurring_total_cents : quote.after.total_cents : upcoming.total_cents;
4009
+ const enter = (next) => {
4010
+ setMode(next);
4011
+ setQuote(null);
4012
+ setConsent(false);
4013
+ setError("");
4014
+ setOption("");
4015
+ setQuantities(
4016
+ Object.fromEntries(
4017
+ upcoming.lines.filter(
4018
+ (li) => li.scope === (next === "mix" ? "recurring" : "one_time")
4019
+ ).map((li) => [li.variant_id, li.quantity])
4020
+ )
4021
+ );
4022
+ };
4023
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4024
+ "section",
4025
+ {
4026
+ style: { ...subscriptionPanel, display: "grid", gap: "1rem" },
4027
+ "aria-busy": busy,
4028
+ children: [
4029
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { children: [
4030
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: { margin: 0, fontWeight: 500 }, children: t("subscriptions.nextDelivery") }),
4031
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
4032
+ t("subscriptions.chargeDate"),
4033
+ ":",
4034
+ " ",
4035
+ /* @__PURE__ */ jsxRuntime.jsx("time", { dateTime: upcoming.due_at, children: new Date(upcoming.due_at).toLocaleString(loc) })
4036
+ ] })
4037
+ ] }),
4038
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: upcoming.lines.map((li) => /* @__PURE__ */ jsxRuntime.jsxs(
4039
+ "li",
4040
+ {
4041
+ style: {
4042
+ display: "flex",
4043
+ justifyContent: "space-between",
4044
+ gap: "1rem",
4045
+ padding: ".65rem 0",
4046
+ borderBottom: "1px solid var(--nv-border)"
4047
+ },
4048
+ children: [
4049
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
4050
+ li.title ?? li.sku,
4051
+ " \xD7 ",
4052
+ li.quantity,
4053
+ li.scope === "one_time" ? /* @__PURE__ */ jsxRuntime.jsx("small", { style: { display: "block", color: "var(--nv-muted)" }, children: t("subscriptions.onlyNext") }) : null
4054
+ ] }),
4055
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: money(li.unit_gross * li.quantity - (li.discount_cents ?? 0)) })
4056
+ ]
4057
+ },
4058
+ li.line_id
4059
+ )) }),
4060
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between" }, children: [
4061
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("totals.shipping") }),
4062
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: money(upcoming.shipping_cents) })
4063
+ ] }),
4064
+ /* @__PURE__ */ jsxRuntime.jsxs(
4065
+ "div",
4066
+ {
4067
+ style: {
4068
+ display: "flex",
4069
+ justifyContent: "space-between",
4070
+ fontWeight: 500
4071
+ },
4072
+ children: [
4073
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("subscriptions.toCharge") }),
4074
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: money(upcoming.total_cents) })
4075
+ ]
4076
+ }
4077
+ ),
4078
+ upcoming.capabilities.reminder?.enabled ? /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { color: "var(--nv-muted)", margin: 0 }, children: [
4079
+ t("subscriptions.noticeStatus"),
4080
+ ":",
4081
+ " ",
4082
+ t(`subscriptions.notice.${upcoming.reminder?.status ?? "pending"}`)
4083
+ ] }) : null,
4084
+ upcoming.charge_held_for_notice && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: t("subscriptions.noticeHeld") }),
4085
+ !upcoming.editable ? /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: t(
4086
+ `subscriptions.locked.${upcoming.locked_reason_code ?? "in_flight"}`
4087
+ ) }) : /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { color: "var(--nv-muted)", margin: 0 }, children: [
4088
+ t("subscriptions.editUntil"),
4089
+ ":",
4090
+ " ",
4091
+ new Date(upcoming.edit_cutoff_at).toLocaleString(loc)
4092
+ ] }),
4093
+ !mode ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: ".5rem", flexWrap: "wrap" }, children: [
4094
+ !!upcoming.capabilities.cadence_options?.length && /* @__PURE__ */ jsxRuntime.jsx(
4095
+ SubscriptionButton,
4096
+ {
4097
+ disabled: !upcoming.editable,
4098
+ onClick: () => enter("cadence"),
4099
+ children: t("subscriptions.changeInterval")
4100
+ }
4101
+ ),
4102
+ upcoming.capabilities.one_time_addons && /* @__PURE__ */ jsxRuntime.jsx(
4103
+ SubscriptionButton,
4104
+ {
4105
+ disabled: !upcoming.editable,
4106
+ onClick: () => enter("addons"),
4107
+ children: t("subscriptions.addNext")
4108
+ }
4109
+ ),
4110
+ upcoming.capabilities.mix && /* @__PURE__ */ jsxRuntime.jsx(
4111
+ SubscriptionButton,
4112
+ {
4113
+ disabled: !upcoming.editable,
4114
+ onClick: () => enter("mix"),
4115
+ children: t("subscriptions.changeMix")
4116
+ }
4117
+ ),
4118
+ s.allowed_actions.skip && /* @__PURE__ */ jsxRuntime.jsx(SubscriptionButton, { onClick: () => enter("skip"), children: t("subscriptions.skip") })
4119
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
4120
+ "form",
4121
+ {
4122
+ noValidate: true,
4123
+ onSubmit: async (event) => {
4124
+ event.preventDefault();
4125
+ if (busy || locked) return;
4126
+ setBusy(true);
4127
+ setError("");
4128
+ try {
4129
+ if (mode === "skip") {
4130
+ if (!consent) return;
4131
+ try {
4132
+ await api.skipNext(s.id, s.version);
4133
+ await refresh();
4134
+ } catch (err) {
4135
+ setLocked(true);
4136
+ throw err;
4137
+ }
4138
+ } else if (!quote) {
4139
+ const q = mode === "addons" ? await api.previewNextItems(s.id, { addons: selected }) : await api.previewChanges(s.id, changes);
4140
+ setQuote(q);
4141
+ setConsent(false);
4142
+ } else {
4143
+ if (!consent) return;
4144
+ try {
4145
+ if (mode === "addons")
4146
+ await api.commitNextItems(
4147
+ s.id,
4148
+ { addons: selected },
4149
+ quote,
4150
+ "Customer explicitly accepted the displayed next-delivery contents and total"
4151
+ );
4152
+ else
4153
+ await api.commitChanges(
4154
+ s.id,
4155
+ changes,
4156
+ quote,
4157
+ "Customer explicitly accepted the displayed future terms, dates and price"
4158
+ );
4159
+ await refresh();
4160
+ } catch (err) {
4161
+ setLocked(true);
4162
+ throw err;
4163
+ }
4164
+ }
4165
+ } catch (err) {
4166
+ setError(err instanceof Error ? err.message : String(err));
4167
+ } finally {
4168
+ setBusy(false);
4169
+ }
4170
+ },
4171
+ style: {
4172
+ borderTop: "1px solid var(--nv-border)",
4173
+ paddingTop: "1rem",
4174
+ display: "grid",
4175
+ gap: "1rem"
4176
+ },
4177
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
4178
+ "fieldset",
4179
+ {
4180
+ disabled: busy,
4181
+ style: {
4182
+ border: 0,
4183
+ margin: 0,
4184
+ padding: 0,
4185
+ display: "grid",
4186
+ gap: "1rem",
4187
+ minWidth: 0
4188
+ },
4189
+ children: [
4190
+ /* @__PURE__ */ jsxRuntime.jsx(
4191
+ "h3",
4192
+ {
4193
+ ref: heading,
4194
+ tabIndex: -1,
4195
+ style: { margin: 0, fontWeight: 500 },
4196
+ children: t(
4197
+ mode === "cadence" ? "subscriptions.changeInterval" : mode === "mix" ? "subscriptions.changeMix" : mode === "skip" ? "subscriptions.skip" : "subscriptions.addNext"
4198
+ )
4199
+ }
4200
+ ),
4201
+ mode === "skip" ? /* @__PURE__ */ jsxRuntime.jsx("p", { children: t("subscriptions.skipWarning") }) : quote ? /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "status", children: [
4202
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: t(
4203
+ mode === "addons" ? "subscriptions.onlyNext" : "subscriptions.futureOnly"
4204
+ ) }),
4205
+ mode === "cadence" ? /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
4206
+ t("subscriptions.nextUnchanged"),
4207
+ ":",
4208
+ " ",
4209
+ new Date(
4210
+ String(quote.after.next_at)
4211
+ ).toLocaleString(loc)
4212
+ ] }) : null,
4213
+ "upcoming_dates" in quote && quote.upcoming_dates?.map((date) => /* @__PURE__ */ jsxRuntime.jsx("p", { children: new Date(date).toLocaleString(loc) }, date)),
4214
+ mode === "addons" && "addons" in quote.after && quote.after.addons.map(
4215
+ (li) => /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
4216
+ li.title ?? li.sku ?? li.variant_id,
4217
+ " \xD7 ",
4218
+ li.quantity
4219
+ ] }, li.variant_id)
4220
+ ),
4221
+ "display_items" in quote && quote.display_items?.map((li) => /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
4222
+ li.title,
4223
+ " \xD7 ",
4224
+ li.quantity
4225
+ ] }, li.variant_id)),
4226
+ /* @__PURE__ */ jsxRuntime.jsxs("strong", { children: [
4227
+ t("subscriptions.reviewTotal"),
4228
+ ": ",
4229
+ money(amount)
4230
+ ] })
4231
+ ] }) : mode === "cadence" ? /* @__PURE__ */ jsxRuntime.jsx(
4232
+ "div",
4233
+ {
4234
+ role: "group",
4235
+ "aria-label": t("subscriptions.changeInterval"),
4236
+ style: { display: "flex", gap: ".5rem", flexWrap: "wrap" },
4237
+ children: upcoming.capabilities.cadence_options?.map((o) => /* @__PURE__ */ jsxRuntime.jsx(
4238
+ SubscriptionButton,
4239
+ {
4240
+ "aria-pressed": option === o.id,
4241
+ onClick: () => setOption(o.id),
4242
+ children: t(`subscriptions.unit.${o.unit}`, { count: o.count })
4243
+ },
4244
+ o.id
4245
+ ))
4246
+ }
4247
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
4248
+ ProductSelection,
4249
+ {
4250
+ maxQuantity: mode === "mix" ? upcoming.capabilities.mix?.per_sku_max ?? 99 : 99,
4251
+ load: loader,
4252
+ value: quantities,
4253
+ onChange: setQuantities,
4254
+ initialItems: upcoming.lines.filter((li) => li.scope === scope).map((li) => ({
4255
+ ...li,
4256
+ title: li.title ?? li.sku ?? li.variant_id,
4257
+ currency: s.currency,
4258
+ variant_title: li.variant_title ?? null,
4259
+ sku: li.sku ?? null
4260
+ })),
4261
+ locale: loc
4262
+ }
4263
+ ),
4264
+ (quote || mode === "skip") && /* @__PURE__ */ jsxRuntime.jsxs(
4265
+ "label",
4266
+ {
4267
+ style: {
4268
+ display: "flex",
4269
+ gap: ".5rem",
4270
+ alignItems: "flex-start"
4271
+ },
4272
+ children: [
4273
+ /* @__PURE__ */ jsxRuntime.jsx(
4274
+ "input",
4275
+ {
4276
+ type: "checkbox",
4277
+ checked: consent,
4278
+ onChange: (e) => setConsent(e.target.checked)
4279
+ }
4280
+ ),
4281
+ t("subscriptions.acceptChange")
4282
+ ]
4283
+ }
4284
+ ),
4285
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: error }),
4286
+ locked ? /* @__PURE__ */ jsxRuntime.jsx(SubscriptionButton, { onClick: () => void refresh(), children: t("subscriptions.reloadBeforeRetry") }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: ".5rem", flexWrap: "wrap" }, children: [
4287
+ /* @__PURE__ */ jsxRuntime.jsx(
4288
+ SubscriptionButton,
4289
+ {
4290
+ disabled: busy,
4291
+ onClick: () => quote ? (setQuote(null), setConsent(false)) : enter(null),
4292
+ children: t(quote ? "subscriptions.back" : "account.cancel")
4293
+ }
4294
+ ),
4295
+ /* @__PURE__ */ jsxRuntime.jsx(
4296
+ SubscriptionButton,
4297
+ {
4298
+ type: "submit",
4299
+ primary: true,
4300
+ disabled: busy || (quote || mode === "skip" ? !consent : mode === "cadence" ? !option : mode === "mix" ? !selected.length : false),
4301
+ children: busy ? t("subscriptions.loading") : t(
4302
+ quote || mode === "skip" ? "subscriptions.confirmChange" : "subscriptions.reviewChange"
4303
+ )
4304
+ }
4305
+ )
4306
+ ] })
4307
+ ]
4308
+ }
4309
+ )
4310
+ }
4311
+ )
4312
+ ]
4313
+ }
4314
+ );
4315
+ }
3672
4316
  function serializeJsonLd(data) {
3673
4317
  return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
3674
4318
  }
@@ -3772,6 +4416,118 @@ function OrganizationJsonLd({
3772
4416
  });
3773
4417
  return /* @__PURE__ */ jsxRuntime.jsx(JsonLd, { data });
3774
4418
  }
4419
+ function Mix({
4420
+ plan,
4421
+ marketId,
4422
+ locale,
4423
+ onAdded
4424
+ }) {
4425
+ const client = useStorefront(), cart = useCart(), t = useT(), [value, setValue] = React27__namespace.useState({}), [busy, setBusy] = React27__namespace.useState(false), [error, setError] = React27__namespace.useState(""), [locked, setLocked] = React27__namespace.useState(false);
4426
+ const load = React27__namespace.useCallback(
4427
+ (q, offset, signal) => client.subscriptions.mixProducts(plan.id, {
4428
+ q,
4429
+ offset,
4430
+ marketId,
4431
+ signal
4432
+ }),
4433
+ [client, plan.id, marketId]
4434
+ );
4435
+ const loc = useFormatLocale(locale);
4436
+ const mix = plan.capabilities?.mix;
4437
+ if (!mix) return null;
4438
+ 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 }));
4439
+ const valid = units >= mix.min_units && units <= mix.max_units && lines.every(
4440
+ (li) => Number.isInteger(li.quantity) && li.quantity <= (mix.per_sku_max ?? 99)
4441
+ ) && (mix.groups ?? []).every((g) => {
4442
+ const n = lines.filter((li) => g.variant_ids.includes(li.variant_id)).reduce((a, li) => a + li.quantity, 0);
4443
+ return n >= g.min_units && n <= g.max_units;
4444
+ });
4445
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { style: { ...subscriptionPanel, display: "grid", gap: "1rem" }, children: [
4446
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: { fontWeight: 500, margin: 0 }, children: plan.name }),
4447
+ plan.price && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
4448
+ formatCents(plan.price.amount_cents, plan.price.currency, loc),
4449
+ " \xB7",
4450
+ " ",
4451
+ t("subscriptions.mixPrice")
4452
+ ] }),
4453
+ /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: t("subscriptions.mixCount", {
4454
+ count: units,
4455
+ min: mix.min_units,
4456
+ max: mix.max_units
4457
+ }) }),
4458
+ mix.groups?.map((g) => /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
4459
+ g.name,
4460
+ ": ",
4461
+ g.min_units,
4462
+ "\u2013",
4463
+ g.max_units
4464
+ ] }, g.name)),
4465
+ /* @__PURE__ */ jsxRuntime.jsx(
4466
+ "fieldset",
4467
+ {
4468
+ disabled: busy || locked,
4469
+ style: { border: 0, padding: 0, margin: 0, minWidth: 0 },
4470
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4471
+ ProductSelection,
4472
+ {
4473
+ maxQuantity: mix.per_sku_max ?? 99,
4474
+ load,
4475
+ value,
4476
+ onChange: (v) => {
4477
+ setValue(v);
4478
+ setError("");
4479
+ },
4480
+ locale
4481
+ }
4482
+ )
4483
+ }
4484
+ ),
4485
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: error }),
4486
+ locked ? /* @__PURE__ */ jsxRuntime.jsx(
4487
+ SubscriptionButton,
4488
+ {
4489
+ onClick: async () => {
4490
+ await cart.refresh();
4491
+ setLocked(false);
4492
+ },
4493
+ children: t("subscriptions.reloadBeforeRetry")
4494
+ }
4495
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
4496
+ SubscriptionButton,
4497
+ {
4498
+ primary: true,
4499
+ disabled: !valid || busy || cart.loading,
4500
+ onClick: async () => {
4501
+ setBusy(true);
4502
+ setError("");
4503
+ try {
4504
+ await cart.setSubscriptionMix(
4505
+ plan.id,
4506
+ lines,
4507
+ cart.cart?.line_items.filter((li) => li.subscription_plan_id).map((li) => li.id) ?? []
4508
+ );
4509
+ onAdded?.();
4510
+ } catch (err) {
4511
+ setLocked(true);
4512
+ setError(err instanceof Error ? err.message : String(err));
4513
+ } finally {
4514
+ setBusy(false);
4515
+ }
4516
+ },
4517
+ children: t("subscriptions.addMix")
4518
+ }
4519
+ )
4520
+ ] });
4521
+ }
4522
+ function SubscriptionMix(props) {
4523
+ return /* @__PURE__ */ jsxRuntime.jsx(
4524
+ Mix,
4525
+ {
4526
+ ...props
4527
+ },
4528
+ `${props.plan.id}:${props.plan.version}:${props.marketId ?? ""}`
4529
+ );
4530
+ }
3775
4531
 
3776
4532
  exports.AccountDashboard = AccountDashboard;
3777
4533
  exports.AccountProvider = AccountProvider;
@@ -3818,6 +4574,8 @@ exports.SortSelect = SortSelect;
3818
4574
  exports.StatusBanner = StatusBanner;
3819
4575
  exports.StockBadge = StockBadge;
3820
4576
  exports.SubscriptionList = SubscriptionList;
4577
+ exports.SubscriptionManager = SubscriptionManager;
4578
+ exports.SubscriptionMix = SubscriptionMix;
3821
4579
  exports.SubscriptionTerms = SubscriptionTerms;
3822
4580
  exports.TagFilter = TagFilter;
3823
4581
  exports.ThemeProvider = ThemeProvider;