@zuplo/zudoku-plugin-monetization 0.0.27 → 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 +109 -87
  2. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -78,6 +78,16 @@ const useDeploymentName = () => {
78
78
  return deploymentName;
79
79
  };
80
80
 
81
+ //#endregion
82
+ //#region src/hooks/usePurchaseDetails.ts
83
+ const usePurchaseDetails = (planId) => {
84
+ const zudoku = useZudoku();
85
+ return useSuspenseQuery({
86
+ queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/plans/${planId}/purchase-details`],
87
+ meta: { context: zudoku }
88
+ });
89
+ };
90
+
81
91
  //#endregion
82
92
  //#region src/MonetizationContext.tsx
83
93
  const MonetizationContext = createContext({});
@@ -215,6 +225,24 @@ const getPriceFromPlan = (plan) => {
215
225
  };
216
226
  };
217
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
+
218
246
  //#endregion
219
247
  //#region src/ZuploMonetizationWrapper.tsx
220
248
  const DEFAULT_GATEWAY_URL = "https://api.zuploedge.com";
@@ -289,15 +317,6 @@ const ZuploMonetizationWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(Query
289
317
 
290
318
  //#endregion
291
319
  //#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
320
  const CheckoutConfirmPage = () => {
302
321
  const [search] = useSearchParams();
303
322
  const planId = search.get("planId");
@@ -306,12 +325,11 @@ const CheckoutConfirmPage = () => {
306
325
  const navigate = useNavigate();
307
326
  const { pricing } = useMonetizationConfig();
308
327
  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
- });
328
+ const purchaseDetails = usePurchaseDetails(planId);
313
329
  const selectedPlan = getPlanFromPurchaseDetails(purchaseDetails.data);
314
330
  const taxAmount = getTaxAmountFromPurchaseDetails(purchaseDetails.data);
331
+ const taxLabel = getTaxLabelFromPurchaseDetails(purchaseDetails.data);
332
+ const taxInclusive = isTaxInclusiveFromPurchaseDetails(purchaseDetails.data);
315
333
  const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
316
334
  const { quotas, features } = categorizeRateCards(rateCards ?? [], {
317
335
  currency: selectedPlan?.currency,
@@ -396,13 +414,9 @@ const CheckoutConfirmPage = () => {
396
414
  className: "text-sm text-muted-foreground font-normal",
397
415
  children: ["Billed ", formatBillingCycle(billingCycle)]
398
416
  }),
399
- taxAmount != null && /* @__PURE__ */ jsxs("div", {
417
+ taxAmount != null && /* @__PURE__ */ jsx("div", {
400
418
  className: "text-xs text-muted-foreground font-normal mt-1",
401
- children: [
402
- "+ ",
403
- formatPrice(taxAmount, selectedPlan?.currency),
404
- " VAT"
405
- ]
419
+ children: taxInclusive ? `${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel} included` : `+ ${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel}`
406
420
  })
407
421
  ]
408
422
  }),
@@ -787,14 +801,18 @@ const SubscriptionChangeConfirmPage = () => {
787
801
  const [search] = useSearchParams();
788
802
  const planId = search.get("planId");
789
803
  const subscriptionId = search.get("subscriptionId");
804
+ const mode = search.get("mode");
790
805
  const zudoku = useZudoku();
791
806
  const deploymentName = useDeploymentName();
792
807
  const navigate = useNavigate();
793
- const { data: plans } = usePlans();
794
808
  const { pricing } = useMonetizationConfig();
795
- const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
796
809
  if (!planId) throw new Error("Parameter `planId` missing");
797
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);
798
816
  const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
799
817
  const { quotas, features } = categorizeRateCards(rateCards ?? [], {
800
818
  currency: selectedPlan?.currency,
@@ -803,6 +821,7 @@ const SubscriptionChangeConfirmPage = () => {
803
821
  });
804
822
  const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
805
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.";
806
825
  const changeMutation = useMutation({
807
826
  mutationKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions/${subscriptionId}/change`],
808
827
  meta: {
@@ -837,13 +856,20 @@ const SubscriptionChangeConfirmPage = () => {
837
856
  }),
838
857
  /* @__PURE__ */ jsxs("div", {
839
858
  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
- })]
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
+ ]
847
873
  }),
848
874
  selectedPlan && /* @__PURE__ */ jsxs(Card, {
849
875
  className: "bg-muted/50",
@@ -868,13 +894,20 @@ const SubscriptionChangeConfirmPage = () => {
868
894
  }),
869
895
  price && price.monthly > 0 && /* @__PURE__ */ jsxs("div", {
870
896
  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
- })]
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
+ ]
878
911
  }),
879
912
  price && price.monthly === 0 && /* @__PURE__ */ jsx("div", {
880
913
  className: "text-2xl text-muted-foreground font-bold",
@@ -1529,7 +1562,12 @@ const modeLabelMap = {
1529
1562
  downgrade: "Downgrade",
1530
1563
  private: "Switch"
1531
1564
  };
1532
- 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 }) => {
1533
1571
  const price = getPriceFromPlan(comparison.plan);
1534
1572
  const isCustom = comparison.plan.key === "enterprise";
1535
1573
  const displayPrice = price.monthly;
@@ -1569,6 +1607,7 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
1569
1607
  mode
1570
1608
  }),
