@zuplo/zudoku-plugin-monetization 0.0.26 → 0.0.27

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 +224 -99
  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";
@@ -79,17 +78,6 @@ const useDeploymentName = () => {
79
78
  return deploymentName;
80
79
  };
81
80
 
82
- //#endregion
83
- //#region src/hooks/usePlans.ts
84
- const usePlans = () => {
85
- const zudoku = useZudoku();
86
- const auth = useAuth();
87
- return useSuspenseQuery({
88
- queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/pricing-page`],
89
- meta: { context: auth.isAuthenticated ? zudoku : void 0 }
90
- });
91
- };
92
-
93
81
  //#endregion
94
82
  //#region src/MonetizationContext.tsx
95
83
  const MonetizationContext = createContext({});
@@ -301,16 +289,29 @@ const ZuploMonetizationWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(Query
301
289
 
302
290
  //#endregion
303
291
  //#region src/pages/CheckoutConfirmPage.tsx
292
+ const getPlanFromPurchaseDetails = (response) => {
293
+ return "plan" in response ? response.plan : response;
294
+ };
295
+ const getTaxAmountFromPurchaseDetails = (response) => {
296
+ const taxAmount = response?.tax?.amount;
297
+ const numericAmount = typeof taxAmount === "number" ? taxAmount : Number.parseFloat(taxAmount ?? "");
298
+ if (!Number.isFinite(numericAmount)) return;
299
+ return numericAmount;
300
+ };
304
301
  const CheckoutConfirmPage = () => {
305
302
  const [search] = useSearchParams();
306
303
  const planId = search.get("planId");
307
304
  const zudoku = useZudoku();
308
305
  const deploymentName = useDeploymentName();
309
306
  const navigate = useNavigate();
310
- const { data: plans } = usePlans();
311
307
  const { pricing } = useMonetizationConfig();
312
- const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
313
308
  if (!planId) throw new Error("Parameter `planId` missing");
309
+ const purchaseDetails = useSuspenseQuery({
310
+ queryKey: [`/v3/zudoku-metering/${deploymentName}/plans/${planId}/purchase-details`],
311
+ meta: { context: zudoku }
312
+ });
313
+ const selectedPlan = getPlanFromPurchaseDetails(purchaseDetails.data);
314
+ const taxAmount = getTaxAmountFromPurchaseDetails(purchaseDetails.data);
314
315
  const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
315
316
  const { quotas, features } = categorizeRateCards(rateCards ?? [], {
316
317
  currency: selectedPlan?.currency,
@@ -386,13 +387,24 @@ const CheckoutConfirmPage = () => {
386
387
  }),
387
388
  price && price.monthly > 0 && /* @__PURE__ */ jsxs("div", {
388
389
  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
- })]
390
+ children: [
391
+ /* @__PURE__ */ jsx("div", {
392
+ className: "text-2xl font-bold",
393
+ children: formatPrice(price.monthly, selectedPlan?.currency)
394
+ }),
395
+ billingCycle && /* @__PURE__ */ jsxs("div", {
396
+ className: "text-sm text-muted-foreground font-normal",
397
+ children: ["Billed ", formatBillingCycle(billingCycle)]
398
+ }),
399
+ taxAmount != null && /* @__PURE__ */ jsxs("div", {
400
+ className: "text-xs text-muted-foreground font-normal mt-1",
401
+ children: [
402
+ "+ ",
403
+ formatPrice(taxAmount, selectedPlan?.currency),
404
+ " VAT"
405
+ ]
406
+ })
407
+ ]
396
408
  }),
397
409
  price && price.monthly === 0 && /* @__PURE__ */ jsx("div", {
398
410
  className: "text-2xl text-muted-foreground font-bold",
@@ -422,7 +434,7 @@ const CheckoutConfirmPage = () => {
422
434
  children: [/* @__PURE__ */ jsx(Button, {
423
435
  className: "w-full",
424
436
  onClick: () => createSubscriptionMutation.mutate(),
425
- disabled: createSubscriptionMutation.isPending,
437
+ disabled: createSubscriptionMutation.isPending || !selectedPlan,
426
438
  children: createSubscriptionMutation.isPending ? "Processing Payment..." : "Confirm & Subscribe"
427
439
  }), /* @__PURE__ */ jsx(Button, {
428
440
  variant: "ghost",
@@ -606,6 +618,17 @@ const ManagePaymentPage = () => {
606
618
  });
607
619
  };
608
620
 
621
+ //#endregion
622
+ //#region src/hooks/usePlans.ts
623
+ const usePlans = () => {
624
+ const zudoku = useZudoku();
625
+ const auth = useAuth();
626
+ return useSuspenseQuery({
627
+ queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/pricing-page`],
628
+ meta: { context: auth.isAuthenticated ? zudoku : void 0 }
629
+ });
630
+ };
631
+
609
632
  //#endregion
610
633
  //#region src/pages/pricing/PricingCard.tsx
