@zuplo/zudoku-plugin-monetization 0.0.26 → 0.0.28

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 (2) hide show
  1. package/dist/index.mjs +308 -161
  2. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { cn, createPlugin, joinUrl, throwIfProblemJson } from "zudoku";
2
- import { AlertTriangleIcon, ArrowDownIcon, ArrowLeftRightIcon, ArrowUpIcon, CalendarIcon, CheckCheckIcon, CheckIcon, CircleAlert, CircleSlashIcon, ClockIcon, CreditCardIcon, Grid2x2XIcon, InfoIcon, Loader2Icon, LockIcon, MoreVerticalIcon, RefreshCcw, RefreshCwIcon, Settings, ShieldIcon, StarsIcon, Trash2Icon, XIcon } from "zudoku/icons";
2
+ import { AlertTriangleIcon, ArrowDownIcon, ArrowLeftRightIcon, ArrowUpIcon, CalendarIcon, CheckCheckIcon, CheckIcon, CircleSlashIcon, ClockIcon, CreditCardIcon, Grid2x2XIcon, InfoIcon, Loader2Icon, LockIcon, MoreVerticalIcon, RefreshCcw, RefreshCwIcon, Settings, ShieldIcon, StarsIcon, Trash2Icon, XIcon } from "zudoku/icons";
3
3
  import { Button, ClientOnly, Head, Heading, Link, Slot } from "zudoku/components";
4
4
  import { useAuth, useZudoku } from "zudoku/hooks";
5
5
  import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient, useSuspenseQuery } from "zudoku/react-query";
@@ -19,7 +19,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
19
19
  import { Frame, FrameFooter, FramePanel } from "zudoku/ui/Frame";
20
20
  import { Secret } from "zudoku/ui/Secret";
21
21
  import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "zudoku/ui/AlertDialog";
22
- import { Tooltip, TooltipContent, TooltipTrigger } from "zudoku/ui/Tooltip";
23
22
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "zudoku/ui/Dialog";
24
23
  import { Input } from "zudoku/ui/Input";
25
24
  import { Progress } from "zudoku/ui/Progress";
@@ -80,13 +79,12 @@ const useDeploymentName = () => {
80
79
  };
81
80
 
82
81
  //#endregion
83
- //#region src/hooks/usePlans.ts
84
- const usePlans = () => {
82
+ //#region src/hooks/usePurchaseDetails.ts
83
+ const usePurchaseDetails = (planId) => {
85
84
  const zudoku = useZudoku();
86
- const auth = useAuth();
87
85
  return useSuspenseQuery({
88
- queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/pricing-page`],
89
- meta: { context: auth.isAuthenticated ? zudoku : void 0 }
86
+ queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/plans/${planId}/purchase-details`],
87
+ meta: { context: zudoku }
90
88
  });
91
89
  };
92
90
 
@@ -227,6 +225,24 @@ const getPriceFromPlan = (plan) => {
227
225
  };
228
226
  };
229
227
 
228
+ //#endregion
229
+ //#region src/utils/purchaseDetails.ts
230
+ const getPlanFromPurchaseDetails = (response) => {
231
+ return response;
232
+ };
233
+ const getTaxAmountFromPurchaseDetails = (response) => {
234
+ const taxAmount = response?.tax?.taxAmount;
235
+ const numericAmount = typeof taxAmount === "number" ? taxAmount : Number.parseFloat(taxAmount ?? "");
236
+ if (!Number.isFinite(numericAmount)) return;
237
+ return numericAmount;
238
+ };
239
+ const getTaxLabelFromPurchaseDetails = (response) => {
240
+ return (response.tax?.taxes ?? []).some((tax) => tax.taxType?.toLowerCase() === "vat") ? "VAT" : "tax";
241
+ };
242
+ const isTaxInclusiveFromPurchaseDetails = (response) => {
243
+ return response.tax?.taxInclusive === true;
244
+ };
245
+
230
246
  //#endregion
231
247
  //#region src/ZuploMonetizationWrapper.tsx
232
248
  const DEFAULT_GATEWAY_URL = "https://api.zuploedge.com";