1571
1609
  size: "sm",
1610
+ disabled: isSwitching,
1572
1611
  children: modeLabelMap[mode]
1573
1612
  })]
1574
1613
  }), hasChanges && /* @__PURE__ */ jsxs("div", {
@@ -1663,24 +1702,32 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
1663
1702
  })]
1664
1703
  });
1665
1704
  };
1666
- const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
1705
+ const SwitchPlanModal = ({ subscription, children }) => {
1706
+ const [open, setOpen] = useState(false);
1707
+ const { data: plansData } = usePlans();
1708
+ const { pricing } = useMonetizationConfig();
1667
1709
  const deploymentName = useDeploymentName();
1668
1710
  const context = useZudoku();
1669
1711
  const { generateUrl } = useUrlUtils();
1670
- const mutation = useMutation({
1712
+ const switchPlanMutation = useMutation({
1671
1713
  mutationKey: [`/v3/zudoku-metering/${deploymentName}/stripe/checkout`],
1672
1714
  meta: {
1673
1715
  context,
1674
- request: {
1675
- method: "POST",
1676
- body: JSON.stringify({
1677
- planId: switchTo.plan.id,
1678
- 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({
1679
1722
  planId: switchTo.plan.id,
1680
- subscriptionId: switchTo.subscriptionId
1681
- } }),
1682
- cancelURL: generateUrl("/subscriptions", { searchParams: { subscriptionId: switchTo.subscriptionId } })
1683
- })
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
+ };
1684
1731
  }
1685
1732
  },
1686
1733
  retry: false,
@@ -1688,38 +1735,6 @@ const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
1688
1735
  window.location.href = data.url;
1689
1736
  }
1690
1737
  });
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
1738
  const currentPlan = plansData?.items.find((p) => p.id === subscription.plan.id);
1724
1739
  const { upgrades, downgrades, privatePlans } = useMemo(() => {
1725
1740
  if (!plansData?.items || !currentPlan) return {
@@ -1742,10 +1757,7 @@ const SwitchPlanModal = ({ subscription, children }) => {
1742
1757
  currentPlan,
1743
1758
  pricing?.units
1744
1759
  ]);
1745
- return /* @__PURE__ */ jsxs(Fragment, { children: [switchTo !== null && /* @__PURE__ */ jsx(ConfirmSwitchAlert, {
1746
- switchTo,
1747
- onRequestClose: () => setSwitchTo(null)
1748
- }), /* @__PURE__ */ jsxs(Dialog, {
1760
+ return /* @__PURE__ */ jsxs(Dialog, {
1749
1761
  open,
1750
1762
  onOpenChange: setOpen,
1751
1763
  children: [/* @__PURE__ */ jsx(DialogTrigger, {
@@ -1766,6 +1778,13 @@ const SwitchPlanModal = ({ subscription, children }) => {
1766
1778
  }), /* @__PURE__ */ jsxs("div", {
1767
1779
  className: "mt-4 space-y-6",
1768
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
+ }),
1769
1788
  currentPlan && /* @__PURE__ */ jsx(Item, {
1770
1789
  variant: "outline",
1771
1790
  children: /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, { children: "Current Plan" }), /* @__PURE__ */ jsx(ItemDescription, {
@@ -1791,7 +1810,8 @@ const SwitchPlanModal = ({ subscription, children }) => {
1791
1810
  comparison,
1792
1811
  subscriptionId: subscription.id,
1793
1812
  mode: "upgrade",
1794
- onRequestChange: setSwitchTo
1813
+ onRequestChange: (target) => switchPlanMutation.mutate(target),
1814
+ isSwitching: switchPlanMutation.isPending
1795
1815
  }, comparison.plan.id))
1796
1816
  })] }),
1797
1817
  downgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
@@ -1812,7 +1832,8 @@ const SwitchPlanModal = ({ subscription, children }) => {
1812
1832
  comparison,
1813
1833
  subscriptionId: subscription.id,
1814
1834
  mode: "downgrade",
1815
- onRequestChange: setSwitchTo
1835
+ onRequestChange: (target) => switchPlanMutation.mutate(target),
1836
+ isSwitching: switchPlanMutation.isPending
1816
1837
  }, comparison.plan.id))
1817
1838
  })] }),
1818
1839
  privatePlans.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
@@ -1833,13 +1854,14 @@ const SwitchPlanModal = ({ subscription, children }) => {
1833
1854
  comparison,
1834
1855
  subscriptionId: subscription.id,
1835
1856
  mode: "private",
1836
- onRequestChange: setSwitchTo
1857
+ onRequestChange: (target) => switchPlanMutation.mutate(target),
1858
+ isSwitching: switchPlanMutation.isPending
1837
1859
  }, comparison.plan.id))
1838
1860
  })] })
1839
1861
  ]
1840
1862
  })]
1841
1863
  }) })]
1842
- })] });
1864
+ });
1843
1865
  };
1844
1866
 
1845
1867
  //#endregion
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.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.72.0"
34
+ "zudoku": "0.73.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=19.2.0",