vonpay-medusa 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.medusa/server/src/admin/index.js +566 -89
  2. package/.medusa/server/src/admin/index.mjs +568 -91
  3. package/.medusa/server/src/api/admin/vonpay/offers/[id]/route.js +20 -0
  4. package/.medusa/server/src/api/admin/vonpay/offers/import/route.js +12 -0
  5. package/.medusa/server/src/api/admin/vonpay/offers/reorder/route.js +11 -0
  6. package/.medusa/server/src/api/admin/vonpay/offers/route.js +18 -0
  7. package/.medusa/server/src/api/middlewares.js +4 -0
  8. package/.medusa/server/src/lib/errors.js +16 -1
  9. package/.medusa/server/src/lib/money.js +8 -0
  10. package/.medusa/server/src/modules/vonpay-upsells/index.js +11 -0
  11. package/.medusa/server/src/modules/vonpay-upsells/key.js +4 -0
  12. package/.medusa/server/src/modules/vonpay-upsells/migrations/Migration20261023131258.js +20 -0
  13. package/.medusa/server/src/modules/vonpay-upsells/migrations/Migration20261023142238.js +15 -0
  14. package/.medusa/server/src/modules/vonpay-upsells/models/event.js +25 -0
  15. package/.medusa/server/src/modules/vonpay-upsells/models/offer.js +23 -0
  16. package/.medusa/server/src/modules/vonpay-upsells/service.js +8 -0
  17. package/.medusa/server/src/providers/vonpay/options.js +11 -7
  18. package/.medusa/server/src/providers/vonpay/service.js +29 -2
  19. package/.medusa/server/src/providers/vonpay/session-data.js +11 -2
  20. package/.medusa/server/src/providers/vonpay/setup-check.js +69 -0
  21. package/.medusa/server/src/providers/vonpay/ship-hold.js +12 -6
  22. package/.medusa/server/src/providers/vonpay/upsell-accept.js +151 -24
  23. package/.medusa/server/src/providers/vonpay/upsell-copy.js +48 -0
  24. package/.medusa/server/src/providers/vonpay/upsell-offers-admin.js +275 -0
  25. package/.medusa/server/src/providers/vonpay/upsell-offers.js +136 -0
  26. package/.medusa/server/src/providers/vonpay/upsell-routes.js +5 -0
  27. package/.medusa/server/src/providers/vonpay/upsell-rules.js +106 -0
  28. package/README.md +78 -7
  29. package/bin/vonpay-medusa.cjs +15 -4
  30. package/package.json +2 -1
  31. package/storefront/src/context.tsx +2 -1
  32. package/storefront/src/csp.ts +1 -1
  33. package/storefront/src/index.ts +1 -1
  34. package/storefront/src/offer-pass.ts +22 -0
  35. package/storefront/src/offer-prepare.ts +21 -0
  36. package/storefront/src/server.ts +29 -1
  37. package/storefront/src/upsell.ts +45 -1
  38. package/storefront/src/vonpay-card-payment.tsx +12 -6
  39. package/storefront/src/vonpay-upsell-offer.tsx +60 -38
  40. package/storefront/src/vonpay-upsell-return.tsx +30 -28
  41. package/storefront/src/vora-csp.json +85 -5
@@ -558,11 +558,11 @@ const HeldPaymentsBanner = () => {
558
558
  ] }) });
559
559
  };
560
560
  adminSdk.defineWidgetConfig({ zone: "order.list.before" });
561
- const KEY = "vonpay-ship-hold";
561
+ const KEY$1 = "vonpay-ship-hold";
562
562
  const SHIP_HOLD_REFRESH_MS = 3e4;
563
563
  function useShipHold(orderId) {
564
564
  return reactQuery.useQuery({
565
- queryKey: [KEY, orderId],
565
+ queryKey: [KEY$1, orderId],
566
566
  queryFn: () => sdk.client.fetch(`/admin/vonpay/orders/${encodeURIComponent(orderId)}/offer-hold`),
567
567
  enabled: !!orderId,
568
568
  retry: false,
@@ -577,7 +577,7 @@ function useShipOverride(orderId) {
577
577
  return reactQuery.useMutation({
578
578
  mutationFn: () => sdk.client.fetch(`/admin/vonpay/orders/${encodeURIComponent(orderId)}/ship-override`, { method: "POST", body: {} }),
579
579
  retry: false,
580
- onSettled: () => qc.invalidateQueries({ queryKey: [KEY, orderId] })
580
+ onSettled: () => qc.invalidateQueries({ queryKey: [KEY$1, orderId] })
581
581
  });
582
582
  }
583
583
  function clockTime(iso) {
@@ -626,6 +626,548 @@ const UpdateBanner = () => {
626
626
  ] }) });
627
627
  };
628
628
  adminSdk.defineWidgetConfig({ zone: "order.list.before" });
