vonpay-medusa 1.0.4 → 1.1.1
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/.medusa/server/src/admin/index.js +482 -5
- package/.medusa/server/src/admin/index.mjs +484 -7
- package/.medusa/server/src/api/admin/vonpay/offers/[id]/route.js +20 -0
- package/.medusa/server/src/api/admin/vonpay/offers/import/route.js +12 -0
- package/.medusa/server/src/api/admin/vonpay/offers/reorder/route.js +11 -0
- package/.medusa/server/src/api/admin/vonpay/offers/route.js +18 -0
- package/.medusa/server/src/api/middlewares.js +4 -0
- package/.medusa/server/src/lib/errors.js +16 -1
- package/.medusa/server/src/lib/money.js +8 -0
- package/.medusa/server/src/modules/vonpay-upsells/index.js +11 -0
- package/.medusa/server/src/modules/vonpay-upsells/key.js +4 -0
- package/.medusa/server/src/modules/vonpay-upsells/migrations/Migration20261023131258.js +20 -0
- package/.medusa/server/src/modules/vonpay-upsells/migrations/Migration20261023142238.js +15 -0
- package/.medusa/server/src/modules/vonpay-upsells/models/event.js +25 -0
- package/.medusa/server/src/modules/vonpay-upsells/models/offer.js +23 -0
- package/.medusa/server/src/modules/vonpay-upsells/service.js +8 -0
- package/.medusa/server/src/providers/vonpay/options.js +11 -7
- package/.medusa/server/src/providers/vonpay/service.js +29 -2
- package/.medusa/server/src/providers/vonpay/session-data.js +11 -2
- package/.medusa/server/src/providers/vonpay/setup-check.js +69 -0
- package/.medusa/server/src/providers/vonpay/ship-hold.js +12 -6
- package/.medusa/server/src/providers/vonpay/upsell-accept.js +151 -24
- package/.medusa/server/src/providers/vonpay/upsell-copy.js +48 -0
- package/.medusa/server/src/providers/vonpay/upsell-offers-admin.js +275 -0
- package/.medusa/server/src/providers/vonpay/upsell-offers.js +136 -0
- package/.medusa/server/src/providers/vonpay/upsell-routes.js +5 -0
- package/.medusa/server/src/providers/vonpay/upsell-rules.js +106 -0
- package/README.md +66 -7
- package/package.json +1 -1
- package/storefront/src/index.ts +1 -1
- package/storefront/src/offer-pass.ts +15 -3
- package/storefront/src/server.ts +3 -6
- package/storefront/src/upsell.ts +45 -1
- package/storefront/src/vonpay-upsell-offer.tsx +60 -38
- package/storefront/src/vonpay-upsell-return.tsx +30 -28
|
@@ -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) {
|
|
@@ -1050,11 +1050,475 @@ const HeldPaymentsPage = () => {
|
|
|
1050
1050
|
/* @__PURE__ */ jsxRuntime.jsx(SetupCheckPanel, {})
|
|
1051
1051
|
] });
|
|
1052
1052
|
};
|
|
1053
|
-
const config$
|
|
1053
|
+
const config$2 = adminSdk.defineRouteConfig({
|
|
1054
1054
|
label: "Held payments",
|
|
1055
1055
|
icon: icons.ExclamationCircle,
|
|
1056
1056
|
nested: "/orders"
|
|
1057
1057
|
});
|
|
1058
|
+
const HEADLINE_MAX = 80;
|
|
1059
|
+
const PITCH_MAX = 300;
|
|
1060
|
+
const OFFERS_COPY = {
|
|
1061
|
+
title: "One-click offers",
|
|
1062
|
+
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.",
|
|
1063
|
+
off: "One-click offers are off. Turn them on with the upsells option in medusa-config; the offers below are kept.",
|
|
1064
|
+
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.",
|
|
1065
|
+
configEmpty: "No offers yet. Add the first one below.",
|
|
1066
|
+
adminEmpty: "No offers are shown to shoppers. Add one below.",
|
|
1067
|
+
importButton: "Import from medusa-config",
|
|
1068
|
+
add: "Add offer",
|
|
1069
|
+
name: "Name (only you see it)",
|
|
1070
|
+
variant: "Product variant id (variant_…)",
|
|
1071
|
+
shown: "Shown",
|
|
1072
|
+
up: "Move up",
|
|
1073
|
+
down: "Move down",
|
|
1074
|
+
remove: "Remove",
|
|
1075
|
+
removeConfirm: "Remove this offer? A shopper already paying for it is not affected.",
|
|
1076
|
+
missingVariant: "This product variant no longer exists — it is not offered.",
|
|
1077
|
+
loadFailed: "The offers could not be loaded. Reload the page to try again.",
|
|
1078
|
+
lag: "A change shows on your store at once; with more than one server it can take up to 30 seconds everywhere.",
|
|
1079
|
+
rules: "Who sees it",
|
|
1080
|
+
downsell: "If they say No thanks",
|
|
1081
|
+
downsellNone: "Nothing",
|
|
1082
|
+
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.',
|
|
1083
|
+
downsellVariant: "Downsell product variant id (variant_…)",
|
|
1084
|
+
downsellRemove: "Remove downsell",
|
|
1085
|
+
downsellMissing: "This product variant no longer exists — it is not offered.",
|
|
1086
|
+
wording: "Wording",
|
|
1087
|
+
wordingDefault: "Standard",
|
|
1088
|
+
wordingPitchOnly: "Pitch only",
|
|
1089
|
+
wordingUnreadable: "The saved wording can't be read, so shoppers see the standard wording. Save it again.",
|
|
1090
|
+
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.",
|
|
1091
|
+
headline: "Headline",
|
|
1092
|
+
headlineHint: 'Replaces the heading. Standard: "Add this to your order?"',
|
|
1093
|
+
pitch: "Pitch",
|
|
1094
|
+
pitchHint: "Replaces the product's description under its name. A phone shows about two lines.",
|
|
1095
|
+
wordingDownsellNote: `A downsell keeps the standard wording: "How about this instead?" and its product's description.`,
|
|
1096
|
+
wordingReset: "Use the standard wording",
|
|
1097
|
+
rulesEveryone: "Everyone",
|
|
1098
|
+
rulesUnreadable: "Its rules can't be read, so it is not shown. Set them again.",
|
|
1099
|
+
rulesIntro: "Show this offer only when ALL the conditions you set match. Leave a condition empty to ignore it.",
|
|
1100
|
+
customers: "Customers",
|
|
1101
|
+
customersAll: "Everyone",
|
|
1102
|
+
customersGuests: "Guests only",
|
|
1103
|
+
customersSignedIn: "Signed-in customers only",
|
|
1104
|
+
products: "The shopper bought one of these products at checkout",
|
|
1105
|
+
collections: "The shopper bought a product from one of these collections at checkout",
|
|
1106
|
+
groups: "The signed-in customer is in one of these groups",
|
|
1107
|
+
paid: "The amount paid at checkout (tax and shipping included)",
|
|
1108
|
+
paidNote: "An order in a currency not listed here does not see this offer.",
|
|
1109
|
+
paidAtLeast: "At least",
|
|
1110
|
+
paidAtMost: "At most",
|
|
1111
|
+
search: "Search by name",
|
|
1112
|
+
addCurrency: "Add currency",
|
|
1113
|
+
removeCurrency: "Remove currency",
|
|
1114
|
+
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.",
|
|
1115
|
+
replaceRules: "Replace the rules",
|
|
1116
|
+
save: "Save",
|
|
1117
|
+
cancel: "Cancel",
|
|
1118
|
+
saveFailed: "That change was not saved."
|
|
1119
|
+
};
|
|
1120
|
+
function errorSentence(err, fallback = OFFERS_COPY.saveFailed) {
|
|
1121
|
+
const m = err?.message;
|
|
1122
|
+
if (typeof m !== "string") return fallback;
|
|
1123
|
+
const i = m.indexOf(": ");
|
|
1124
|
+
return m.startsWith("vonpay_") && i > 0 ? m.slice(i + 2) : fallback;
|
|
1125
|
+
}
|
|
1126
|
+
function productLabel(o) {
|
|
1127
|
+
if (o.product_title === null) return o.variant_id;
|
|
1128
|
+
return [o.product_title, o.variant_title].filter((x) => x && x !== "Default variant").join(" — ");
|
|
1129
|
+
}
|
|
1130
|
+
function wordingSummary(copy) {
|
|
1131
|
+
if (copy === "unreadable") return OFFERS_COPY.wordingUnreadable;
|
|
1132
|
+
if (!copy) return OFFERS_COPY.wordingDefault;
|
|
1133
|
+
return copy.headline ?? OFFERS_COPY.wordingPitchOnly;
|
|
1134
|
+
}
|
|
1135
|
+
function rulesSummary(rules) {
|
|
1136
|
+
if (rules === "unreadable") return OFFERS_COPY.rulesUnreadable;
|
|
1137
|
+
if (!rules) return OFFERS_COPY.rulesEveryone;
|
|
1138
|
+
const parts = [];
|
|
1139
|
+
if (rules.customers === "guests") parts.push("Guests");
|
|
1140
|
+
if (rules.customers === "signed_in") parts.push("Signed-in");
|
|
1141
|
+
if (rules.groups) parts.push(`${rules.groups.length} group${rules.groups.length === 1 ? "" : "s"}`);
|
|
1142
|
+
if (rules.products) parts.push(`${rules.products.length} product${rules.products.length === 1 ? "" : "s"}`);
|
|
1143
|
+
if (rules.collections) parts.push(`${rules.collections.length} collection${rules.collections.length === 1 ? "" : "s"}`);
|
|
1144
|
+
for (const [c, n] of Object.entries(rules.min_paid ?? {})) parts.push(`paid ≥ ${n} ${c.toUpperCase()}`);
|
|
1145
|
+
for (const [c, n] of Object.entries(rules.max_paid ?? {})) parts.push(`paid ≤ ${n} ${c.toUpperCase()}`);
|
|
1146
|
+
return parts.length ? parts.join(" · ") : OFFERS_COPY.rulesEveryone;
|
|
1147
|
+
}
|
|
1148
|
+
function OfferDownsellDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
1149
|
+
const [variant, setVariant] = react.useState(offer.downsell_variant_id ?? "");
|
|
1150
|
+
const [problem, setProblem] = react.useState(null);
|
|
1151
|
+
const save = async (value) => {
|
|
1152
|
+
if (value !== null && !/^variant_[A-Za-z0-9]{1,64}$/.test(value)) return setProblem("Enter a product variant id, like variant_01H….");
|
|
1153
|
+
setProblem(null);
|
|
1154
|
+
const refused = await onSave(value);
|
|
1155
|
+
if (refused === null) onOpenChange(false);
|
|
1156
|
+
else setProblem(refused);
|
|
1157
|
+
};
|
|
1158
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
|
|
1159
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
|
|
1160
|
+
OFFERS_COPY.downsell,
|
|
1161
|
+
": ",
|
|
1162
|
+
offer.name
|
|
1163
|
+
] }) }),
|
|
1164
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
|
|
1165
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.downsellIntro }),
|
|
1166
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1167
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-downsell-variant", children: OFFERS_COPY.downsellVariant }),
|
|
1168
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-downsell-variant", value: variant, maxLength: 72, onChange: (e) => setVariant(e.target.value) })
|
|
1169
|
+
] }),
|
|
1170
|
+
problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-downsell-problem", children: problem })
|
|
1171
|
+
] }),
|
|
1172
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
|
|
1173
|
+
offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "danger", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.downsellRemove }),
|
|
1174
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
|
|
1175
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(variant.trim()), children: OFFERS_COPY.save })
|
|
1176
|
+
] })
|
|
1177
|
+
] }) });
|
|
1178
|
+
}
|
|
1179
|
+
const SOURCES = {
|
|
1180
|
+
products: { path: "/admin/products", key: "products", label: "title" },
|
|
1181
|
+
collections: { path: "/admin/collections", key: "collections", label: "title" },
|
|
1182
|
+
groups: { path: "/admin/customer-groups", key: "customer_groups", label: "name" }
|
|
1183
|
+
};
|
|
1184
|
+
async function fetchNamed(kind, query) {
|
|
1185
|
+
const s = SOURCES[kind];
|
|
1186
|
+
const body = await sdk.client.fetch(s.path, { query: { ...query, fields: `id,${s.label}` } });
|
|
1187
|
+
const plain = (t) => t.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, "").trim();
|
|
1188
|
+
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 }] : []);
|
|
1189
|
+
}
|
|
1190
|
+
function Picker({ kind, label, ids, onChange }) {
|
|
1191
|
+
const [q, setQ] = react.useState("");
|
|
1192
|
+
const names = reactQuery.useQuery({ queryKey: ["vonpay-offer-rule-names", kind, ids], queryFn: () => fetchNamed(kind, { id: ids, limit: ids.length }), enabled: ids.length > 0 });
|
|
1193
|
+
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 });
|
|
1194
|
+
const nameOf = (id) => names.data?.find((n) => n.id === id)?.label ?? id;
|
|
1195
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": `vonpay-rule-${kind}`, children: [
|
|
1196
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: label }),
|
|
1197
|
+
/* @__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: [
|
|
1198
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: nameOf(id) }),
|
|
1199
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": `Remove ${nameOf(id)}`, onClick: () => onChange(ids.filter((x) => x !== id)), children: /* @__PURE__ */ jsxRuntime.jsx(icons.XMarkMini, {}) })
|
|
1200
|
+
] }, id)) }),
|
|
1201
|
+
/* @__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) }),
|
|
1202
|
+
(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: [
|
|
1203
|
+
"+ ",
|
|
1204
|
+
n.label
|
|
1205
|
+
] }, n.id))
|
|
1206
|
+
] });
|
|
1207
|
+
}
|
|
1208
|
+
function rowsOf(rules) {
|
|
1209
|
+
const cs = [.../* @__PURE__ */ new Set([...Object.keys(rules?.min_paid ?? {}), ...Object.keys(rules?.max_paid ?? {})])];
|
|
1210
|
+
return cs.map((c) => ({ currency: c, min: rules?.min_paid?.[c] ?? "", max: rules?.max_paid?.[c] ?? "" }));
|
|
1211
|
+
}
|
|
1212
|
+
function amountText(text) {
|
|
1213
|
+
const t = text.trim().replace(",", ".");
|
|
1214
|
+
if (t === "") return void 0;
|
|
1215
|
+
return /^\d{1,10}(\.\d{1,6})?$/.test(t) ? t : null;
|
|
1216
|
+
}
|
|
1217
|
+
function rulesFromForm(f) {
|
|
1218
|
+
const out = {};
|
|
1219
|
+
if (f.customers !== "all") out.customers = f.customers;
|
|
1220
|
+
if (f.products.length) out.products = f.products;
|
|
1221
|
+
if (f.collections.length) out.collections = f.collections;
|
|
1222
|
+
if (f.groups.length) out.groups = f.groups;
|
|
1223
|
+
for (const r of f.paid) {
|
|
1224
|
+
const c = r.currency.trim().toLowerCase();
|
|
1225
|
+
if (!c && !r.min.trim() && !r.max.trim()) continue;
|
|
1226
|
+
if (!/^[a-z]{3}$/.test(c)) return "Use a 3-letter currency code, like EUR.";
|
|
1227
|
+
const min = amountText(r.min);
|
|
1228
|
+
const max = amountText(r.max);
|
|
1229
|
+
if (min === null || max === null) return `Enter a plain amount for ${c.toUpperCase()}, like 49.95.`;
|
|
1230
|
+
if (min === void 0 && max === void 0) return `Enter the least or the most for ${c.toUpperCase()}, or remove its row.`;
|
|
1231
|
+
if (out.min_paid?.[c] !== void 0 || out.max_paid?.[c] !== void 0) return `${c.toUpperCase()} is listed twice.`;
|
|
1232
|
+
if (min !== void 0) out.min_paid = { ...out.min_paid, [c]: min };
|
|
1233
|
+
if (max !== void 0) out.max_paid = { ...out.max_paid, [c]: max };
|
|
1234
|
+
}
|
|
1235
|
+
if (out.customers === "guests" && out.groups) return "Groups apply to signed-in customers only.";
|
|
1236
|
+
return Object.keys(out).length ? out : null;
|
|
1237
|
+
}
|
|
1238
|
+
function OfferRulesDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
1239
|
+
const unreadable = offer.rules === "unreadable";
|
|
1240
|
+
const start = offer.rules === "unreadable" ? null : offer.rules;
|
|
1241
|
+
const [customers, setCustomers] = react.useState(start?.customers ?? "all");
|
|
1242
|
+
const [products, setProducts] = react.useState(start?.products ?? []);
|
|
1243
|
+
const [collections, setCollections] = react.useState(start?.collections ?? []);
|
|
1244
|
+
const [groups, setGroups] = react.useState(start?.groups ?? []);
|
|
1245
|
+
const [paid, setPaid] = react.useState(rowsOf(start));
|
|
1246
|
+
const [problem, setProblem] = react.useState(null);
|
|
1247
|
+
const save = async () => {
|
|
1248
|
+
const r = rulesFromForm({ customers, products, collections, groups, paid });
|
|
1249
|
+
if (typeof r === "string") return setProblem(r);
|
|
1250
|
+
setProblem(null);
|
|
1251
|
+
const refused = await onSave(r);
|
|
1252
|
+
if (refused === null) onOpenChange(false);
|
|
1253
|
+
else setProblem(refused);
|
|
1254
|
+
};
|
|
1255
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
|
|
1256
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
|
|
1257
|
+
OFFERS_COPY.rules,
|
|
1258
|
+
": ",
|
|
1259
|
+
offer.name
|
|
1260
|
+
] }) }),
|
|
1261
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-6 overflow-y-auto", children: [
|
|
1262
|
+
unreadable && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-unreadable", children: OFFERS_COPY.rulesUnreadableNotice }),
|
|
1263
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.rulesIntro }),
|
|
1264
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", children: [
|
|
1265
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.customers }),
|
|
1266
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: customers, onValueChange: (v) => setCustomers(v), children: [
|
|
1267
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { "aria-label": OFFERS_COPY.customers, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, {}) }),
|
|
1268
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
|
|
1269
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "all", children: OFFERS_COPY.customersAll }),
|
|
1270
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "guests", children: OFFERS_COPY.customersGuests }),
|
|
1271
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: "signed_in", children: OFFERS_COPY.customersSignedIn })
|
|
1272
|
+
] })
|
|
1273
|
+
] })
|
|
1274
|
+
] }),
|
|
1275
|
+
/* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "products", label: OFFERS_COPY.products, ids: products, onChange: setProducts }),
|
|
1276
|
+
/* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "collections", label: OFFERS_COPY.collections, ids: collections, onChange: setCollections }),
|
|
1277
|
+
/* @__PURE__ */ jsxRuntime.jsx(Picker, { kind: "groups", label: OFFERS_COPY.groups, ids: groups, onChange: setGroups }),
|
|
1278
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2", "data-testid": "vonpay-rule-paid", children: [
|
|
1279
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: OFFERS_COPY.paid }),
|
|
1280
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.paidNote }),
|
|
1281
|
+
paid.map((r, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-[1fr_1fr_1fr_auto] items-center gap-2", children: [
|
|
1282
|
+
/* @__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)) }),
|
|
1283
|
+
/* @__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)) }),
|
|
1284
|
+
/* @__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)) }),
|
|
1285
|
+
/* @__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, {}) })
|
|
1286
|
+
] }, i)),
|
|
1287
|
+
/* @__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 }) })
|
|
1288
|
+
] }),
|
|
1289
|
+
problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-rules-problem", children: problem })
|
|
1290
|
+
] }),
|
|
1291
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
|
|
1292
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
|
|
1293
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(), children: unreadable ? OFFERS_COPY.replaceRules : OFFERS_COPY.save })
|
|
1294
|
+
] })
|
|
1295
|
+
] }) });
|
|
1296
|
+
}
|
|
1297
|
+
function OfferWordingDrawer({ offer, open, onOpenChange, onSave, busy }) {
|
|
1298
|
+
const start = offer.copy === "unreadable" ? null : offer.copy;
|
|
1299
|
+
const [headline, setHeadline] = react.useState(start?.headline ?? "");
|
|
1300
|
+
const [pitch, setPitch] = react.useState(start?.pitch ?? "");
|
|
1301
|
+
const [problem, setProblem] = react.useState(null);
|
|
1302
|
+
const save = async (copy) => {
|
|
1303
|
+
setProblem(null);
|
|
1304
|
+
const refused = await onSave(copy);
|
|
1305
|
+
if (refused === null) onOpenChange(false);
|
|
1306
|
+
else setProblem(refused);
|
|
1307
|
+
};
|
|
1308
|
+
const typed = () => {
|
|
1309
|
+
const h = headline.trim();
|
|
1310
|
+
const p = pitch.trim();
|
|
1311
|
+
return h || p ? { ...h ? { headline: h } : {}, ...p ? { pitch: p } : {} } : null;
|
|
1312
|
+
};
|
|
1313
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { "aria-describedby": void 0, children: [
|
|
1314
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
|
|
1315
|
+
OFFERS_COPY.wording,
|
|
1316
|
+
": ",
|
|
1317
|
+
offer.name
|
|
1318
|
+
] }) }),
|
|
1319
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-y-4", children: [
|
|
1320
|
+
offer.copy === "unreadable" && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-unreadable", children: OFFERS_COPY.wordingUnreadable }),
|
|
1321
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingIntro }),
|
|
1322
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1323
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-headline", children: OFFERS_COPY.headline }),
|
|
1324
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-wording-headline", value: headline, maxLength: HEADLINE_MAX, onChange: (e) => setHeadline(e.target.value) }),
|
|
1325
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.headlineHint })
|
|
1326
|
+
] }),
|
|
1327
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1328
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-wording-pitch", children: OFFERS_COPY.pitch }),
|
|
1329
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Textarea, { id: "vonpay-wording-pitch", value: pitch, maxLength: PITCH_MAX, rows: 4, onChange: (e) => setPitch(e.target.value) }),
|
|
1330
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
1331
|
+
OFFERS_COPY.pitchHint,
|
|
1332
|
+
" (",
|
|
1333
|
+
pitch.length,
|
|
1334
|
+
"/",
|
|
1335
|
+
PITCH_MAX,
|
|
1336
|
+
")"
|
|
1337
|
+
] })
|
|
1338
|
+
] }),
|
|
1339
|
+
offer.downsell_variant_id && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: OFFERS_COPY.wordingDownsellNote }),
|
|
1340
|
+
problem && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-error", "data-testid": "vonpay-wording-problem", children: problem })
|
|
1341
|
+
] }),
|
|
1342
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
|
|
1343
|
+
offer.copy !== null && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", disabled: busy, onClick: () => void save(null), children: OFFERS_COPY.wordingReset }),
|
|
1344
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => onOpenChange(false), children: OFFERS_COPY.cancel }),
|
|
1345
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: busy, onClick: () => void save(typed()), children: OFFERS_COPY.save })
|
|
1346
|
+
] })
|
|
1347
|
+
] }) });
|
|
1348
|
+
}
|
|
1349
|
+
const KEY = "vonpay-offers";
|
|
1350
|
+
function useOffers() {
|
|
1351
|
+
return reactQuery.useQuery({ queryKey: [KEY], queryFn: () => sdk.client.fetch("/admin/vonpay/offers") });
|
|
1352
|
+
}
|
|
1353
|
+
function useWrite(send) {
|
|
1354
|
+
const qc = reactQuery.useQueryClient();
|
|
1355
|
+
return reactQuery.useMutation({ mutationFn: send, retry: false, onSettled: () => qc.invalidateQueries({ queryKey: [KEY] }) });
|
|
1356
|
+
}
|
|
1357
|
+
const useCreateOffer = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers", { method: "POST", body: v }));
|
|
1358
|
+
const useUpdateOffer = () => useWrite((v) => {
|
|
1359
|
+
const { id, ...body } = v;
|
|
1360
|
+
return sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(id)}`, { method: "POST", body });
|
|
1361
|
+
});
|
|
1362
|
+
const useDeleteOffer = () => useWrite((v) => sdk.client.fetch(`/admin/vonpay/offers/${encodeURIComponent(v.id)}`, { method: "DELETE" }));
|
|
1363
|
+
const useReorderOffers = () => useWrite((v) => sdk.client.fetch("/admin/vonpay/offers/reorder", { method: "POST", body: v }));
|
|
1364
|
+
const useImportOffers = () => useWrite(() => sdk.client.fetch("/admin/vonpay/offers/import", { method: "POST", body: {} }));
|
|
1365
|
+
const VonpayOffersPage = () => {
|
|
1366
|
+
const { data, isLoading, isError, error: loadError } = useOffers();
|
|
1367
|
+
const create = useCreateOffer();
|
|
1368
|
+
const update = useUpdateOffer();
|
|
1369
|
+
const remove = useDeleteOffer();
|
|
1370
|
+
const reorder = useReorderOffers();
|
|
1371
|
+
const importConfig = useImportOffers();
|
|
1372
|
+
const prompt = ui.usePrompt();
|
|
1373
|
+
const [name, setName] = react.useState("");
|
|
1374
|
+
const [variantId, setVariantId] = react.useState("");
|
|
1375
|
+
const [error, setError] = react.useState(null);
|
|
1376
|
+
const [editing, setEditing] = react.useState(null);
|
|
1377
|
+
const [downsellFor, setDownsellFor] = react.useState(null);
|
|
1378
|
+
const [wordingFor, setWordingFor] = react.useState(null);
|
|
1379
|
+
const busy = create.isPending || update.isPending || remove.isPending || reorder.isPending || importConfig.isPending;
|
|
1380
|
+
const run = async (work) => {
|
|
1381
|
+
setError(null);
|
|
1382
|
+
try {
|
|
1383
|
+
await work();
|
|
1384
|
+
return true;
|
|
1385
|
+
} catch (err) {
|
|
1386
|
+
setError(errorSentence(err));
|
|
1387
|
+
return false;
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
const onAdd = async (e) => {
|
|
1391
|
+
e.preventDefault();
|
|
1392
|
+
if (await run(() => create.mutateAsync({ name: name.trim(), variant_id: variantId.trim() }))) {
|
|
1393
|
+
setName("");
|
|
1394
|
+
setVariantId("");
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
const move = (offers, i, by) => {
|
|
1398
|
+
const ids = offers.map((o) => o.id);
|
|
1399
|
+
[ids[i], ids[i + by]] = [ids[i + by], ids[i]];
|
|
1400
|
+
void run(() => reorder.mutateAsync({ ids }));
|
|
1401
|
+
};
|
|
1402
|
+
const onRemove = async (o) => {
|
|
1403
|
+
if (await prompt({ title: OFFERS_COPY.remove, description: OFFERS_COPY.removeConfirm, confirmText: OFFERS_COPY.remove, cancelText: "Cancel" })) void run(() => remove.mutateAsync({ id: o.id }));
|
|
1404
|
+
};
|
|
1405
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
1406
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
|
|
1407
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: OFFERS_COPY.title }),
|
|
1408
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
1409
|
+
OFFERS_COPY.intro,
|
|
1410
|
+
" ",
|
|
1411
|
+
OFFERS_COPY.lag
|
|
1412
|
+
] })
|
|
1413
|
+
] }),
|
|
1414
|
+
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: [
|
|
1415
|
+
!data.upsells_on && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: OFFERS_COPY.off }) }),
|
|
1416
|
+
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: [
|
|
1417
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "info", children: OFFERS_COPY.fromConfig }),
|
|
1418
|
+
/* @__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)) }),
|
|
1419
|
+
/* @__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 }) })
|
|
1420
|
+
] }),
|
|
1421
|
+
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 }) }),
|
|
1422
|
+
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: [
|
|
1423
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
1424
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Offer" }),
|
|
1425
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Product" }),
|
|
1426
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.rules }),
|
|
1427
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.downsell }),
|
|
1428
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.wording }),
|
|
1429
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: OFFERS_COPY.shown }),
|
|
1430
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, {})
|
|
1431
|
+
] }) }),
|
|
1432
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: data.offers.map((o, i) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { "data-testid": "vonpay-offer-row", children: [
|
|
1433
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: o.name }),
|
|
1434
|
+
/* @__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) }),
|
|
1435
|
+
/* @__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) }) }) }),
|
|
1436
|
+
/* @__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 }) }) }),
|
|
1437
|
+
/* @__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) }) }) }),
|
|
1438
|
+
/* @__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 })) }) }),
|
|
1439
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-x-1", children: [
|
|
1440
|
+
/* @__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, {}) }),
|
|
1441
|
+
/* @__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, {}) }),
|
|
1442
|
+
/* @__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, {}) })
|
|
1443
|
+
] }) })
|
|
1444
|
+
] }, o.id)) })
|
|
1445
|
+
] }) }),
|
|
1446
|
+
downsellFor && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1447
|
+
OfferDownsellDrawer,
|
|
1448
|
+
{
|
|
1449
|
+
offer: downsellFor,
|
|
1450
|
+
open: downsellFor !== null,
|
|
1451
|
+
busy,
|
|
1452
|
+
onOpenChange: (o) => !o && setDownsellFor(null),
|
|
1453
|
+
onSave: async (downsell) => {
|
|
1454
|
+
try {
|
|
1455
|
+
await update.mutateAsync({ id: downsellFor.id, downsell_variant_id: downsell });
|
|
1456
|
+
return null;
|
|
1457
|
+
} catch (err) {
|
|
1458
|
+
return errorSentence(err);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
downsellFor.id
|
|
1463
|
+
),
|
|
1464
|
+
wordingFor && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1465
|
+
OfferWordingDrawer,
|
|
1466
|
+
{
|
|
1467
|
+
offer: wordingFor,
|
|
1468
|
+
open: wordingFor !== null,
|
|
1469
|
+
busy,
|
|
1470
|
+
onOpenChange: (o) => !o && setWordingFor(null),
|
|
1471
|
+
onSave: async (copy) => {
|
|
1472
|
+
try {
|
|
1473
|
+
await update.mutateAsync({ id: wordingFor.id, copy });
|
|
1474
|
+
return null;
|
|
1475
|
+
} catch (err) {
|
|
1476
|
+
return errorSentence(err);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
},
|
|
1480
|
+
wordingFor.id
|
|
1481
|
+
),
|
|
1482
|
+
editing && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1483
|
+
OfferRulesDrawer,
|
|
1484
|
+
{
|
|
1485
|
+
offer: editing,
|
|
1486
|
+
open: editing !== null,
|
|
1487
|
+
busy,
|
|
1488
|
+
onOpenChange: (o) => !o && setEditing(null),
|
|
1489
|
+
onSave: async (rules) => {
|
|
1490
|
+
try {
|
|
1491
|
+
await update.mutateAsync({ id: editing.id, rules });
|
|
1492
|
+
return null;
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
return errorSentence(err);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
},
|
|
1498
|
+
editing.id
|
|
1499
|
+
),
|
|
1500
|
+
/* @__PURE__ */ jsxRuntime.jsxs("form", { className: "flex flex-col gap-y-3 px-6 py-4", onSubmit: (e) => void onAdd(e), children: [
|
|
1501
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: OFFERS_COPY.add }),
|
|
1502
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [
|
|
1503
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1504
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-name", children: OFFERS_COPY.name }),
|
|
1505
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-name", value: name, maxLength: 80, onChange: (e) => setName(e.target.value) })
|
|
1506
|
+
] }),
|
|
1507
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-1", children: [
|
|
1508
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vonpay-offer-variant", children: OFFERS_COPY.variant }),
|
|
1509
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { id: "vonpay-offer-variant", value: variantId, onChange: (e) => setVariantId(e.target.value) })
|
|
1510
|
+
] })
|
|
1511
|
+
] }),
|
|
1512
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", type: "submit", disabled: busy || !name.trim() || !variantId.trim(), children: OFFERS_COPY.add }) })
|
|
1513
|
+
] })
|
|
1514
|
+
] })
|
|
1515
|
+
] });
|
|
1516
|
+
};
|
|
1517
|
+
const config$1 = adminSdk.defineRouteConfig({
|
|
1518
|
+
label: "One-click offers",
|
|
1519
|
+
icon: icons.Sparkles,
|
|
1520
|
+
nested: "/orders"
|
|
1521
|
+
});
|
|
1058
1522
|
const EVENT_LABEL = {
|
|
1059
1523
|
"subscription.created": "Signed up",
|
|
1060
1524
|
"subscription.renewed": "Renewal paid",
|
|
@@ -1156,6 +1620,11 @@ const routeModule = {
|
|
|
1156
1620
|
{
|
|
1157
1621
|
Component: HeldPaymentsPage,
|
|
1158
1622
|
path: "/vonpay-holds",
|
|
1623
|
+
handle: { label: config$2.label, translationNs: config$2.translationNs }
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
Component: VonpayOffersPage,
|
|
1627
|
+
path: "/vonpay-offers",
|
|
1159
1628
|
handle: { label: config$1.label, translationNs: config$1.translationNs }
|
|
1160
1629
|
},
|
|
1161
1630
|
{
|
|
@@ -1167,10 +1636,18 @@ const routeModule = {
|
|
|
1167
1636
|
};
|
|
1168
1637
|
const menuItemModule = {
|
|
1169
1638
|
menuItems: [
|
|
1639
|
+
{
|
|
1640
|
+
label: config$2.label,
|
|
1641
|
+
icon: config$2.icon,
|
|
1642
|
+
path: "/vonpay-holds",
|
|
1643
|
+
nested: "/orders",
|
|
1644
|
+
rank: void 0,
|
|
1645
|
+
translationNs: void 0
|
|
1646
|
+
},
|
|
1170
1647
|
{
|
|
1171
1648
|
label: config$1.label,
|
|
1172
1649
|
icon: config$1.icon,
|
|
1173
|
-
path: "/vonpay-
|
|
1650
|
+
path: "/vonpay-offers",
|
|
1174
1651
|
nested: "/orders",
|
|
1175
1652
|
rank: void 0,
|
|
1176
1653
|
translationNs: void 0
|