611
634
  const PhaseSection = ({ phase, currency, showName, billingCadence }) => {
@@ -1251,15 +1274,24 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1251
1274
  children: [
1252
1275
  /* @__PURE__ */ jsx(CalendarIcon, { className: "size-4" }),
1253
1276
  /* @__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)] })
1277
+ /* @__PURE__ */ jsxs(AlertDescription, { children: [
1278
+ "You'll retain access until ",
1279
+ formatDate$1(billingPeriodEnd),
1280
+ ". After your billing period ends, this plan will not renew and you would need to subscribe again to continue."
1281
+ ] })
1255
1282
  ]
1256
1283
  }),
1257
1284
  /* @__PURE__ */ jsxs(Alert, {
1258
- variant: "destructive",
1285
+ variant: "info",
1259
1286
  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." })
1287
+ /* @__PURE__ */ jsx(InfoIcon, { className: "size-4" }),
1288
+ /* @__PURE__ */ jsx(AlertTitle, { children: "You can still resume before then" }),
1289
+ /* @__PURE__ */ jsxs(AlertDescription, { children: [
1290
+ "If you change your mind you have until",
1291
+ " ",
1292
+ formatDate$1(billingPeriodEnd),
1293
+ " to remove this cancellation from Manage subscription."
1294
+ ] })
1263
1295
  ]
1264
1296
  }),
1265
1297
  /* @__PURE__ */ jsxs("div", {
@@ -1312,6 +1344,88 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1312
1344
  });
1313
1345
  };
1314
1346
 