629
+ const HEADLINE_MAX = 80;
630
+ const PITCH_MAX = 300;
631
+ const OFFERS_COPY = {
632
+ title: "One-click offers",
633
+ intro: "After paying, shoppers see these offers one at a time (up to three per order), in this order. Only switched-on offers are shown.",
634
+ off: "One-click offers are off. Turn them on with the upsells option in medusa-config; the offers below are kept.",
635
+ fromConfig: "Your offers come from medusa-config right now (upsells.variantIds). Import them to manage them here. Once an offer exists here, this list is used instead.",
636
+ configEmpty: "No offers yet. Add the first one below.",
637
+ adminEmpty: "No offers are shown to shoppers. Add one below.",
638
+ importButton: "Import from medusa-config",
639
+ add: "Add offer",
640
+ name: "Name (only you see it)",
641
+ variant: "Product variant id (variant_…)",
642
+ shown: "Shown",
643
+ up: "Move up",
644
+ down: "Move down",
645
+ remove: "Remove",
646
+ removeConfirm: "Remove this offer? A shopper already paying for it is not affected.",
647
+ missingVariant: "This product variant no longer exists — it is not offered.",
648
+ loadFailed: "The offers could not be loaded. Reload the page to try again.",
649
+ lag: "A change shows on your store at once; with more than one server it can take up to 30 seconds everywhere.",
650
+ rules: "Who sees it",
651
+ downsell: "If they say No thanks",
652
+ downsellNone: "Nothing",
653
+ downsellIntro: 'Show one other product after a shopper says No thanks to this offer — usually a smaller or cheaper option. It counts toward the three offers per order, and the same "Who sees it" applies.',
654
+ downsellVariant: "Downsell product variant id (variant_…)",
655
+ downsellRemove: "Remove downsell",
656
+ downsellMissing: "This product variant no longer exists — it is not offered.",
657
+ wording: "Wording",
658
+ wordingDefault: "Standard",
659
+ wordingPitchOnly: "Pitch only",
660
+ wordingUnreadable: "The saved wording can't be read, so shoppers see the standard wording. Save it again.",
661
+ wordingIntro: "What shoppers read on this offer. Leave a field empty to use the standard wording. The payment line, the buttons and every result message stay the same.",
662
+ headline: "Headline",
663
+ headlineHint: 'Replaces the heading. Standard: "Add this to your order?"',
664
+ pitch: "Pitch",
665
+ pitchHint: "Replaces the product's description under its name. A phone shows about two lines.",
666
+ wordingDownsellNote: `A downsell keeps the standard wording: "How about this instead?" and its product's description.`,
667
+ wordingReset: "Use the standard wording",
668
+ rulesEveryone: "Everyone",
669
+ rulesUnreadable: "Its rules can't be read, so it is not shown. Set them again.",
670
+ rulesIntro: "Show this offer only when ALL the conditions you set match. Leave a condition empty to ignore it.",
671
+ customers: "Customers",
672
+ customersAll: "Everyone",
673
+ customersGuests: "Guests only",
674
+ customersSignedIn: "Signed-in customers only",
675
+ products: "The shopper bought one of these products at checkout",
676
+ collections: "The shopper bought a product from one of these collections at checkout",
677
+ groups: "The signed-in customer is in one of these groups",
678
+ paid: "The amount paid at checkout (tax and shipping included)",
679
+ paidNote: "An order in a currency not listed here does not see this offer.",
680
+ paidAtLeast: "At least",
681
+ paidAtMost: "At most",
682
+ search: "Search by name",
683
+ addCurrency: "Add currency",
684
+ removeCurrency: "Remove currency",
685
+ rulesUnreadableNotice: "The saved rules for this offer can't be read, so the offer is not shown. Saving replaces them with what you set here.",
686
+ replaceRules: "Replace the rules",
687
+ save: "Save",
688
+ cancel: "Cancel",
689
+ saveFailed: "That change was not saved."
690
+ };
691
+ function errorSentence(err, fallback = OFFERS_COPY.saveFailed) {
692
+ const m = err?.message;
693
+ if (typeof m !== "string") return fallback;
694
+ const i = m.indexOf(": ");
695
+ return m.startsWith("vonpay_") && i > 0 ? m.slice(i + 2) : fallback;
696
+ }
697
+ function productLabel(o) {
698
+ if (o.product_title === null) return o.variant_id;
699
+ return [o.product_title, o.variant_title].filter((x) => x && x !== "Default variant").join(" — ");
700
+ }
701
+ function wordingSummary(copy) {
702
+ if (copy === "unreadable") return OFFERS_COPY.wordingUnreadable;
703
+ if (!copy) return OFFERS_COPY.wordingDefault;
704
+ return copy.headline ?? OFFERS_COPY.wordingPitchOnly;
705
+ }
706
+ function rulesSummary(rules) {
707
+ if (rules === "unreadable") return OFFERS_COPY.rulesUnreadable;
708
+ if (!rules) return OFFERS_COPY.rulesEveryone;
709
+ const parts = [];
710
+ if (rules.customers === "guests") parts.push("Guests");
711
+ if (rules.customers === "signed_in") parts.push("Signed-in");
712
+ if (rules.groups) parts.push(`${rules.groups.length} group${rules.groups.length === 1 ? "" : "s"}`);
713
+ if (rules.products) parts.push(`${rules.products.length} product${rules.products.length === 1 ? "" : "s"}`);
714
+ if (rules.collections) parts.push(`${rules.collections.length} collection${rules.collections.length === 1 ? "" : "s"}`);
715
+ for (const [c, n] of Object.entries(rules.min_paid ?? {})) parts.push(`paid ≥ ${n} ${c.toUpperCase()}`);
716
+ for (const [c, n] of Object.entries(rules.max_paid ?? {})) parts.push(`paid ≤ ${n} ${c.toUpperCase()}`);
717
+ return parts.length ? parts.join(" · ") : OFFERS_COPY.rulesEveryone;
718
+ }
719
+ function OfferDownsellDrawer({ offer, open, onOpenChange, onSave, busy }) {
720
+ const [variant, setVariant] = react.useState(offer.downsell_variant_id ?? "");
721
+ const [problem, setProblem] = react.useState(null);
722
+ const save = async (value) => {
723
+ if (value !== null && !/^variant_[A-Za-z0-9]{1,64}$/.test(value)) return setProblem("Enter a product variant id, like variant_01H….");
724
+ setProblem(null);
725
+ const refused = await onSave(value);
726
+ if (refused === null) onOpenChange(false);
727
+ else setProblem(refused);
728
+ };
729
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
730
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
731
+ OFFERS_COPY.downsell,
732
+ ": ",
733
+ offer.name
734
+ ] }) }),
735
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
736
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.downsellIntro }),
737
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
738
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-downsell-variant", children: OFFERS_COPY.downsellVariant }),
739
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-downsell-variant", value: variant, maxLength: 72, onChange: (e) => setVariant(e.target.value) })
740
+ ] }),
741
+ problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-downsell-problem", children: problem })
742
+ ] }),
743
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
744
+ offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "danger", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.downsellRemove }),
745
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
746
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(variant.trim()), children: OFFERS_COPY.save })
747
+ ] })
748
+ ] }) });
749
+ }
750
+ const SOURCES = {
751
+ products: { path: "/admin/products", key: "products", label: "title" },
752
+ collections: { path: "/admin/collections", key: "collections", label: "title" },
753
+ groups: { path: "/admin/customer-groups", key: "customer_groups", label: "name" }
754
+ };
755
+ async function fetchNamed(kind, query) {
756
+ const s = SOURCES[kind];
757
+ const body = await sdk.client.fetch(s.path, { query: { ...query, fields: `id,${s.label}` } });
758
+ const plain = (t) => t.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, "").trim();
759
+ return (body[s.key] ?? []).flatMap((r) => typeof r.id === "string" ? [{ id: r.id, label: typeof r[s.label] === "string" && plain(r[s.label]) || r.id }] : []);
760
+ }
761
+ function Picker({ kind, label, ids, onChange }) {
762
+ const [q, setQ] = react.useState("");
763
+ const names = reactQuery.useQuery({ queryKey: ["vonpay-offer-rule-names", kind, ids], queryFn: () => fetchNamed(kind, { id: ids, limit: ids.length }), enabled: ids.length > 0 });
764
+ const found = reactQuery.useQuery({ queryKey: ["vonpay-offer-rule-search", kind, q], queryFn: () => fetchNamed(kind, { q, limit: 8 }), enabled: q.trim().length >= 2, placeholderData: reactQuery.keepPreviousData });
765
+ const nameOf = (id) => names.data?.find((n) => n.id === id)?.label ?? id;
766
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": `vonpay-rule-${kind}`, children: [
767
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: label }),
768
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: ids.map((id) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "bg-ui-bg-subtle flex items-center gap-x-1 rounded-md px-2 py-1", children: [
769
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: nameOf(id) }),
770
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `Remove ${nameOf(id)}`, onClick: () => onChange(ids.filter((x) => x !== id)), children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) })
771
+ ] }, id)) }),
772
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { placeholder: OFFERS_COPY.search, value: q, maxLength: 100, "aria-label": `${label}: ${OFFERS_COPY.search}`, onChange: (e) => setQ(e.target.value) }),
773
+ (q.trim().length >= 2 ? found.data ?? [] : []).filter((n) => !ids.includes(n.id)).map((n) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { size: "small", variant: "transparent", type: "button", onClick: () => onChange([...ids, n.id]), children: [
774
+ "+ ",
775
+ n.label
776
+ ] }, n.id))
777
+ ] });
778
+ }
779
+ function rowsOf(rules) {
780
+ const cs = [.../* @__PURE__ */ new Set([...Object.keys(rules?.min_paid ?? {}), ...Object.keys(rules?.max_paid ?? {})])];
781
+ return cs.map((c) => ({ currency: c, min: rules?.min_paid?.[c] ?? "", max: rules?.max_paid?.[c] ?? "" }));
782
+ }
783
+ function amountText(text) {
784
+ const t = text.trim().replace(",", ".");
785
+ if (t === "") return void 0;
786
+ return /^\d{1,10}(\.\d{1,6})?$/.test(t) ? t : null;
787
+ }
788
+ function rulesFromForm(f) {
789
+ const out = {};
790
+ if (f.customers !== "all") out.customers = f.customers;
791
+ if (f.products.length) out.products = f.products;
792
+ if (f.collections.length) out.collections = f.collections;
793
+ if (f.groups.length) out.groups = f.groups;
794
+ for (const r of f.paid) {
795
+ const c = r.currency.trim().toLowerCase();
796
+ if (!c && !r.min.trim() && !r.max.trim()) continue;
797
+ if (!/^[a-z]{3}$/.test(c)) return "Use a 3-letter currency code, like EUR.";
798
+ const min = amountText(r.min);
799
+ const max = amountText(r.max);
800
+ if (min === null || max === null) return `Enter a plain amount for ${c.toUpperCase()}, like 49.95.`;
801
+ if (min === void 0 && max === void 0) return `Enter the least or the most for ${c.toUpperCase()}, or remove its row.`;
802
+ if (out.min_paid?.[c] !== void 0 || out.max_paid?.[c] !== void 0) return `${c.toUpperCase()} is listed twice.`;
803
+ if (min !== void 0) out.min_paid = { ...out.min_paid, [c]: min };
804
+ if (max !== void 0) out.max_paid = { ...out.max_paid, [c]: max };
805
+ }
806
+ if (out.customers === "guests" && out.groups) return "Groups apply to signed-in customers only.";
807
+ return Object.keys(out).length ? out : null;
808
+ }
809
+ function OfferRulesDrawer({ offer, open, onOpenChange, onSave, busy }) {
810
+ const unreadable = offer.rules === "unreadable";
811
+ const start = offer.rules === "unreadable" ? null : offer.rules;
812
+ const [customers, setCustomers] = react.useState(start?.customers ?? "all");
813
+ const [products, setProducts] = react.useState(start?.products ?? []);
814
+ const [collections, setCollections] = react.useState(start?.collections ?? []);
815
+ const [groups, setGroups] = react.useState(start?.groups ?? []);
816
+ const [paid, setPaid] = react.useState(rowsOf(start));
817
+ const [problem, setProblem] = react.useState(null);
818
+ const save = async () => {
819
+ const r = rulesFromForm({ customers, products, collections, groups, paid });
820
+ if (typeof r === "string") return setProblem(r);
821
+ setProblem(null);
822
+ const refused = await onSave(r);
823
+ if (refused === null) onOpenChange(false);
824
+ else setProblem(refused);
825
+ };
826
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
827
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
828
+ OFFERS_COPY.rules,
829
+ ": ",
830
+ offer.name
831
+ ] }) }),
832
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
833
+ unreadable && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-unreadable", children: OFFERS_COPY.rulesUnreadableNotice }),
834
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.rulesIntro }),
835
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
836
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.customers }),
837
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: customers, onValueChange: (v) => setCustomers(v), children: [
838
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": OFFERS_COPY.customers, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
839
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
840
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "all", children: OFFERS_COPY.customersAll }),
841
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "guests", children: OFFERS_COPY.customersGuests }),
842
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "signed_in", children: OFFERS_COPY.customersSignedIn })
843
+ ] })
844
+ ] })
845
+ ] }),
846
+ /* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "products", label: OFFERS_COPY.products, ids: products, onChange: setProducts }),
847
+ /* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "collections", label: OFFERS_COPY.collections, ids: collections, onChange: setCollections }),
848
+ /* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "groups", label: OFFERS_COPY.groups, ids: groups, onChange: setGroups }),
849
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": "vonpay-rule-paid", children: [
850
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.paid }),
851
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.paidNote }),
852
+ paid.map((r, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr_auto] items-center gap-2", children: [
853
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { "aria-label": "Currency", placeholder: "EUR", value: r.currency, maxLength: 3, onChange: (e) => setPaid(paid.map((x, j) => j === i ? { ...x, currency: e.target.value } : x)) }),
854
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { "aria-label": `${OFFERS_COPY.paidAtLeast} (${r.currency || "currency"})`, placeholder: OFFERS_COPY.paidAtLeast, value: r.min, maxLength: 17, inputMode: "decimal", onChange: (e) => setPaid(paid.map((x, j) => j === i ? { ...x, min: e.target.value } : x)) }),
855
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { "aria-label": `${OFFERS_COPY.paidAtMost} (${r.currency || "currency"})`, placeholder: OFFERS_COPY.paidAtMost, value: r.max, maxLength: 17, inputMode: "decimal", onChange: (e) => setPaid(paid.map((x, j) => j === i ? { ...x, max: e.target.value } : x)) }),
856
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `${OFFERS_COPY.removeCurrency} ${r.currency.toUpperCase()}`.trim(), onClick: () => setPaid(paid.filter((_, j) => j !== i)), children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) })
857
+ ] }, i)),
858
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", type: "button", onClick: () => setPaid([...paid, { currency: "", min: "", max: "" }]), children: OFFERS_COPY.addCurrency }) })
859
+ ] }),
860
+ problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-problem", children: problem })
861
+ ] }),
862
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
863
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
864
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(), children: unreadable ? OFFERS_COPY.replaceRules : OFFERS_COPY.save })
865
+ ] })
866
+ ] }) });
867
+ }
868
+ function OfferWordingDrawer({ offer, open, onOpenChange, onSave, busy }) {
869
+ const start = offer.copy === "unreadable" ? null : offer.copy;
870
+ const [headline, setHeadline] = react.useState(start?.headline ?? "");
871
+ const [pitch, setPitch] = react.useState(start?.pitch ?? "");
872
+ const [problem, setProblem] = react.useState(null);
873
+ const save = async (copy) => {
874
+ setProblem(null);
875
+ const refused = await onSave(copy);
876
+ if (refused === null) onOpenChange(false);
877
+ else setProblem(refused);
878
+ };
879
+ const typed = () => {
880
+ const h = headline.trim();
881
+ const p = pitch.trim();
882
+ return h || p ? { ...h ? { headline: h } : {}, ...p ? { pitch: p } : {} } : null;
883
+ };
884
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
885
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
886
+ OFFERS_COPY.wording,
887
+ ": ",
888
+ offer.name
889
+ ] }) }),
890
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
891
+ offer.copy === "unreadable" && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-unreadable", children: OFFERS_COPY.wordingUnreadable }),
892
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingIntro }),
893
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
894
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-headline", children: OFFERS_COPY.headline }),
895
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-wording-headline", value: headline, maxLength: HEADLINE_MAX, onChange: (e) => setHeadline(e.target.value) }),
896
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.headlineHint })
897
+ ] }),
898
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
899
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-pitch", children: OFFERS_COPY.pitch }),
900
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Textarea, { id: "vonpay-wording-pitch", value: pitch, maxLength: PITCH_MAX, rows: 4, onChange: (e) => setPitch(e.target.value) }),
901
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
902
+ OFFERS_COPY.pitchHint,
903
+ " (",
904
+ pitch.length,
905
+ "/",
906
+ PITCH_MAX,
907
+ ")"
908
+ ] })
909
+ ] }),
910
+ offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingDownsellNote }),
911
+ problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-problem", children: problem })
912
+ ] }),
913
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
914
+ offer.copy !== null && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.wordingReset }),
915
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
916
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(typed()), children: OFFERS_COPY.save })
917
+ ] })
918
+ ] }) });
919
+ }
920
+ const KEY = "vonpay-offers";
921
+ function useOffers() {
922
+ return reactQuery.useQuery({ queryKey: [KEY], queryFn: () => sdk.client.fetch("/admin/vonpay/offers") });
923
+ }
924
+ function useWrite(send) {
925
+ const qc = reactQuery.useQueryClient();
926
+ return reactQuery.useMutation({ mutationFn: send, retry: false, onSettled: () => qc.invalidateQueries({ queryKey: [KEY] }) });
927
+ }
928
+ const useCreateOffer = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers", { method: "POST", body: v }));
929
+ const useUpdateOffer = () => useWrite((v) => {
930
+ const { id, ...body } = v;
931
+ return sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(id)}`, { method: "POST", body });
932
+ });
933
+ const useDeleteOffer = () => useWrite((v) => sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(v.id)}`, { method: "DELETE" }));
934
+ const useReorderOffers = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers/reorder", { method: "POST", body: v }));
935
+ const useImportOffers = () => useWrite(() => sdk.client.fetch("/admin/vonpay/offers/import", { method: "POST", body: {} }));
936
+ const VonpayOffersPage = () => {
937
+ const { data, isLoading, isError, error: loadError } = useOffers();
938
+ const create = useCreateOffer();
939
+ const update = useUpdateOffer();
940
+ const remove = useDeleteOffer();
941
+ const reorder = useReorderOffers();
942
+ const importConfig = useImportOffers();
943
+ const prompt = ui.usePrompt();
944
+ const [name, setName] = react.useState("");
945
+ const [variantId, setVariantId] = react.useState("");
946
+ const [error, setError] = react.useState(null);
947
+ const [editing, setEditing] = react.useState(null);
948
+ const [downsellFor, setDownsellFor] = react.useState(null);
949
+ const [wordingFor, setWordingFor] = react.useState(null);
950
+ const busy = create.isPending || update.isPending || remove.isPending || reorder.isPending || importConfig.isPending;
951
+ const run = async (work) => {
952
+ setError(null);
953
+ try {
954
+ await work();
955
+ return true;
956
+ } catch (err) {
957
+ setError(errorSentence(err));
958
+ return false;
959
+ }
960
+ };
961
+ const onAdd = async (e) => {
962
+ e.preventDefault();
963
+ if (await run(() => create.mutateAsync({ name: name.trim(), variant_id: variantId.trim() }))) {
964
+ setName("");
965
+ setVariantId("");
966
+ }
967
+ };
968
+ const move = (offers, i, by) => {
969
+ const ids = offers.map((o) => o.id);
970
+ [ids[i], ids[i + by]] = [ids[i + by], ids[i]];
971
+ void run(() => reorder.mutateAsync({ ids }));
972
+ };
973
+ const onRemove = async (o) => {
974
+ if (await prompt({ title: OFFERS_COPY.remove, description: OFFERS_COPY.removeConfirm, confirmText: OFFERS_COPY.remove, cancelText: "Cancel" })) void run(() => remove.mutateAsync({ id: o.id }));
975
+ };
976
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
977
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
978
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: OFFERS_COPY.title }),
979
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
980
+ OFFERS_COPY.intro,
981
+ " ",
982
+ OFFERS_COPY.lag
983
+ ] })
984
+ ] }),
985
+ isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) }) : isError || !data ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", "data-testid": "vonpay-offers-load-error", children: errorSentence(loadError, OFFERS_COPY.loadFailed) }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
986
+ !data.upsells_on && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: OFFERS_COPY.off }) }),
987
+ data.source === "config" && data.config.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 px-6 py-4", "data-testid": "vonpay-offers-from-config", children: [
988
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: OFFERS_COPY.fromConfig }),
989
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "list-disc pl-6", children: data.config.map((c) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: productLabel(c) }) }, c.variant_id)) }),
990
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", disabled: busy, onClick: () => void run(() => importConfig.mutateAsync(void 0)), children: OFFERS_COPY.importButton }) })
991
+ ] }),
992
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", "data-testid": "vonpay-offers-error", children: error }) }),
993
+ data.offers.length === 0 ? (data.source === "admin" || data.config.length === 0) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: data.source === "admin" ? OFFERS_COPY.adminEmpty : OFFERS_COPY.configEmpty }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
994
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
995
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Offer" }),
996
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Product" }),
997
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.rules }),
998
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.downsell }),
999
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.wording }),
1000
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.shown }),
1001
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, {})
1002
+ ] }) }),
1003
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.offers.map((o, i) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-offer-row", children: [
1004
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: o.name }),
1005
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: o.product_title === null ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: OFFERS_COPY.missingVariant }) : productLabel(o) }),
1006
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.rules}: ${o.name}`, disabled: busy, onClick: () => setEditing(o), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: o.rules === "unreadable" ? "text-ui-fg-error" : void 0, children: rulesSummary(o.rules) }) }) }),
1007
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.downsell}: ${o.name}`, disabled: busy, onClick: () => setDownsellFor(o), children: o.downsell_variant_id === null ? OFFERS_COPY.downsellNone : o.downsell_product_title === null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-fg-error", children: OFFERS_COPY.downsellMissing }) : productLabel({ product_title: o.downsell_product_title, variant_title: o.downsell_variant_title, variant_id: o.downsell_variant_id }) }) }),
1008
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.wording}: ${o.name}`, disabled: busy, onClick: () => setWordingFor(o), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `max-w-[16rem] truncate ${o.copy === "unreadable" ? "text-ui-fg-error" : ""}`, children: wordingSummary(o.copy) }) }) }),
1009
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Switch, { "aria-label": `${OFFERS_COPY.shown}: ${o.name}`, checked: o.enabled, disabled: busy, onCheckedChange: (on) => void run(() => update.mutateAsync({ id: o.id, enabled: on })) }) }),
1010
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-x-1", children: [
1011
+ /* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.up}: ${o.name}`, disabled: busy || i === 0, onClick: () => move(data.offers, i, -1), children: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpMini, {}) }),
1012
+ /* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.down}: ${o.name}`, disabled: busy || i === data.offers.length - 1, onClick: () => move(data.offers, i, 1), children: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowDownMini, {}) }),
1013
+ /* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", "aria-label": `${OFFERS_COPY.remove}: ${o.name}`, disabled: busy, onClick: () => void onRemove(o), children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {}) })
1014
+ ] }) })
1015
+ ] }, o.id)) })
1016
+ ] }) }),
1017
+ downsellFor && /* @__PURE__ */ jsxRuntime.jsx(
1018
+ OfferDownsellDrawer,
1019
+ {
1020
+ offer: downsellFor,
1021
+ open: downsellFor !== null,
1022
+ busy,
1023
+ onOpenChange: (o) => !o && setDownsellFor(null),
1024
+ onSave: async (downsell) => {
1025
+ try {
1026
+ await update.mutateAsync({ id: downsellFor.id, downsell_variant_id: downsell });
1027
+ return null;
1028
+ } catch (err) {
1029
+ return errorSentence(err);
1030
+ }
1031
+ }
1032
+ },
1033
+ downsellFor.id
1034
+ ),
1035
+ wordingFor && /* @__PURE__ */ jsxRuntime.jsx(
1036
+ OfferWordingDrawer,
1037
+ {
1038
+ offer: wordingFor,
1039
+ open: wordingFor !== null,
1040
+ busy,
1041
+ onOpenChange: (o) => !o && setWordingFor(null),
1042
+ onSave: async (copy) => {
1043
+ try {
1044
+ await update.mutateAsync({ id: wordingFor.id, copy });
1045
+ return null;
1046
+ } catch (err) {
1047
+ return errorSentence(err);
1048
+ }
1049
+ }
1050
+ },
1051
+ wordingFor.id
1052
+ ),
1053
+ editing && /* @__PURE__ */ jsxRuntime.jsx(
1054
+ OfferRulesDrawer,
1055
+ {
1056
+ offer: editing,
1057
+ open: editing !== null,
1058
+ busy,
1059
+ onOpenChange: (o) => !o && setEditing(null),
1060
+ onSave: async (rules) => {
1061
+ try {
1062
+ await update.mutateAsync({ id: editing.id, rules });
1063
+ return null;
1064
+ } catch (err) {
1065
+ return errorSentence(err);
1066
+ }
1067
+ }
1068
+ },
1069
+ editing.id
1070
+ ),
1071
+ /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "flex flex-col gap-y-3 px-6 py-4", onSubmit: (e) => void onAdd(e), children: [
1072
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: OFFERS_COPY.add }),
1073
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [
1074
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1075
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-name", children: OFFERS_COPY.name }),
1076
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-name", value: name, maxLength: 80, onChange: (e) => setName(e.target.value) })
1077
+ ] }),
1078
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
1079
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-variant", children: OFFERS_COPY.variant }),
1080
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-variant", value: variantId, onChange: (e) => setVariantId(e.target.value) })
1081
+ ] })
1082
+ ] }),
1083
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", type: "submit", disabled: busy || !name.trim() || !variantId.trim(), children: OFFERS_COPY.add }) })
1084
+ ] })
1085
+ ] })
1086
+ ] });
1087
+ };
1088
+ const config$2 = adminSdk.defineRouteConfig({
1089
+ label: "One-click offers",
1090
+ icon: icons.Sparkles,
1091
+ nested: "/orders"
1092
+ });
1093
+ const EVENT_LABEL = {
1094
+ "subscription.created": "Signed up",
1095
+ "subscription.renewed": "Renewal paid",
1096
+ "subscription.payment_failed": "Renewal declined",
1097
+ "subscription.cancelled": "Cancelled",
1098
+ "subscription.updated": "Changed"
1099
+ };
1100
+ const eventLabel = (type) => EVENT_LABEL[type] ?? "Other";
1101
+ const amountLabel = (major, currency) => major && currency ? `${major} ${currency}` : "—";
1102
+ const NOT_ENABLED_NOTE = `Vonpay is not running this store's subscriptions. To let it, set the payment provider's subscriptions option to "vonpay", register the plugin's subscriptions module in medusa-config, run npx medusa db:migrate and restart — the setup check confirms each step.`;
1103
+ const ORDERS_NOT_YET_NOTE = "Renewal orders are not created from these notifications yet: this list shows what Vonpay reported, so nothing here changes an order.";
1104
+ const SUBSCRIPTION_PAGE_SIZE = 20;
1105
+ function useSubscriptionEvents(page) {
1106
+ return reactQuery.useQuery({
1107
+ queryKey: ["vonpay-subscription-events", page.limit, page.offset],
1108
+ queryFn: () => sdk.client.fetch("/admin/vonpay/subscriptions", { query: { limit: page.limit, offset: page.offset } }),
1109
+ placeholderData: reactQuery.keepPreviousData
1110
+ });
1111
+ }
1112
+ const pageCountFor = (count) => {
1113
+ const remainder = count % SUBSCRIPTION_PAGE_SIZE;
1114
+ return Math.max(1, (count - remainder) / SUBSCRIPTION_PAGE_SIZE + (remainder > 0 ? 1 : 0));
1115
+ };
1116
+ const when = (iso) => iso ? new Date(iso).toLocaleString() : "—";
1117
+ const VonpaySubscriptionsPage = () => {
1118
+ const [pageIndex, setPageIndex] = react.useState(0);
1119
+ const { data, isLoading, isError } = useSubscriptionEvents({ limit: SUBSCRIPTION_PAGE_SIZE, offset: pageIndex * SUBSCRIPTION_PAGE_SIZE });
1120
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1121
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
1122
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Vonpay subscriptions" }),
1123
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Notifications from Vonpay's subscription billing, newest first." })
1124
+ ] }),
1125
+ isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) }) : isError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "The list could not be loaded. Reload the page to try again." }) }) : !data?.enabled ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: NOT_ENABLED_NOTE }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1126
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: ORDERS_NOT_YET_NOTE }) }),
1127
+ data.events.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "No subscription notifications yet." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1128
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
1129
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1130
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "What happened" }),
1131
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Subscription" }),
1132
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Period" }),
1133
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Amount" }),
1134
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Next bill" }),
1135
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Received" })
1136
+ ] }) }),
1137
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.events.map((e) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1138
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Cell, { children: [
1139
+ eventLabel(e.type),
1140
+ e.mode === "test" ? " (test)" : ""
1141
+ ] }),
1142
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", children: e.subscription_id }),
1143
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: e.billing_cycle ?? "—" }),
1144
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: amountLabel(e.amount_major, e.currency) }),
1145
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.next_bill_at) }),
1146
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.received_at) })
1147
+ ] }, e.id)) })
1148
+ ] }) }),
1149
+ /* @__PURE__ */ jsxRuntime.jsx(
1150
+ ui.Table.Pagination,
1151
+ {
1152
+ count: data.count,
1153
+ pageSize: SUBSCRIPTION_PAGE_SIZE,
1154
+ pageIndex,
1155
+ pageCount: pageCountFor(data.count),
1156
+ canPreviousPage: pageIndex > 0,
1157
+ canNextPage: (pageIndex + 1) * SUBSCRIPTION_PAGE_SIZE < data.count,
1158
+ previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
1159
+ nextPage: () => setPageIndex((p) => p + 1)
1160
+ }
1161
+ )
1162
+ ] })
1163
+ ] })
1164
+ ] });
1165
+ };
1166
+ const config$1 = adminSdk.defineRouteConfig({
1167
+ label: "Vonpay subscriptions",
1168
+ icon: icons.ArrowPath,
1169
+ nested: "/orders"
1170
+ });
629
1171
  function useSetupCheck() {
630
1172
  return reactQuery.useMutation({
631
1173
  mutationFn: () => sdk.client.fetch("/admin/vonpay/setup-check", { method: "POST", body: {} }),
@@ -1050,89 +1592,11 @@ const HeldPaymentsPage = () => {
1050
1592
  /* @__PURE__ */ jsxRuntime.jsx(SetupCheckPanel, {})
1051
1593
  ] });
1052
1594
  };
