@zuplo/zudoku-plugin-monetization 0.0.27 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import * as zudoku from "zudoku";
2
1
  import "react";
2
+ import * as zudoku from "zudoku";
3
3
 
4
4
  //#region src/MonetizationContext.d.ts
5
5
  interface MonetizationConfig {
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { Suspense, createContext, use, useEffect, useMemo, useState } from "react";
1
2
  import { cn, createPlugin, joinUrl, throwIfProblemJson } from "zudoku";
2
3
  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
4
  import { Button, ClientOnly, Head, Heading, Link, Slot } from "zudoku/components";
@@ -8,9 +9,9 @@ import { Alert, AlertAction, AlertDescription, AlertTitle } from "zudoku/ui/Aler
8
9
  import { Card, CardContent, CardHeader, CardTitle } from "zudoku/ui/Card";
9
10
  import { Separator } from "zudoku/ui/Separator";
10
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
- import { createContext, use, useEffect, useMemo, useState } from "react";
12
12
  import { parse } from "tinyduration";
13
13
  import { Button as Button$1 } from "zudoku/ui/Button";
14
+ import { Skeleton } from "zudoku/ui/Skeleton";
14
15
  import { DismissibleAlert, DismissibleAlertAction } from "zudoku/ui/DismissibleAlert";
15
16
  import { ActionButton } from "zudoku/ui/ActionButton";
16
17
  import { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "zudoku/ui/Item";
@@ -78,6 +79,16 @@ const useDeploymentName = () => {
78
79
  return deploymentName;
79
80
  };
80
81
 
82
+ //#endregion
83
+ //#region src/hooks/usePurchaseDetails.ts
84
+ const usePurchaseDetails = (planId) => {
85
+ const zudoku = useZudoku();
86
+ return useSuspenseQuery({
87
+ queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/plans/${planId}/purchase-details`],
88
+ meta: { context: zudoku }
89
+ });
90
+ };
91
+
81
92
  //#endregion
82
93
  //#region src/MonetizationContext.tsx
83
94
  const MonetizationContext = createContext({});
@@ -215,6 +226,24 @@ const getPriceFromPlan = (plan) => {
215
226
  };
216
227
  };
217
228
 
229
+ //#endregion
230
+ //#region src/utils/purchaseDetails.ts
231
+ const getPlanFromPurchaseDetails = (response) => {
232
+ return response;
233
+ };
234
+ const getTaxAmountFromPurchaseDetails = (response) => {
235
+ const taxAmount = response?.tax?.taxAmount;
236
+ const numericAmount = typeof taxAmount === "number" ? taxAmount : Number.parseFloat(taxAmount ?? "");
237
+ if (!Number.isFinite(numericAmount)) return;
238
+ return numericAmount;
239
+ };
240
+ const getTaxLabelFromPurchaseDetails = (response) => {
241
+ return (response.tax?.taxes ?? []).some((tax) => tax.taxType?.toLowerCase() === "vat") ? "VAT" : "tax";
242
+ };
243
+ const isTaxInclusiveFromPurchaseDetails = (response) => {
244
+ return response.tax?.taxInclusive === true;
245
+ };
246
+
218
247
  //#endregion
219
248
  //#region src/ZuploMonetizationWrapper.tsx
220
249
  const DEFAULT_GATEWAY_URL = "https://api.zuploedge.com";
@@ -289,15 +318,6 @@ const ZuploMonetizationWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(Query
289
318
 
290
319
  //#endregion
291
320
  //#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
- };
301
321
  const CheckoutConfirmPage = () => {
302
322
  const [search] = useSearchParams();
303
323
  const planId = search.get("planId");
@@ -306,12 +326,11 @@ const CheckoutConfirmPage = () => {
306
326
  const navigate = useNavigate();
307
327
  const { pricing } = useMonetizationConfig();
308
328
  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
- });
329
+ const purchaseDetails = usePurchaseDetails(planId);
313
330
  const selectedPlan = getPlanFromPurchaseDetails(purchaseDetails.data);
314
331
  const taxAmount = getTaxAmountFromPurchaseDetails(purchaseDetails.data);
332
+ const taxLabel = getTaxLabelFromPurchaseDetails(purchaseDetails.data);
333
+ const taxInclusive = isTaxInclusiveFromPurchaseDetails(purchaseDetails.data);
315
334
  const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
316
335
  const { quotas, features } = categorizeRateCards(rateCards ?? [], {
317
336
  currency: selectedPlan?.currency,
@@ -396,13 +415,9 @@ const CheckoutConfirmPage = () => {
396
415
  className: "text-sm text-muted-foreground font-normal",
397
416
  children: ["Billed ", formatBillingCycle(billingCycle)]
398
417
  }),
399
- taxAmount != null && /* @__PURE__ */ jsxs("div", {
418
+ taxAmount != null && /* @__PURE__ */ jsx("div", {
400
419
  className: "text-xs text-muted-foreground font-normal mt-1",
401
- children: [
402
- "+ ",
403
- formatPrice(taxAmount, selectedPlan?.currency),
404
- " VAT"
405
- ]
420
+ children: taxInclusive ? `${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel} included` : `+ ${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel}`
406
421
  })
407
422
  ]
408
423
  }),
@@ -781,20 +796,55 @@ const PricingPage = () => {
781
796
  });
782
797
  };
783
798
 
799
+ //#endregion
800
+ //#region src/pages/PricingPageSkeleton.tsx
801
+ const PricingPageSkeleton = () => /* @__PURE__ */ jsxs("div", {
802
+ className: "w-full px-4 pt-(--padding-content-top) pb-(--padding-content-bottom)",
803
+ children: [/* @__PURE__ */ jsxs("div", {
804
+ className: "text-center space-y-4 mb-12",
805
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-48 mx-auto" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-96 mx-auto" })]
806
+ }), /* @__PURE__ */ jsx("div", {
807
+ className: "w-full grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(300px,max-content))] justify-center gap-6",
808
+ children: [
809
+ 1,
810
+ 2,
811
+ 3
812
+ ].map((i) => /* @__PURE__ */ jsxs(Card, {
813
+ className: "w-[300px]",
814
+ children: [/* @__PURE__ */ jsxs(CardHeader, {
815
+ className: "space-y-3",
816
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-6 w-24" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-32" })]
817
+ }), /* @__PURE__ */ jsxs(CardContent, {
818
+ className: "space-y-3",
819
+ children: [
820
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }),
821
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }),
822
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/4" }),
823
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full mt-4" })
824
+ ]
825
+ })]
826
+ }, i))
827
+ })]
828
+ });
829
+
784
830
  //#endregion
785
831
  //#region src/pages/SubscriptionChangeConfirmPage.tsx
786
832
  const SubscriptionChangeConfirmPage = () => {
787
833
  const [search] = useSearchParams();
788
834
  const planId = search.get("planId");
789
835
  const subscriptionId = search.get("subscriptionId");
836
+ const mode = search.get("mode");
790
837
  const zudoku = useZudoku();
791
838
  const deploymentName = useDeploymentName();
792
839
  const navigate = useNavigate();
793
- const { data: plans } = usePlans();
794
840
  const { pricing } = useMonetizationConfig();
795
- const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
796
841
  if (!planId) throw new Error("Parameter `planId` missing");
797
842
  if (!subscriptionId) throw new Error("Parameter `subscriptionId` missing");
843
+ const purchaseDetails = usePurchaseDetails(planId);
844
+ const selectedPlan = getPlanFromPurchaseDetails(purchaseDetails.data);
845
+ const taxAmount = getTaxAmountFromPurchaseDetails(purchaseDetails.data);
846
+ const taxLabel = getTaxLabelFromPurchaseDetails(purchaseDetails.data);
847
+ const taxInclusive = isTaxInclusiveFromPurchaseDetails(purchaseDetails.data);
798
848
  const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
799
849
  const { quotas, features } = categorizeRateCards(rateCards ?? [], {
800
850
  currency: selectedPlan?.currency,
@@ -803,6 +853,7 @@ const SubscriptionChangeConfirmPage = () => {
803
853
  });
804
854
  const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
805
855
  const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
856
+ const effectiveChangeMessage = mode === "downgrade" ? "This change will take effect at the start of your next billing cycle." : "This change will take effect immediately.";
806
857
  const changeMutation = useMutation({
807
858
  mutationKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions/${subscriptionId}/change`],
808
859
  meta: {
@@ -837,13 +888,20 @@ const SubscriptionChangeConfirmPage = () => {
837
888
  }),
838
889
  /* @__PURE__ */ jsxs("div", {
839
890
  className: "text-center mb-8",
840
- children: [/* @__PURE__ */ jsx("h1", {
841
- className: "text-2xl font-bold text-card-foreground mb-3",
842
- children: "Confirm plan change"
843
- }), /* @__PURE__ */ jsx("p", {
844
- className: "text-muted-foreground text-base",
845
- children: "Please confirm the details below to change your subscription."
846
- })]
891
+ children: [
892
+ /* @__PURE__ */ jsx("h1", {
893
+ className: "text-2xl font-bold text-card-foreground mb-3",
894
+ children: "Confirm plan change"
895
+ }),
896
+ /* @__PURE__ */ jsx("p", {
897
+ className: "text-muted-foreground text-base",
898
+ children: effectiveChangeMessage
899
+ }),
900
+ /* @__PURE__ */ jsx("p", {
901
+ className: "text-muted-foreground text-base",
902
+ children: "Please confirm the details below to change your subscription."
903
+ })
904
+ ]
847
905
  }),
848
906
  selectedPlan && /* @__PURE__ */ jsxs(Card, {
849
907
  className: "bg-muted/50",
@@ -868,13 +926,20 @@ const SubscriptionChangeConfirmPage = () => {
868
926
  }),
869
927
  price && price.monthly > 0 && /* @__PURE__ */ jsxs("div", {
870
928
  className: "text-right",
871
- children: [/* @__PURE__ */ jsx("div", {
872
- className: "text-2xl font-bold",
873
- children: formatPrice(price.monthly, selectedPlan?.currency)
874
- }), billingCycle && /* @__PURE__ */ jsxs("div", {
875
- className: "text-sm text-muted-foreground font-normal",
876
- children: ["Billed ", formatBillingCycle(billingCycle)]
877
- })]
929
+ children: [
930
+ /* @__PURE__ */ jsx("div", {
931
+ className: "text-2xl font-bold",
932
+ children: formatPrice(price.monthly, selectedPlan?.currency)
933
+ }),
934
+ billingCycle && /* @__PURE__ */ jsxs("div", {
935
+ className: "text-sm text-muted-foreground font-normal",
936
+ children: ["Billed ", formatBillingCycle(billingCycle)]
937
+ }),
938
+ taxAmount != null && /* @__PURE__ */ jsx("div", {
939
+ className: "text-xs text-muted-foreground font-normal mt-1",
940
+ children: taxInclusive ? `${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel} included` : `+ ${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel}`
941
+ })
942
+ ]
878
943
  }),
879
944
  price && price.monthly === 0 && /* @__PURE__ */ jsx("div", {
880
945
  className: "text-2xl text-muted-foreground font-bold",
@@ -936,7 +1001,15 @@ const useSubscriptions = (environmentName) => {
936
1001
  const zudoku = useZudoku();
937
1002
  return useSuspenseQuery({
938
1003
  queryKey: [`/v3/zudoku-metering/${environmentName}/subscriptions`],
939
- meta: { context: zudoku }
1004
+ meta: { context: zudoku },
1005
+ select: (data) => ({
1006
+ ...data,
1007
+ items: [...data.items].sort((a, b) => {
1008
+ if (a.status === "active" && b.status !== "active") return -1;
1009
+ if (a.status !== "active" && b.status === "active") return 1;
1010
+ return 0;
1011
+ })
1012
+ })
940
1013
  });
941
1014
  };
942
1015
 
@@ -1428,6 +1501,23 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
1428
1501
 
1429
1502
  //#endregion
1430
1503
  //#region src/pages/subscriptions/SwitchPlanModal.tsx
1504
+ const getAllKeysAcrossPhases = (plan, units) => {
1505
+ const quotaKeys = /* @__PURE__ */ new Set();
1506
+ const featureKeys = /* @__PURE__ */ new Set();
1507
+ for (const phase of plan.phases) {
1508
+ const { quotas, features } = categorizeRateCards(phase.rateCards, {
1509
+ currency: plan.currency,
1510
+ units,
1511
+ planBillingCadence: plan.billingCadence
1512
+ });
1513
+ for (const q of quotas) quotaKeys.add(q.key);
1514
+ for (const f of features) featureKeys.add(f.key);
1515
+ }
1516
+ return {
1517
+ quotaKeys,
1518
+ featureKeys
1519
+ };
1520
+ };
1431
1521
  const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units) => {
1432
1522
  const isUpgrade = targetIndex > currentIndex;
1433
1523
  const currentPhase = currentPlan?.phases.at(-1);
@@ -1448,6 +1538,11 @@ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units)
1448
1538
  quotas: [],
1449
1539
  features: []
1450
1540
  };
1541
+ const currentAllKeys = currentPlan ? getAllKeysAcrossPhases(currentPlan, units) : {
1542
+ quotaKeys: /* @__PURE__ */ new Set(),
1543
+ featureKeys: /* @__PURE__ */ new Set()
1544
+ };
1545
+ const targetAllKeys = getAllKeysAcrossPhases(targetPlan, units);
1451
1546
  const quotaChanges = [];
1452
1547
  const allQuotaKeys = new Set([...currentQuotas.map((q) => q.key), ...targetQuotas.map((q) => q.key)]);
1453
1548
  for (const key of allQuotaKeys) {
@@ -1465,22 +1560,37 @@ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units)
1465
1560
  period: target.period,
1466
1561
  change
1467
1562
  });
1468
- } else if (target && !current) quotaChanges.push({
1469
- key: key ?? "",
1470
- name: target.name,
1471
- currentValue: null,
1472
- newValue: target.limit,
1473
- period: target.period,
1474
- change: "added"
1475
- });
1476
- else if (current && !target) quotaChanges.push({
1477
- key: key ?? "",
1478
- name: current.name,
1479
- currentValue: current.limit,
1480
- newValue: null,
1481
- period: current.period,
1482
- change: "removed"
1483
- });
1563
+ } else if (target && !current) {
1564
+ if (currentAllKeys.featureKeys.has(key)) {
1565
+ quotaChanges.push({
1566
+ key: key ?? "",
1567
+ name: target.name,
1568
+ currentValue: null,
1569
+ newValue: target.limit,
1570
+ period: target.period,
1571
+ change: "same"
1572
+ });
1573
+ continue;
1574
+ }
1575
+ quotaChanges.push({
1576
+ key: key ?? "",
1577
+ name: target.name,
1578
+ currentValue: null,
1579
+ newValue: target.limit,
1580
+ period: target.period,
1581
+ change: "added"
1582
+ });
1583
+ } else if (current && !target) {
1584
+ if (targetAllKeys.featureKeys.has(key)) continue;
1585
+ quotaChanges.push({
1586
+ key: key ?? "",
1587
+ name: current.name,
1588
+ currentValue: current.limit,
1589
+ newValue: null,
1590
+ period: current.period,
1591
+ change: "removed"
1592
+ });
1593
+ }
1484
1594
  }