@@ -307,10 +323,13 @@ const CheckoutConfirmPage = () => {
307
323
  const zudoku = useZudoku();
308
324
  const deploymentName = useDeploymentName();
309
325
  const navigate = useNavigate();
310
- const { data: plans } = usePlans();
311
326
  const { pricing } = useMonetizationConfig();
312
- const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
313
327
  if (!planId) throw new Error("Parameter `planId` missing");
328
+ const purchaseDetails = usePurchaseDetails(planId);
329
+ const selectedPlan = getPlanFromPurchaseDetails(purchaseDetails.data);
330
+ const taxAmount = getTaxAmountFromPurchaseDetails(purchaseDetails.data);
331
+ const taxLabel = getTaxLabelFromPurchaseDetails(purchaseDetails.data);
332
+ const taxInclusive = isTaxInclusiveFromPurchaseDetails(purchaseDetails.data);
314
333
  const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
315
334
  const { quotas, features } = categorizeRateCards(rateCards ?? [], {
316
335
  currency: selectedPlan?.currency,
@@ -386,13 +405,20 @@ const CheckoutConfirmPage = () => {
386
405
  }),
387
406
  price && price.monthly > 0 && /* @__PURE__ */ jsxs("div", {
388
407
  className: "text-right",
389
- children: [/* @__PURE__ */ jsx("div", {
390
- className: "text-2xl font-bold",
391
- children: formatPrice(price.monthly, selectedPlan?.currency)
392
- }), billingCycle && /* @__PURE__ */ jsxs("div", {
393
- className: "text-sm text-muted-foreground font-normal",
394
- children: ["Billed ", formatBillingCycle(billingCycle)]
395
- })]
408
+ children: [
409
+ /* @__PURE__ */ jsx("div", {
410
+ className: "text-2xl font-bold",
411
+ children: formatPrice(price.monthly, selectedPlan?.currency)
412
+ }),
413
+ billingCycle && /* @__PURE__ */ jsxs("div", {
414
+ className: "text-sm text-muted-foreground font-normal",
415
+ children: ["Billed ", formatBillingCycle(billingCycle)]
416
+ }),
417
+ taxAmount != null && /* @__PURE__ */ jsx("div", {
418
+ className: "text-xs text-muted-foreground font-normal mt-1",
419
+ children: taxInclusive ? `${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel} included` : `+ ${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel}`
420
+ })
421
+ ]
396
422
  }),
397
423
  price && price.monthly === 0 && /* @__PURE__ */ jsx("div", {
398
424
  className: "text-2xl text-muted-foreground font-bold",
@@ -422,7 +448,7 @@ const CheckoutConfirmPage = () => {
422
448
  children: [/* @__PURE__ */ jsx(Button, {
423
449
  className: "w-full",
424
450
  onClick: () => createSubscriptionMutation.mutate(),
425
- disabled: createSubscriptionMutation.isPending,
451
+ disabled: createSubscriptionMutation.isPending || !selectedPlan,
426
452
  children: createSubscriptionMutation.isPending ? "Processing Payment..." : "Confirm & Subscribe"
427
453
  }), /* @__PURE__ */ jsx(Button, {
428
454
  variant: "ghost",
@@ -606,6 +632,17 @@ const ManagePaymentPage = () => {
606
632
  });
607
633
  };
608
634
 
635
+ //#endregion
636
+ //#region src/hooks/usePlans.ts
637
+ const usePlans = () => {
638
+ const zudoku = useZudoku();
639
+ const auth = useAuth();
640
+ return useSuspenseQuery({
641
+ queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/pricing-page`],
642
+ meta: { context: auth.isAuthenticated ? zudoku : void 0 }
643
+ });
644
+ };
645
+
609
646
  //#endregion
610
647
  //#region src/pages/pricing/PricingCard.tsx
611
648
  const PhaseSection = ({ phase, currency, showName, billingCadence }) => {
@@ -764,14 +801,18 @@ const SubscriptionChangeConfirmPage = () => {
764
801
  const [search] = useSearchParams();
765
802
  const planId = search.get("planId");
766
803
  const subscriptionId = search.get("subscriptionId");
804
+ const mode = search.get("mode");
767
805
  const zudoku = useZudoku();
768
806
  const deploymentName = useDeploymentName();
769
807
  const navigate = useNavigate();
770
- const { data: plans } = usePlans();
771
808
  const { pricing } = useMonetizationConfig();
772
- const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
773
809
  if (!planId) throw new Error("Parameter `planId` missing");
774
810
  if (!subscriptionId) throw new Error("Parameter `subscriptionId` missing");
811
+ const purchaseDetails = usePurchaseDetails(planId);
812
+ const selectedPlan = getPlanFromPurchaseDetails(purchaseDetails.data);
813
+ const taxAmount = getTaxAmountFromPurchaseDetails(purchaseDetails.data);
814
+ const taxLabel = getTaxLabelFromPurchaseDetails(purchaseDetails.data);
815
+ const taxInclusive = isTaxInclusiveFromPurchaseDetails(purchaseDetails.data);
775
816
  const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
776
817
  const { quotas, features } = categorizeRateCards(rateCards ?? [], {
777
818
  currency: selectedPlan?.currency,
@@ -780,6 +821,7 @@ const SubscriptionChangeConfirmPage = () => {
780
821
  });
781
822
  const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
782
823
  const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
824
+ const effectiveChangeMessage = mode === "downgrade" ? "This change will take effect at the start of your next billing cycle." : "This change will take effect immediately.";
783
825
  const changeMutation = useMutation({
784
826
  mutationKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions/${subscriptionId}/change`],
785
827
  meta: {
@@ -814,13 +856,20 @@ const SubscriptionChangeConfirmPage = () => {
814
856
  }),
815
857
  /* @__PURE__ */ jsxs("div", {
816
858
  className: "text-center mb-8",
817
- children: [/* @__PURE__ */ jsx("h1", {
818
- className: "text-2xl font-bold text-card-foreground mb-3",
819
- children: "Confirm plan change"
820
- }), /* @__PURE__ */ jsx("p", {
821
- className: "text-muted-foreground text-base",
822
- children: "Please confirm the details below to change your subscription."
823
- })]
859
+ children: [
860
+ /* @__PURE__ */ jsx("h1", {
861
+ className: "text-2xl font-bold text-card-foreground mb-3",
862
+ children: "Confirm plan change"
863
+ }),
864
+ /* @__PURE__ */ jsx("p", {
865
+ className: "text-muted-foreground text-base",
866
+ children: effectiveChangeMessage
867
+ }),
868
+ /* @__PURE__ */ jsx("p", {
869
+ className: "text-muted-foreground text-base",
870
+ children: "Please confirm the details below to change your subscription."
871
+ })
872
+ ]
824
873
  }),
825
874
  selectedPlan && /* @__PURE__ */ jsxs(Card, {
826
875
  className: "bg-muted/50",
@@ -845,13 +894,20 @@ const SubscriptionChangeConfirmPage = () => {
845
894
  }),
846
895
  price && price.monthly > 0 && /* @__PURE__ */ jsxs("div", {
847
896
  className: "text-right",
848
- children: [/* @__PURE__ */ jsx("div", {
849
- className: "text-2xl font-bold",
850
- children: formatPrice(price.monthly, selectedPlan?.currency)
851
- }), billingCycle && /* @__PURE__ */ jsxs("div", {
852
- className: "text-sm text-muted-foreground font-normal",
853
- children: ["Billed ", formatBillingCycle(billingCycle)]
854
- })]
897
+ children: [
898
+ /* @__PURE__ */ jsx("div", {
899
+ className: "text-2xl font-bold",
900
+ children: formatPrice(price.monthly, selectedPlan?.currency)
901
+ }),
902
+ billingCycle && /* @__PURE__ */ jsxs("div", {
903
+ className: "text-sm text-muted-foreground font-normal",
904
+ children: ["Billed ", formatBillingCycle(billingCycle)]
905
+ }),
906
+ taxAmount != null && /* @__PURE__ */ jsx("div", {
907
+ className: "text-xs text-muted-foreground font-normal mt-1",
908
+ children: taxInclusive ? `${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel} included` : `+ ${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel}`
909
+ })
910
+ ]
855
911
  }),
856
912
  price && price.monthly === 0 && /* @__PURE__ */ jsx("div", {
857
913
  className: "text-2xl text-muted-foreground font-bold",
@@ -1251,15 +1307,24 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1251
1307
  children: [
1252
1308
  /* @__PURE__ */ jsx(CalendarIcon, { className: "size-4" }),
1253
1309
  /* @__PURE__ */ jsx(AlertTitle, { children: "Your plan will be canceled at the end of your billing cycle." }),
1254
- /* @__PURE__ */ jsxs(AlertDescription, { children: ["You'll retain access until ", formatDate$1(billingPeriodEnd)] })
1310
+ /* @__PURE__ */ jsxs(AlertDescription, { children: [
1311
+ "You'll retain access until ",
1312
+ formatDate$1(billingPeriodEnd),
1313
+ ". After your billing period ends, this plan will not renew and you would need to subscribe again to continue."
1314
+ ] })
1255
1315
  ]
1256
1316
  }),
1257
1317
  /* @__PURE__ */ jsxs(Alert, {
1258
- variant: "destructive",
1318
+ variant: "info",
1259
1319
  children: [
1260
- /* @__PURE__ */ jsx(CircleAlert, { className: "size-4" }),
1261
- /* @__PURE__ */ jsx(AlertTitle, { children: "This action cannot be undone" }),
1262
- /* @__PURE__ */ jsx(AlertDescription, { children: "Once cancelled, you will not be able to recover this plan or its associated settings. You would need to subscribe again." })
1320
+ /* @__PURE__ */ jsx(InfoIcon, { className: "size-4" }),
1321
+ /* @__PURE__ */ jsx(AlertTitle, { children: "You can still resume before then" }),
1322
+ /* @__PURE__ */ jsxs(AlertDescription, { children: [
1323
+ "If you change your mind you have until",
1324
+ " ",
1325
+ formatDate$1(billingPeriodEnd),
1326
+ " to remove this cancellation from Manage subscription."
1327
+ ] })
1263
1328
  ]
1264
1329
  }),
1265
1330
  /* @__PURE__ */ jsxs("div", {
@@ -1312,6 +1377,88 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1312
1377
  });
1313
1378
  };
1314
1379
 
1380
+ //#endregion
1381
+ //#region src/pages/subscriptions/RestoreSubscriptionDialog.tsx
1382
+ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId, billingPeriodEnd }) => {
1383
+ const deploymentName = useDeploymentName();
1384
+ const context = useZudoku();
1385
+ const queryClient = useQueryClient();
1386
+ const restoreSubscriptionMutation = useMutation({
1387
+ mutationKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions/${subscriptionId}/restore`],
1388
+ meta: {
1389
+ context,
1390
+ request: { method: "POST" }
1391
+ },
1392
+ onSuccess: async () => {
1393
+ await queryClient.invalidateQueries();
1394
+ onOpenChange(false);
1395
+ }
1396
+ });
1397
+ useEffect(() => {
1398
+ if (open) restoreSubscriptionMutation.reset();
1399
+ }, [open, restoreSubscriptionMutation]);
1400
+ const handleOpenChange = (nextOpen) => {
1401
+ if (!nextOpen) restoreSubscriptionMutation.reset();
1402
+ onOpenChange(nextOpen);
1403
+ };
1404
+ return /* @__PURE__ */ jsx(Dialog, {
1405
+ open,
1406
+ onOpenChange: handleOpenChange,
1407
+ children: /* @__PURE__ */ jsxs(DialogContent, {
1408
+ className: "sm:max-w-md",
1409
+ children: [
1410
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogTitle, { children: "Resume subscription" }), /* @__PURE__ */ jsxs(DialogDescription, { children: [
1411
+ "You scheduled ",
1412
+ /* @__PURE__ */ jsx("span", {
1413
+ className: "font-medium",
1414
+ children: planName
1415
+ }),
1416
+ " to end. You can still change your mind before the current billing period ends."
1417
+ ] })] }),
1418
+ /* @__PURE__ */ jsxs("div", {
1419
+ className: "space-y-4 mt-4",
1420
+ children: [/* @__PURE__ */ jsxs(Alert, {
1421
+ variant: "info",
1422
+ children: [
1423
+ /* @__PURE__ */ jsx(CalendarIcon, { className: "size-4" }),
1424
+ /* @__PURE__ */ jsx(AlertTitle, { children: "What happens if you resume" }),
1425
+ /* @__PURE__ */ jsxs(AlertDescription, {
1426
+ className: "space-y-2",
1427
+ children: [/* @__PURE__ */ jsxs("p", { children: [
1428
+ "Your access stays in place until ",
1429
+ formatDate$1(billingPeriodEnd),
1430
+ " ",
1431
+ "either way."
1432
+ ] }), /* @__PURE__ */ jsx("p", { children: "Confirming will remove the pending cancellation. Your subscription will remain active and continue to renew on your normal billing schedule, and charges will apply as usual." })]
1433
+ })
1434
+ ]
1435
+ }), restoreSubscriptionMutation.isError && /* @__PURE__ */ jsxs(Alert, {
1436
+ variant: "destructive",
1437
+ children: [
1438
+ /* @__PURE__ */ jsx(CircleSlashIcon, { className: "size-4" }),
1439
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Could not resume subscription" }),
1440
+ /* @__PURE__ */ jsx(AlertDescription, { children: restoreSubscriptionMutation.error.message })
1441
+ ]
1442
+ })]
1443
+ }),
1444
+ /* @__PURE__ */ jsxs("div", {
1445
+ className: "flex flex-col gap-2",
1446
+ children: [/* @__PURE__ */ jsx(ActionButton, {
1447
+ disabled: restoreSubscriptionMutation.isPending,
1448
+ isPending: restoreSubscriptionMutation.isPending || restoreSubscriptionMutation.isSuccess,
1449
+ onClick: () => restoreSubscriptionMutation.mutate(),
1450
+ children: "Resume subscription"
1451
+ }), /* @__PURE__ */ jsx(Button$1, {
1452
+ variant: "ghost",
1453
+ onClick: () => handleOpenChange(false),
1454
+ children: "Keep cancellation"
1455
+ })]
1456
+ })
1457
+ ]
1458
+ })
1459
+ });
1460
+ };
1461
+
1315
1462
  //#endregion
1316
1463
  //#region src/pages/subscriptions/SwitchPlanModal.tsx
1317
1464
  const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units) => {
@@ -1415,7 +1562,12 @@ const modeLabelMap = {
1415
1562
  downgrade: "Downgrade",
1416
1563
  private: "Switch"
1417
1564
  };
1418
- const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange }) => {
1565
+ const isSwitchPlanTarget = (value) => {
1566
+ if (typeof value !== "object" || value === null) return false;
1567
+ if (!("subscriptionId" in value) || !("plan" in value) || !("mode" in value)) return false;
1568
+ return true;
1569
+ };
1570
+ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange, isSwitching }) => {
1419
1571
  const price = getPriceFromPlan(comparison.plan);
1420
1572
  const isCustom = comparison.plan.key === "enterprise";
1421
1573
  const displayPrice = price.monthly;
@@ -1455,6 +1607,7 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
1455
1607
  mode
1456
1608
  }),
1457
1609
  size: "sm",
1610
+ disabled: isSwitching,
1458
1611
  children: modeLabelMap[mode]
1459
1612
  })]
1460
1613
  }), hasChanges && /* @__PURE__ */ jsxs("div", {
@@ -1549,24 +1702,32 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
1549
1702
  })]
1550
1703
  });
1551
1704
  };
1552
- const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
1705
+ const SwitchPlanModal = ({ subscription, children }) => {
1706
+ const [open, setOpen] = useState(false);
1707
+ const { data: plansData } = usePlans();
1708
+ const { pricing } = useMonetizationConfig();
1553
1709
  const deploymentName = useDeploymentName();
1554
1710
  const context = useZudoku();
1555
1711
  const { generateUrl } = useUrlUtils();
1556
- const mutation = useMutation({
1712
+ const switchPlanMutation = useMutation({
1557
1713
  mutationKey: [`/v3/zudoku-metering/${deploymentName}/stripe/checkout`],
1558
1714
  meta: {
1559
1715
  context,
1560
- request: {
1561
- method: "POST",
1562
- body: JSON.stringify({
1563
- planId: switchTo.plan.id,
1564
- successURL: generateUrl(`/subscription-change-confirm`, { searchParams: {
1716
+ request: (variables) => {
1717
+ if (!isSwitchPlanTarget(variables)) throw new Error("Couldn't start the plan change. Please refresh and try again.");
1718
+ const switchTo = variables;
1719
+ return {
1720
+ method: "POST",
1721
+ body: JSON.stringify({
1565
1722
  planId: switchTo.plan.id,
1566
- subscriptionId: switchTo.subscriptionId
1567
- } }),
1568
- cancelURL: generateUrl("/subscriptions", { searchParams: { subscriptionId: switchTo.subscriptionId } })
1569
- })
1723
+ successURL: generateUrl(`/subscription-change-confirm`, { searchParams: {
1724
+ planId: switchTo.plan.id,
1725
+ subscriptionId: switchTo.subscriptionId,
1726
+ mode: switchTo.mode
1727
+ } }),
1728
+ cancelURL: generateUrl("/subscriptions", { searchParams: { subscriptionId: switchTo.subscriptionId } })
1729
+ })
1730
+ };
1570
1731
  }
1571
1732
  },
1572
1733
  retry: false,
@@ -1574,38 +1735,6 @@ const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
1574
1735
  window.location.href = data.url;
1575
1736
  }
1576
1737
  });
1577
- return /* @__PURE__ */ jsx(AlertDialog, {
1578
- open: true,
1579
- onOpenChange: onRequestClose,
1580
- children: /* @__PURE__ */ jsxs(AlertDialogContent, { children: [/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [
1581
- /* @__PURE__ */ jsxs(AlertDialogTitle, { children: [
1582
- "Confirm",
1583
- " ",
1584
- switchTo.mode === "private" ? "plan change" : switchTo.mode === "upgrade" ? "upgrade" : "downgrade"
1585
- ] }),
1586
- mutation.isError && /* @__PURE__ */ jsx(Alert, {
1587
- variant: "destructive",
1588
- children: /* @__PURE__ */ jsx(AlertDescription, {
1589
- className: "first-letter:uppercase",
1590
- children: mutation.error.message
1591
- })
1592
- }),
1593
- /* @__PURE__ */ jsx(AlertDialogDescription, { children: switchTo.mode === "private" ? `Are you sure you want to switch to ${switchTo.plan.name}? This will take effect immediately.` : switchTo.mode === "upgrade" ? `Are you sure you want to upgrade to ${switchTo.plan.name}? This will take effect immediately.` : `Are you sure you want to downgrade to ${switchTo.plan.name}? This will take effect at the start of your next billing cycle.` })
1594
- ] }), /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [/* @__PURE__ */ jsx(AlertDialogCancel, {
1595
- disabled: mutation.isPending,
1596
- children: "Cancel"
1597
- }), /* @__PURE__ */ jsx(ActionButton, {
1598
- isPending: mutation.isPending,
1599
- onClick: () => mutation.mutate(),
1600
- children: modeLabelMap[switchTo.mode]
1601
- })] })] })
1602
- });
1603
- };
1604
- const SwitchPlanModal = ({ subscription, children }) => {
1605
- const [open, setOpen] = useState(false);
1606
- const { data: plansData } = usePlans();
1607
- const [switchTo, setSwitchTo] = useState(null);
1608
- const { pricing } = useMonetizationConfig();
1609
1738
  const currentPlan = plansData?.items.find((p) => p.id === subscription.plan.id);
1610
1739
  const { upgrades, downgrades, privatePlans } = useMemo(() => {
1611
1740
  if (!plansData?.items || !currentPlan) return {
@@ -1628,10 +1757,7 @@ const SwitchPlanModal = ({ subscription, children }) => {
1628
1757
  currentPlan,
1629
1758
  pricing?.units
1630
1759
  ]);
1631
- return /* @__PURE__ */ jsxs(Fragment, { children: [switchTo !== null && /* @__PURE__ */ jsx(ConfirmSwitchAlert, {
1632
- switchTo,
1633
- onRequestClose: () => setSwitchTo(null)
1634
- }), /* @__PURE__ */ jsxs(Dialog, {
1760
+ return /* @__PURE__ */ jsxs(Dialog, {
1635
1761
  open,
1636
1762
  onOpenChange: setOpen,
1637
1763
  children: [/* @__PURE__ */ jsx(DialogTrigger, {
@@ -1652,6 +1778,13 @@ const SwitchPlanModal = ({ subscription, children }) => {
1652
1778
  }), /* @__PURE__ */ jsxs("div", {
1653
1779
  className: "mt-4 space-y-6",
1654
1780
  children: [
1781
+ switchPlanMutation.isError && /* @__PURE__ */ jsx(Alert, {
1782
+ variant: "destructive",
1783
+ children: /* @__PURE__ */ jsx(AlertDescription, {
1784
+ className: "first-letter:uppercase",
1785
+ children: switchPlanMutation.error.message
1786
+ })
1787
+ }),
1655
1788
  currentPlan && /* @__PURE__ */ jsx(Item, {
1656
1789
  variant: "outline",
1657
1790
  children: /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, { children: "Current Plan" }), /* @__PURE__ */ jsx(ItemDescription, {
@@ -1677,7 +1810,8 @@ const SwitchPlanModal = ({ subscription, children }) => {
1677
1810
  comparison,
1678
1811
  subscriptionId: subscription.id,
1679
1812
  mode: "upgrade",
1680
- onRequestChange: setSwitchTo
1813
+ onRequestChange: (target) => switchPlanMutation.mutate(target),
1814
+ isSwitching: switchPlanMutation.isPending
1681
1815
  }, comparison.plan.id))
1682
1816
  })] }),
1683
1817
  downgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
@@ -1698,7 +1832,8 @@ const SwitchPlanModal = ({ subscription, children }) => {
1698
1832
  comparison,
1699
1833
  subscriptionId: subscription.id,
1700
1834
  mode: "downgrade",
1701
- onRequestChange: setSwitchTo
1835
+ onRequestChange: (target) => switchPlanMutation.mutate(target),
1836
+ isSwitching: switchPlanMutation.isPending
1702
1837
  }, comparison.plan.id))
1703
1838
  })] }),
1704
1839
  privatePlans.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
@@ -1719,94 +1854,106 @@ const SwitchPlanModal = ({ subscription, children }) => {
1719
1854
  comparison,
1720
1855
  subscriptionId: subscription.id,
1721
1856
  mode: "private",
1722
- onRequestChange: setSwitchTo
1857
+ onRequestChange: (target) => switchPlanMutation.mutate(target),
1858
+ isSwitching: switchPlanMutation.isPending
1723
1859
  }, comparison.plan.id))
1724
1860
  })] })
1725
1861
  ]
1726
1862
  })]
1727
1863
  }) })]
1728
- })] });
1864
+ });
1729
1865
  };
1730
1866
 
1731
1867
  //#endregion
1732
1868
  //#region src/pages/subscriptions/ManageSubscription.tsx
1733
1869
  const ManageSubscription = ({ subscription, planName }) => {
1734
1870
  const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
1735
- return /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CancelSubscriptionDialog, {
1736
- open: cancelDialogOpen,
1737
- onOpenChange: setCancelDialogOpen,
1738
- planName,
1739
- subscriptionId: subscription.id,
1740
- billingPeriodEnd: subscription.alignment.currentAlignedBillingPeriod.to
1741
- }), /* @__PURE__ */ jsx(CardContent, {
1742
- className: "p-6",
1743
- children: /* @__PURE__ */ jsxs("div", {
1744
- className: "flex gap-4",
1745
- children: [/* @__PURE__ */ jsx("div", {
1746
- className: "flex items-center justify-center w-12 h-12 rounded-full bg-primary/10 shrink-0",
1747
- children: /* @__PURE__ */ jsx(Settings, { className: "w-6 h-6 text-primary" })
1748
- }), /* @__PURE__ */ jsxs("div", {
1749
- className: "flex-1",
1750
- id: "manage",
1751
- children: [
1752
- /* @__PURE__ */ jsx("h2", {
1753
- className: "text-lg font-semibold text-foreground mb-1",
1754
- children: "Manage Subscription"
1755
- }),
1756
- /* @__PURE__ */ jsx("p", {
1757
- className: "text-sm text-muted-foreground mb-4",
1758
- children: "Switch to a different plan or cancel your current subscription."
1759
- }),
1760
- /* @__PURE__ */ jsxs("div", {
1761
- className: "flex flex-wrap gap-3",
1762
- children: [
1763
- subscription.status === "canceled" && /* @__PURE__ */ jsx(Button$1, {
1764
- variant: "outline",
1765
- size: "sm",
1766
- asChild: true,
1767
- children: /* @__PURE__ */ jsxs(Link, {
1768
- to: "/pricing",
1769
- children: [/* @__PURE__ */ jsx(RefreshCcw, { className: "w-4 h-4 mr-2" }), "New subscription"]
1770
- })
1771
- }),
1772
- subscription.status === "active" && /* @__PURE__ */ jsx(SwitchPlanModal, { subscription }),
1773
- /* @__PURE__ */ jsxs(Tooltip, {
1774
- delayDuration: 0,
1775
- children: [/* @__PURE__ */ jsx(TooltipTrigger, {
1871
+ const [restoreDialogOpen, setRestoreDialogOpen] = useState(false);
1872
+ const billingPeriodEnd = subscription.alignment.currentAlignedBillingPeriod.to;
1873
+ const canResumeCanceledSubscription = subscription.status === "canceled" && new Date(billingPeriodEnd) > /* @__PURE__ */ new Date();
1874
+ return /* @__PURE__ */ jsxs(Card, { children: [
1875
+ /* @__PURE__ */ jsx(CancelSubscriptionDialog, {
1876
+ open: cancelDialogOpen,
1877
+ onOpenChange: setCancelDialogOpen,
1878
+ planName,
1879
+ subscriptionId: subscription.id,
1880
+ billingPeriodEnd
1881
+ }),
1882
+ /* @__PURE__ */ jsx(RestoreSubscriptionDialog, {
1883
+ open: restoreDialogOpen,
1884
+ onOpenChange: setRestoreDialogOpen,
1885
+ planName,
1886
+ subscriptionId: subscription.id,
1887
+ billingPeriodEnd
1888
+ }),
1889
+ /* @__PURE__ */ jsx(CardContent, {
1890
+ className: "p-6",
1891
+ children: /* @__PURE__ */ jsxs("div", {
1892
+ className: "flex gap-4",
1893
+ children: [/* @__PURE__ */ jsx("div", {
1894
+ className: "flex items-center justify-center w-12 h-12 rounded-full bg-primary/10 shrink-0",
1895
+ children: /* @__PURE__ */ jsx(Settings, { className: "w-6 h-6 text-primary" })
1896
+ }), /* @__PURE__ */ jsxs("div", {
1897
+ className: "flex-1",
1898
+ id: "manage",
1899
+ children: [
1900
+ /* @__PURE__ */ jsx("h2", {
1901
+ className: "text-lg font-semibold text-foreground mb-1",
1902
+ children: "Manage Subscription"
1903
+ }),
1904
+ /* @__PURE__ */ jsx("p", {
1905
+ className: "text-sm text-muted-foreground mb-4",
1906
+ children: "Switch to a different plan or cancel your current subscription."
1907
+ }),
1908
+ /* @__PURE__ */ jsxs("div", {
1909
+ className: "flex flex-wrap gap-3",
1910
+ children: [
1911
+ subscription.status === "canceled" && /* @__PURE__ */ jsx(Button$1, {
1912
+ variant: "outline",
1913
+ size: "sm",
1776
1914
  asChild: true,
1777
- children: /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button$1, {
1778
- variant: "outline",
1779
- size: "sm",
1780
- onClick: () => setCancelDialogOpen(true),
1781
- title: "You can only cancel your subscription if it is not active.",
1782
- disabled: subscription.status !== "active",
1783
- children: "Cancel subscription"
1784
- }) })
1785
- }), subscription.status === "canceled" && /* @__PURE__ */ jsx(TooltipContent, { children: "Your subscription is already cancelled." })]
1786
- }),
1787
- /* @__PURE__ */ jsx(Button$1, {
1788
- asChild: true,
1789
- size: "sm",
1790
- variant: "secondary",
1791
- children: /* @__PURE__ */ jsx(Link, {
1792
- to: "/manage-payment",
1793
- children: /* @__PURE__ */ jsxs("div", {
1794
- className: "flex items-center gap-2",
1795
- children: [/* @__PURE__ */ jsx(CreditCardIcon, {}), "Manage payment details"]
1915
+ children: /* @__PURE__ */ jsxs(Link, {
1916
+ to: "/pricing",
1917
+ children: [/* @__PURE__ */ jsx(RefreshCcw, { className: "w-4 h-4 mr-2" }), "New subscription"]
1918
+ })
1919
+ }),
1920
+ subscription.status === "active" && /* @__PURE__ */ jsx(SwitchPlanModal, { subscription }),
1921
+ subscription.status === "active" && /* @__PURE__ */ jsx(Button$1, {
1922
+ variant: "outline",
1923
+ size: "sm",
1924
+ onClick: () => setCancelDialogOpen(true),
1925
+ children: "Cancel subscription"
1926
+ }),
1927
+ canResumeCanceledSubscription && /* @__PURE__ */ jsx(Button$1, {
1928
+ variant: "outline",
1929
+ size: "sm",
1930
+ onClick: () => setRestoreDialogOpen(true),
1931
+ children: "Resume subscription"
1932
+ }),
1933
+ /* @__PURE__ */ jsx(Button$1, {
1934
+ asChild: true,
1935
+ size: "sm",
1936
+ variant: "secondary",
1937
+ children: /* @__PURE__ */ jsx(Link, {
1938
+ to: "/manage-payment",
1939
+ children: /* @__PURE__ */ jsxs("div", {
1940
+ className: "flex items-center gap-2",
1941
+ children: [/* @__PURE__ */ jsx(CreditCardIcon, {}), "Manage payment details"]
1942
+ })
1796
1943
  })
1797
1944
  })
1798
- })
1799
- ]
1800
- }),
1801
- /* @__PURE__ */ jsx(Separator, { className: "my-4" }),
1802
- /* @__PURE__ */ jsx("span", {
1803
- className: "text-sm text-muted-foreground",
1804
- children: "Your payment is securely managed by Stripe."
1805
- })
1806
- ]
1807
- })]
1945
+ ]
1946
+ }),
1947
+ /* @__PURE__ */ jsx(Separator, { className: "my-4" }),
1948
+ /* @__PURE__ */ jsx("span", {
1949
+ className: "text-sm text-muted-foreground",
1950
+ children: "Your payment is securely managed by Stripe."
1951
+ })
1952
+ ]
1953
+ })]
1954
+ })
1808
1955
  })
1809
- })] });
1956
+ ] });
1810
1957
  };
1811
1958
 
1812
1959
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuplo/zudoku-plugin-monetization",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
@@ -31,7 +31,7 @@
31
31
  "react": "19.2.4",
32
32
  "react-dom": "19.2.4",
33
33
  "tsdown": "0.20.3",
34
- "zudoku": "0.71.10"
34
+ "zudoku": "0.73.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=19.2.0",