1053
- const config$1 = adminSdk.defineRouteConfig({
1595
+ const config = adminSdk.defineRouteConfig({
1054
1596
  label: "Held payments",
1055
1597
  icon: icons.ExclamationCircle,
1056
1598
  nested: "/orders"
1057
1599
  });
1058
- const EVENT_LABEL = {
1059
- "subscription.created": "Signed up",
1060
- "subscription.renewed": "Renewal paid",
1061
- "subscription.payment_failed": "Renewal declined",
1062
- "subscription.cancelled": "Cancelled",
1063
- "subscription.updated": "Changed"
1064
- };
1065
- const eventLabel = (type) => EVENT_LABEL[type] ?? "Other";
1066
- const amountLabel = (major, currency) => major && currency ? `${major} ${currency}` : "—";
1067
- const NOT_ENABLED_NOTE = `Vonpay is not running this store's subscriptions. To let it, set the payment provider's subscriptions option to "vonpay", register the plugin's subscriptions module in medusa-config, run npx medusa db:migrate and restart — the setup check confirms each step.`;
1068
- const ORDERS_NOT_YET_NOTE = "Renewal orders are not created from these notifications yet: this list shows what Vonpay reported, so nothing here changes an order.";
1069
- const SUBSCRIPTION_PAGE_SIZE = 20;
1070
- function useSubscriptionEvents(page) {
1071
- return reactQuery.useQuery({
1072
- queryKey: ["vonpay-subscription-events", page.limit, page.offset],
1073
- queryFn: () => sdk.client.fetch("/admin/vonpay/subscriptions", { query: { limit: page.limit, offset: page.offset } }),
1074
- placeholderData: reactQuery.keepPreviousData
1075
- });
1076
- }
1077
- const pageCountFor = (count) => {
1078
- const remainder = count % SUBSCRIPTION_PAGE_SIZE;
1079
- return Math.max(1, (count - remainder) / SUBSCRIPTION_PAGE_SIZE + (remainder > 0 ? 1 : 0));
1080
- };
1081
- const when = (iso) => iso ? new Date(iso).toLocaleString() : "—";
1082
- const VonpaySubscriptionsPage = () => {
1083
- const [pageIndex, setPageIndex] = react.useState(0);
1084
- const { data, isLoading, isError } = useSubscriptionEvents({ limit: SUBSCRIPTION_PAGE_SIZE, offset: pageIndex * SUBSCRIPTION_PAGE_SIZE });
1085
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
1086
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
1087
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Vonpay subscriptions" }),
1088
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Notifications from Vonpay's subscription billing, newest first." })
1089
- ] }),
1090
- isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-8 w-full" }) }) : isError ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: "The list could not be loaded. Reload the page to try again." }) }) : !data?.enabled ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: NOT_ENABLED_NOTE }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1091
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: ORDERS_NOT_YET_NOTE }) }),
1092
- data.events.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "No subscription notifications yet." }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1093
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
1094
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1095
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "What happened" }),
1096
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Subscription" }),
1097
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Period" }),
1098
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Amount" }),
1099
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Next bill" }),
1100
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Received" })
1101
- ] }) }),
1102
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.events.map((e) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1103
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Cell, { children: [
1104
- eventLabel(e.type),
1105
- e.mode === "test" ? " (test)" : ""
1106
- ] }),
1107
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "font-mono text-xs", children: e.subscription_id }),
1108
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: e.billing_cycle ?? "—" }),
1109
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: amountLabel(e.amount_major, e.currency) }),
1110
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.next_bill_at) }),
1111
- /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: when(e.received_at) })
1112
- ] }, e.id)) })
1113
- ] }) }),
1114
- /* @__PURE__ */ jsxRuntime.jsx(
1115
- ui.Table.Pagination,
1116
- {
1117
- count: data.count,
1118
- pageSize: SUBSCRIPTION_PAGE_SIZE,
1119
- pageIndex,
1120
- pageCount: pageCountFor(data.count),
1121
- canPreviousPage: pageIndex > 0,
1122
- canNextPage: (pageIndex + 1) * SUBSCRIPTION_PAGE_SIZE < data.count,
1123
- previousPage: () => setPageIndex((p) => Math.max(0, p - 1)),
1124
- nextPage: () => setPageIndex((p) => p + 1)
1125
- }
1126
- )
1127
- ] })
1128
- ] })
1129
- ] });
1130
- };
1131
- const config = adminSdk.defineRouteConfig({
1132
- label: "Vonpay subscriptions",
1133
- icon: icons.ArrowPath,
1134
- nested: "/orders"
1135
- });
1136
1600
  const i18nTranslations0 = {};