1485
1595
  const featureChanges = [];
1486
1596
  const allFeatureKeys = new Set([...currentFeatures.map((f) => f.key), ...targetFeatures.map((f) => f.key)]);
@@ -1497,20 +1607,34 @@ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units)
1497
1607
  newValue: target.value ?? true,
1498
1608
  change
1499
1609
  });
1500
- } else if (target && !current) featureChanges.push({
1501
- key: key ?? "",
1502
- name: target.name,
1503
- currentValue: null,
1504
- newValue: target.value ?? true,
1505
- change: "added"
1506
- });
1507
- else if (current && !target) featureChanges.push({
1508
- key: key ?? "",
1509
- name: current.name,
1510
- currentValue: current.value ?? true,
1511
- newValue: null,
1512
- change: "removed"
1513
- });
1610
+ } else if (target && !current) {
1611
+ if (currentAllKeys.quotaKeys.has(key)) {
1612
+ featureChanges.push({
1613
+ key: key ?? "",
1614
+ name: target.name,
1615
+ currentValue: true,
1616
+ newValue: target.value ?? true,
1617
+ change: "same"
1618
+ });
1619
+ continue;
1620
+ }
1621
+ featureChanges.push({
1622
+ key: key ?? "",
1623
+ name: target.name,
1624
+ currentValue: null,
1625
+ newValue: target.value ?? true,
1626
+ change: "added"
1627
+ });
1628
+ } else if (current && !target) {
1629
+ if (targetAllKeys.quotaKeys.has(key)) continue;
1630
+ featureChanges.push({
1631
+ key: key ?? "",
1632
+ name: current.name,
1633
+ currentValue: current.value ?? true,
1634
+ newValue: null,
1635
+ change: "removed"
1636
+ });
1637
+ }
1514
1638
  }