1347
+ //#endregion
1348
+ //#region src/pages/subscriptions/RestoreSubscriptionDialog.tsx
1349
+ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId, billingPeriodEnd }) => {
1350
+ const deploymentName = useDeploymentName();
1351
+ const context = useZudoku();
1352
+ const queryClient = useQueryClient();
1353
+ const restoreSubscriptionMutation = useMutation({
1354
+ mutationKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions/${subscriptionId}/restore`],
1355
+ meta: {
1356
+ context,
1357
+ request: { method: "POST" }
1358
+ },
1359
+ onSuccess: async () => {
1360
+ await queryClient.invalidateQueries();
1361
+ onOpenChange(false);
1362
+ }
1363
+ });
1364
+ useEffect(() => {
1365
+ if (open) restoreSubscriptionMutation.reset();
1366
+ }, [open, restoreSubscriptionMutation]);
1367
+ const handleOpenChange = (nextOpen) => {
1368
+ if (!nextOpen) restoreSubscriptionMutation.reset();
1369
+ onOpenChange(nextOpen);
1370
+ };
1371
+ return /* @__PURE__ */ jsx(Dialog, {
1372
+ open,
1373
+ onOpenChange: handleOpenChange,
1374
+ children: /* @__PURE__ */ jsxs(DialogContent, {
1375
+ className: "sm:max-w-md",
1376
+ children: [
1377
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogTitle, { children: "Resume subscription" }), /* @__PURE__ */ jsxs(DialogDescription, { children: [
1378
+ "You scheduled ",
1379
+ /* @__PURE__ */ jsx("span", {
1380
+ className: "font-medium",
1381
+ children: planName
1382
+ }),
1383
+ " to end. You can still change your mind before the current billing period ends."
1384
+ ] })] }),
1385
+ /* @__PURE__ */ jsxs("div", {
1386
+ className: "space-y-4 mt-4",
1387
+ children: [/* @__PURE__ */ jsxs(Alert, {
1388
+ variant: "info",
1389
+ children: [
1390
+ /* @__PURE__ */ jsx(CalendarIcon, { className: "size-4" }),
1391
+ /* @__PURE__ */ jsx(AlertTitle, { children: "What happens if you resume" }),
1392
+ /* @__PURE__ */ jsxs(AlertDescription, {
1393
+ className: "space-y-2",
1394
+ children: [/* @__PURE__ */ jsxs("p", { children: [
1395
+ "Your access stays in place until ",
1396
+ formatDate$1(billingPeriodEnd),
1397
+ " ",
1398
+ "either way."
1399
+ ] }), /* @__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." })]
1400
+ })
1401
+ ]
1402
+ }), restoreSubscriptionMutation.isError && /* @__PURE__ */ jsxs(Alert, {
1403
+ variant: "destructive",
1404
+ children: [
1405
+ /* @__PURE__ */ jsx(CircleSlashIcon, { className: "size-4" }),
1406
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Could not resume subscription" }),
1407
+ /* @__PURE__ */ jsx(AlertDescription, { children: restoreSubscriptionMutation.error.message })
1408
+ ]
1409
+ })]
1410
+ }),
1411
+ /* @__PURE__ */ jsxs("div", {
1412
+ className: "flex flex-col gap-2",
1413
+ children: [/* @__PURE__ */ jsx(ActionButton, {
1414
+ disabled: restoreSubscriptionMutation.isPending,
1415
+ isPending: restoreSubscriptionMutation.isPending || restoreSubscriptionMutation.isSuccess,
1416
+ onClick: () => restoreSubscriptionMutation.mutate(),
1417
+ children: "Resume subscription"
1418
+ }), /* @__PURE__ */ jsx(Button$1, {
1419
+ variant: "ghost",
1420
+ onClick: () => handleOpenChange(false),
1421
+ children: "Keep cancellation"
1422
+ })]
1423
+ })
1424
+ ]
1425
+ })
1426
+ });
1427
+ };
1428
+
1315
1429
  //#endregion
1316
1430
  //#region src/pages/subscriptions/SwitchPlanModal.tsx
1317
1431
  const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units) => {
@@ -1732,81 +1846,92 @@ const SwitchPlanModal = ({ subscription, children }) => {
1732
1846
  //#region src/pages/subscriptions/ManageSubscription.tsx
1733
1847
  const ManageSubscription = ({ subscription, planName }) => {
1734
1848
  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, {
1849
+ const [restoreDialogOpen, setRestoreDialogOpen] = useState(false);
1850
+ const billingPeriodEnd = subscription.alignment.currentAlignedBillingPeriod.to;
1851
+ const canResumeCanceledSubscription = subscription.status === "canceled" && new Date(billingPeriodEnd) > /* @__PURE__ */ new Date();
1852
+ return /* @__PURE__ */ jsxs(Card, { children: [
1853
+ /* @__PURE__ */ jsx(CancelSubscriptionDialog, {
1854
+ open: cancelDialogOpen,
1855
+ onOpenChange: setCancelDialogOpen,
1856
+ planName,
1857
+ subscriptionId: subscription.id,
1858
+ billingPeriodEnd
1859
+ }),
1860
+ /* @__PURE__ */ jsx(RestoreSubscriptionDialog, {
1861
+ open: restoreDialogOpen,
1862
+ onOpenChange: setRestoreDialogOpen,
1863
+ planName,
1864
+ subscriptionId: subscription.id,
1865
+ billingPeriodEnd
1866
+ }),
1867
+ /* @__PURE__ */ jsx(CardContent, {
1868
+ className: "p-6",
1869
+ children: /* @__PURE__ */ jsxs("div", {
1870
+ className: "flex gap-4",
1871
+ children: [/* @__PURE__ */ jsx("div", {
1872
+ className: "flex items-center justify-center w-12 h-12 rounded-full bg-primary/10 shrink-0",
1873
+ children: /* @__PURE__ */ jsx(Settings, { className: "w-6 h-6 text-primary" })
1874
+ }), /* @__PURE__ */ jsxs("div", {
1875
+ className: "flex-1",
1876
+ id: "manage",
1877
+ children: [
1878
+ /* @__PURE__ */ jsx("h2", {
1879
+ className: "text-lg font-semibold text-foreground mb-1",
1880
+ children: "Manage Subscription"
1881
+ }),
1882
+ /* @__PURE__ */ jsx("p", {
1883
+ className: "text-sm text-muted-foreground mb-4",
1884
+ children: "Switch to a different plan or cancel your current subscription."
1885
+ }),
1886
+ /* @__PURE__ */ jsxs("div", {
1887
+ className: "flex flex-wrap gap-3",
1888
+ children: [
1889
+ subscription.status === "canceled" && /* @__PURE__ */ jsx(Button$1, {
1890
+ variant: "outline",
1891
+ size: "sm",
1776
1892
  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"]
1893
+ children: /* @__PURE__ */ jsxs(Link, {
1894
+ to: "/pricing",
1895
+ children: [/* @__PURE__ */ jsx(RefreshCcw, { className: "w-4 h-4 mr-2" }), "New subscription"]
1896
+ })
1897
+ }),
1898
+ subscription.status === "active" && /* @__PURE__ */ jsx(SwitchPlanModal, { subscription }),
1899
+ subscription.status === "active" && /* @__PURE__ */ jsx(Button$1, {
1900
+ variant: "outline",
1901
+ size: "sm",
1902
+ onClick: () => setCancelDialogOpen(true),
1903
+ children: "Cancel subscription"
1904
+ }),
1905
+ canResumeCanceledSubscription && /* @__PURE__ */ jsx(Button$1, {
1906
+ variant: "outline",
1907
+ size: "sm",
1908
+ onClick: () => setRestoreDialogOpen(true),
1909
+ children: "Resume subscription"
1910
+ }),
1911
+ /* @__PURE__ */ jsx(Button$1, {
1912
+ asChild: true,
1913
+ size: "sm",
1914
+ variant: "secondary",
1915
+ children: /* @__PURE__ */ jsx(Link, {
1916
+ to: "/manage-payment",
1917
+ children: /* @__PURE__ */ jsxs("div", {
1918
+ className: "flex items-center gap-2",
1919
+ children: [/* @__PURE__ */ jsx(CreditCardIcon, {}), "Manage payment details"]
1920
+ })
1796
1921
  })
1797
1922
  })
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
- })]
1923
+ ]
1924
+ }),
1925
+ /* @__PURE__ */ jsx(Separator, { className: "my-4" }),
1926
+ /* @__PURE__ */ jsx("span", {
1927
+ className: "text-sm text-muted-foreground",
1928
+ children: "Your payment is securely managed by Stripe."
1929
+ })
1930
+ ]
1931
+ })]
1932
+ })
1808
1933
  })
1809
- })] });
1934
+ ] });
1810
1935
  };
1811
1936
 
1812
1937
  //#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.27",
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.72.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=19.2.0",