1137
1601
  const widgetModule = { widgets: [
1138
1602
  {
@@ -1154,13 +1618,18 @@ const widgetModule = { widgets: [
1154
1618
  const routeModule = {
1155
1619
  routes: [
1156
1620
  {
1157
- Component: HeldPaymentsPage,
1158
- path: "/vonpay-holds",
1159
- handle: { label: config$1.label, translationNs: config$1.translationNs }
1621
+ Component: VonpayOffersPage,
1622
+ path: "/vonpay-offers",
1623
+ handle: { label: config$2.label, translationNs: config$2.translationNs }
1160
1624
  },
1161
1625
  {
1162
1626
  Component: VonpaySubscriptionsPage,
1163
1627
  path: "/vonpay-subscriptions",
1628
+ handle: { label: config$1.label, translationNs: config$1.translationNs }
1629
+ },
1630
+ {
1631
+ Component: HeldPaymentsPage,
1632
+ path: "/vonpay-holds",
1164
1633
  handle: { label: config.label, translationNs: config.translationNs }
1165
1634
  }
1166
1635
  ]
@@ -1168,16 +1637,24 @@ const routeModule = {
1168
1637
  const menuItemModule = {
1169
1638
  menuItems: [
1170
1639
  {
1171
- label: config$1.label,
1172
- icon: config$1.icon,
1640
+ label: config.label,
1641
+ icon: config.icon,
1173
1642
  path: "/vonpay-holds",
1174
1643
  nested: "/orders",
1175
1644
  rank: void 0,
1176
1645
  translationNs: void 0
1177
1646
  },
1178
1647
  {
1179
- label: config.label,
1180
- icon: config.icon,
1648
+ label: config$2.label,
1649
+ icon: config$2.icon,
1650
+ path: "/vonpay-offers",
1651
+ nested: "/orders",
1652
+ rank: void 0,
1653
+ translationNs: void 0
1654
+ },
1655
+ {
1656
+ label: config$1.label,
1657
+ icon: config$1.icon,
1181
1658
  path: "/vonpay-subscriptions",
1182
1659
  nested: "/orders",
1183
1660
  rank: void 0,