1515
1639
  return {
1516
1640
  plan: targetPlan,
@@ -1529,11 +1653,16 @@ const modeLabelMap = {
1529
1653
  downgrade: "Downgrade",
1530
1654
  private: "Switch"
1531
1655
  };
1532
- const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange }) => {
1656
+ const isSwitchPlanTarget = (value) => {
1657
+ if (typeof value !== "object" || value === null) return false;
1658
+ if (!("subscriptionId" in value) || !("plan" in value) || !("mode" in value)) return false;
1659
+ return true;
1660
+ };
1661
+ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange, isSwitching }) => {
1533
1662
  const price = getPriceFromPlan(comparison.plan);
1534
1663
  const isCustom = comparison.plan.key === "enterprise";
1535
1664
  const displayPrice = price.monthly;
1536
- const hasChanges = comparison.quotaChanges.some((q) => q.change !== "same") || comparison.featureChanges.some((f) => f.change !== "same");
1665
+ const hasChanges = comparison.quotaChanges.length > 0 || comparison.featureChanges.length > 0;
1537
1666
  return /* @__PURE__ */ jsxs("div", {
1538
1667
  className: "border rounded-lg p-4",
1539
1668
  children: [/* @__PURE__ */ jsxs("div", {
@@ -1569,11 +1698,12 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
1569
1698
  mode
1570
1699
  }),
1571
1700
  size: "sm",
1701
+ disabled: isSwitching,
1572
1702
  children: modeLabelMap[mode]
1573
1703
  })]
1574
1704
  }), hasChanges && /* @__PURE__ */ jsxs("div", {
1575
1705
  className: "space-y-1.5",
1576
- children: [comparison.quotaChanges.filter((q) => q.change !== "same").map((quota) => /* @__PURE__ */ jsxs("div", {
1706
+ children: [comparison.quotaChanges.map((quota) => /* @__PURE__ */ jsxs("div", {
1577
1707
  className: "flex items-center gap-2 text-sm",
1578
1708
  children: [
1579
1709
  /* @__PURE__ */ jsx(ChangeIndicator, { change: quota.change }),
@@ -1581,7 +1711,14 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
1581
1711
  className: "font-medium",
1582
1712
  children: [quota.name, ":"]
1583
1713
  }),
1584
- quota.change === "added" ? /* @__PURE__ */ jsx("span", {
1714
+ quota.change === "same" ? /* @__PURE__ */ jsxs("span", {
1715
+ className: "text-muted-foreground",
1716
+ children: [
1717
+ (quota.newValue ?? quota.currentValue)?.toLocaleString(),
1718
+ "/",
1719
+ quota.period
1720
+ ]
1721
+ }) : quota.change === "added" ? /* @__PURE__ */ jsx("span", {
1585
1722
  className: "text-green-600",
1586
1723
  children: "Now included"
1587
1724
  }) : quota.change === "removed" ? /* @__PURE__ */ jsx("span", {
@@ -1610,9 +1747,12 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
1610
1747
  })
1611
1748
  ] })
1612
1749
  ]
1613
- }, quota.key)), comparison.featureChanges.filter((f) => f.change !== "same").map((feature) => /* @__PURE__ */ jsx("div", {
1750
+ }, quota.key)), comparison.featureChanges.map((feature) => /* @__PURE__ */ jsx("div", {
1614
1751
  className: "flex items-center gap-2 text-sm",
1615
- children: feature.change === "added" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1752
+ children: feature.change === "same" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" }), /* @__PURE__ */ jsxs("span", {
1753
+ className: "text-muted-foreground",
1754
+ children: [feature.name, typeof feature.newValue === "string" ? `: ${feature.newValue}` : typeof feature.currentValue === "string" ? `: ${feature.currentValue}` : ""]
1755
+ })] }) : feature.change === "added" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1616
1756
  /* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" }),
1617
1757
  /* @__PURE__ */ jsx("span", {
1618
1758
  className: "text-muted-foreground font-medium",
@@ -1663,24 +1803,32 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
1663
1803
  })]
1664
1804
  });
1665
1805
  };
1666
- const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
1806
+ const SwitchPlanModal = ({ subscription, children }) => {
1807
+ const [open, setOpen] = useState(false);
1808
+ const { data: plansData } = usePlans();
1809
+ const { pricing } = useMonetizationConfig();
1667
1810
  const deploymentName = useDeploymentName();
1668
1811
  const context = useZudoku();
1669
1812
  const { generateUrl } = useUrlUtils();
1670
- const mutation = useMutation({
1813
+ const switchPlanMutation = useMutation({
1671
1814
  mutationKey: [`/v3/zudoku-metering/${deploymentName}/stripe/checkout`],
1672
1815
  meta: {
1673
1816
  context,
1674
- request: {
1675
- method: "POST",
1676
- body: JSON.stringify({
1677
- planId: switchTo.plan.id,
1678
- successURL: generateUrl(`/subscription-change-confirm`, { searchParams: {
1817
+ request: (variables) => {
1818
+ if (!isSwitchPlanTarget(variables)) throw new Error("Couldn't start the plan change. Please refresh and try again.");
1819
+ const switchTo = variables;
1820
+ return {
1821
+ method: "POST",
1822
+ body: JSON.stringify({
1679
1823
  planId: switchTo.plan.id,
1680
- subscriptionId: switchTo.subscriptionId
1681
- } }),
1682
- cancelURL: generateUrl("/subscriptions", { searchParams: { subscriptionId: switchTo.subscriptionId } })
1683
- })
1824
+ successURL: generateUrl(`/subscription-change-confirm`, { searchParams: {
1825
+ planId: switchTo.plan.id,
1826
+ subscriptionId: switchTo.subscriptionId,
1827
+ mode: switchTo.mode
1828
+ } }),
1829
+ cancelURL: generateUrl("/subscriptions", { searchParams: { subscriptionId: switchTo.subscriptionId } })
1830
+ })
1831
+ };
1684
1832
  }
1685
1833
  },
1686
1834
  retry: false,
@@ -1688,39 +1836,7 @@ const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
1688
1836
  window.location.href = data.url;
1689
1837
  }
1690
1838
  });
1691
- return /* @__PURE__ */ jsx(AlertDialog, {
1692
- open: true,
1693
- onOpenChange: onRequestClose,
1694
- children: /* @__PURE__ */ jsxs(AlertDialogContent, { children: [/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [
1695
- /* @__PURE__ */ jsxs(AlertDialogTitle, { children: [
1696
- "Confirm",
1697
- " ",
1698
- switchTo.mode === "private" ? "plan change" : switchTo.mode === "upgrade" ? "upgrade" : "downgrade"
1699
- ] }),
1700
- mutation.isError && /* @__PURE__ */ jsx(Alert, {
1701
- variant: "destructive",
1702
- children: /* @__PURE__ */ jsx(AlertDescription, {
1703
- className: "first-letter:uppercase",
1704
- children: mutation.error.message
1705
- })
1706
- }),
1707
- /* @__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.` })
1708
- ] }), /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [/* @__PURE__ */ jsx(AlertDialogCancel, {
1709
- disabled: mutation.isPending,
1710
- children: "Cancel"
1711
- }), /* @__PURE__ */ jsx(ActionButton, {
1712
- isPending: mutation.isPending,
1713
- onClick: () => mutation.mutate(),
1714
- children: modeLabelMap[switchTo.mode]
1715
- })] })] })
1716
- });
1717
- };
1718
- const SwitchPlanModal = ({ subscription, children }) => {
1719
- const [open, setOpen] = useState(false);
1720
- const { data: plansData } = usePlans();
1721
- const [switchTo, setSwitchTo] = useState(null);
1722
- const { pricing } = useMonetizationConfig();
1723
- const currentPlan = plansData?.items.find((p) => p.id === subscription.plan.id);
1839
+ const currentPlan = plansData?.items.find((p) => p.key === subscription.plan.key);
1724
1840
  const { upgrades, downgrades, privatePlans } = useMemo(() => {
1725
1841
  if (!plansData?.items || !currentPlan) return {
1726
1842
  upgrades: [],
@@ -1742,10 +1858,7 @@ const SwitchPlanModal = ({ subscription, children }) => {
1742
1858
  currentPlan,
1743
1859
  pricing?.units
1744
1860
  ]);
1745
- return /* @__PURE__ */ jsxs(Fragment, { children: [switchTo !== null && /* @__PURE__ */ jsx(ConfirmSwitchAlert, {
1746
- switchTo,
1747
- onRequestClose: () => setSwitchTo(null)
1748
- }), /* @__PURE__ */ jsxs(Dialog, {
1861
+ return /* @__PURE__ */ jsxs(Dialog, {
1749
1862
  open,
1750
1863
  onOpenChange: setOpen,
1751
1864
  children: [/* @__PURE__ */ jsx(DialogTrigger, {
@@ -1766,6 +1879,13 @@ const SwitchPlanModal = ({ subscription, children }) => {
1766
1879
  }), /* @__PURE__ */ jsxs("div", {
1767
1880
  className: "mt-4 space-y-6",
1768
1881
  children: [
1882
+ switchPlanMutation.isError && /* @__PURE__ */ jsx(Alert, {
1883
+ variant: "destructive",
1884
+ children: /* @__PURE__ */ jsx(AlertDescription, {
1885
+ className: "first-letter:uppercase",
1886
+ children: switchPlanMutation.error.message
1887
+ })
1888
+ }),
1769
1889
  currentPlan && /* @__PURE__ */ jsx(Item, {
1770
1890
  variant: "outline",
1771
1891
  children: /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, { children: "Current Plan" }), /* @__PURE__ */ jsx(ItemDescription, {
@@ -1791,7 +1911,8 @@ const SwitchPlanModal = ({ subscription, children }) => {
1791
1911
  comparison,
1792
1912
  subscriptionId: subscription.id,
1793
1913
  mode: "upgrade",
1794
- onRequestChange: setSwitchTo
1914
+ onRequestChange: (target) => switchPlanMutation.mutate(target),
1915
+ isSwitching: switchPlanMutation.isPending
1795
1916
  }, comparison.plan.id))
1796
1917
  })] }),
1797
1918
  downgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
@@ -1812,7 +1933,8 @@ const SwitchPlanModal = ({ subscription, children }) => {
1812
1933
  comparison,
1813
1934
  subscriptionId: subscription.id,
1814
1935
  mode: "downgrade",
1815
- onRequestChange: setSwitchTo
1936
+ onRequestChange: (target) => switchPlanMutation.mutate(target),
1937
+ isSwitching: switchPlanMutation.isPending
1816
1938
  }, comparison.plan.id))
1817
1939
  })] }),
1818
1940
  privatePlans.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
@@ -1833,13 +1955,14 @@ const SwitchPlanModal = ({ subscription, children }) => {
1833
1955
  comparison,
1834
1956
  subscriptionId: subscription.id,
1835
1957
  mode: "private",
1836
- onRequestChange: setSwitchTo
1958
+ onRequestChange: (target) => switchPlanMutation.mutate(target),
1959
+ isSwitching: switchPlanMutation.isPending
1837
1960
  }, comparison.plan.id))
1838
1961
  })] })
1839
1962
  ]
1840
1963
  })]
1841
1964
  }) })]
1842
- })] });
1965
+ });
1843
1966
  };
1844
1967
 
1845
1968
  //#endregion
@@ -1939,7 +2062,7 @@ const ManageSubscription = ({ subscription, planName }) => {
1939
2062
  const isMeteredEntitlement = (entitlement) => {
1940
2063
  return "balance" in entitlement;
1941
2064
  };
1942
- const UsageItem = ({ meter, item, subscription }) => {
2065
+ const UsageItem = ({ meter, item, subscription, featureKey }) => {
1943
2066
  const cadence = item?.billingCadence ?? subscription?.billingCadence;
1944
2067
  const billingPeriod = cadence ? formatDurationAdjective(cadence) : "monthly";
1945
2068
  const isSoftLimit = item?.included?.entitlement?.isSoftLimit ?? true;
@@ -1998,7 +2121,7 @@ const UsageItem = ({ meter, item, subscription }) => {
1998
2121
  ]
1999
2122
  }),
2000
2123
  /* @__PURE__ */ jsxs(CardTitle, { children: [
2001
- item?.name ?? "Limit",
2124
+ item?.name ?? featureKey,
2002
2125
  " ",
2003
2126
  item?.price?.amount
2004
2127
  ] })
@@ -2078,6 +2201,7 @@ const Usage = ({ usage, isFetching, currentItems, subscription, isPendingFirstPa
2078
2201
  ]
2079
2202
  }),
2080
2203
  hasUsage ? Object.entries(usage.entitlements).flatMap(([key, value]) => isMeteredEntitlement(value) ? /* @__PURE__ */ jsx(UsageItem, {
2204
+ featureKey: key,
2081
2205
  meter: { ...value },
2082
2206
  subscription,
2083
2207
  item: currentItems?.find((item) => item.featureKey === key)
@@ -2255,6 +2379,40 @@ const SubscriptionsPage = () => {
2255
2379
  });
2256
2380
  };
2257
2381
 
2382
+ //#endregion
2383
+ //#region src/pages/SubscriptionsPageSkeleton.tsx
2384
+ const SubscriptionsPageSkeleton = () => /* @__PURE__ */ jsx("div", {
2385
+ className: "w-full pt-(--padding-content-top) pb-(--padding-content-bottom)",
2386
+ children: /* @__PURE__ */ jsxs("div", {
2387
+ className: "max-w-4xl space-y-8",
2388
+ children: [
2389
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-56 mb-2" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-80" })] }),
2390
+ /* @__PURE__ */ jsx("div", {
2391
+ className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3",
2392
+ children: [1, 2].map((i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-20 rounded-lg" }, i))
2393
+ }),
2394
+ /* @__PURE__ */ jsxs("div", {
2395
+ className: "space-y-4",
2396
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-16" }), /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-32" }) }), /* @__PURE__ */ jsxs(CardContent, {
2397
+ className: "space-y-2",
2398
+ children: [
2399
+ /* @__PURE__ */ jsxs("div", {
2400
+ className: "flex justify-between",
2401
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-20" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" })]
2402
+ }),
2403
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-2 w-full" }),
2404
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-48" })
2405
+ ]
2406
+ })] })]
2407
+ }),
2408
+ /* @__PURE__ */ jsxs("div", {
2409
+ className: "space-y-4",
2410
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-20" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-12 rounded-lg" })]
2411
+ })
2412
+ ]
2413
+ })
2414
+ });
2415
+
2258
2416
  //#endregion
2259
2417
  //#region src/ZuploMonetizationPlugin.tsx
2260
2418
  const PRICING_PATH = "/pricing";
@@ -2312,18 +2470,23 @@ const zuploMonetizationPlugin = createPlugin((options = {}) => ({
2312
2470
  {
2313
2471
  path: "/manage-payment",
2314
2472
  element: /* @__PURE__ */ jsx(ManagePaymentPage, {})
2315
- },
2316
- {
2317
- path: PRICING_PATH,
2318
- handle: { layout: "default" },
2319
- element: /* @__PURE__ */ jsx(PricingPage, {})
2320
- },
2321
- {
2322
- handle: { layout: "default" },
2323
- path: "/subscriptions",
2324
- element: /* @__PURE__ */ jsx(SubscriptionsPage, {})
2325
2473
  }
2326
2474
  ]
2475
+ }, {
2476
+ element: /* @__PURE__ */ jsx(ZuploMonetizationWrapper, { options }),
2477
+ children: [{
2478
+ path: PRICING_PATH,
2479
+ element: /* @__PURE__ */ jsx(Suspense, {
2480
+ fallback: /* @__PURE__ */ jsx(PricingPageSkeleton, {}),
2481
+ children: /* @__PURE__ */ jsx(PricingPage, {})
2482
+ })
2483
+ }, {
2484
+ path: "/subscriptions",
2485
+ element: /* @__PURE__ */ jsx(Suspense, {
2486
+ fallback: /* @__PURE__ */ jsx(SubscriptionsPageSkeleton, {}),
2487
+ children: /* @__PURE__ */ jsx(SubscriptionsPage, {})
2488
+ })
2489
+ }]
2327
2490
  }];
2328
2491
  },
2329
2492
  getProtectedRoutes: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuplo/zudoku-plugin-monetization",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
@@ -27,11 +27,11 @@
27
27
  "@testing-library/react": "16.3.2",
28
28
  "@types/react": "19.2.14",
29
29
  "@types/react-dom": "19.2.3",
30
- "happy-dom": "20.8.3",
30
+ "happy-dom": "20.8.9",
31
31
  "react": "19.2.4",
32
32
  "react-dom": "19.2.4",
33
33
  "tsdown": "0.20.3",
34
- "zudoku": "0.72.0"
34
+ "zudoku": "0.73.1"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=19.2.0",