@zuplo/zudoku-plugin-monetization 0.0.30 → 0.0.32

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,4 @@
1
- import "react";
2
- import * as zudoku from "zudoku";
1
+ import * as _$zudoku from "zudoku";
3
2
 
4
3
  //#region src/MonetizationContext.d.ts
5
4
  interface MonetizationConfig {
@@ -12,6 +11,6 @@ interface MonetizationConfig {
12
11
  }
13
12
  //#endregion
14
13
  //#region src/ZuploMonetizationPlugin.d.ts
15
- declare const zuploMonetizationPlugin: (options?: MonetizationConfig | undefined) => zudoku.ZudokuPlugin;
14
+ declare const zuploMonetizationPlugin: (options?: MonetizationConfig | undefined) => _$zudoku.ZudokuPlugin;
16
15
  //#endregion
17
16
  export { zuploMonetizationPlugin };
package/dist/index.mjs CHANGED
@@ -6,7 +6,7 @@ import { useAuth, useZudoku } from "zudoku/hooks";
6
6
  import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient, useSuspenseQuery } from "zudoku/react-query";
7
7
  import { Link as Link$1, Outlet, useLocation, useNavigate, useSearchParams } from "zudoku/router";
8
8
  import { Alert, AlertAction, AlertDescription, AlertTitle } from "zudoku/ui/Alert";
9
- import { Card, CardContent, CardHeader, CardTitle } from "zudoku/ui/Card";
9
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "zudoku/ui/Card";
10
10
  import { Separator } from "zudoku/ui/Separator";
11
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
12
  import { parse } from "tinyduration";
@@ -23,7 +23,6 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
23
23
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "zudoku/ui/Dialog";
24
24
  import { Input } from "zudoku/ui/Input";
25
25
  import { Progress } from "zudoku/ui/Progress";
26
-
27
26
  //#region src/components/FeatureItem.tsx
28
27
  const FeatureItem = ({ feature, className }) => {
29
28
  return /* @__PURE__ */ jsxs("div", {
@@ -41,7 +40,6 @@ const FeatureItem = ({ feature, className }) => {
41
40
  })]
42
41
  });
43
42
  };
44
-
45
43
  //#endregion
46
44
  //#region src/components/QuotaItem.tsx
47
45
  const QuotaItem = ({ quota, className }) => {
@@ -70,7 +68,6 @@ const QuotaItem = ({ quota, className }) => {
70
68
  })]
71
69
  });
72
70
  };
73
-
74
71
  //#endregion
75
72
  //#region src/hooks/useDeploymentName.ts
76
73
  const useDeploymentName = () => {
@@ -78,7 +75,6 @@ const useDeploymentName = () => {
78
75
  if (!deploymentName) throw new Error("ZUPLO_PUBLIC_DEPLOYMENT_NAME is not set");
79
76
  return deploymentName;
80
77
  };
81
-
82
78
  //#endregion
83
79
  //#region src/hooks/usePurchaseDetails.ts
84
80
  const usePurchaseDetails = (planId) => {
@@ -88,12 +84,10 @@ const usePurchaseDetails = (planId) => {
88
84
  meta: { context: zudoku }
89
85
  });
90
86
  };
91
-
92
87
  //#endregion
93
88
  //#region src/MonetizationContext.tsx
94
89
  const MonetizationContext = createContext({});
95
90
  const useMonetizationConfig = () => use(MonetizationContext);
96
-
97
91
  //#endregion
98
92
  //#region src/utils/formatDuration.ts
99
93
  const formatDuration = (iso) => {
@@ -146,7 +140,6 @@ const formatDurationAdjective = (iso) => {
146
140
  return "billing period";
147
141
  }
148
142
  };
149
-
150
143
  //#endregion
151
144
  //#region src/utils/formatPrice.ts
152
145
  const formatPrice = (amount, currency) => new Intl.NumberFormat("en-US", {
@@ -156,13 +149,21 @@ const formatPrice = (amount, currency) => new Intl.NumberFormat("en-US", {
156
149
  maximumFractionDigits: 6,
157
150
  trailingZeroDisplay: "stripIfInteger"
158
151
  }).format(amount);
159
- const formatPriceTwoDecimals = (amount, currency) => new Intl.NumberFormat("en-US", {
160
- style: "currency",
161
- currency: currency ?? "USD",
162
- minimumFractionDigits: 2,
163
- maximumFractionDigits: 2
164
- }).format(amount);
165
-
152
+ /** Amount is in the smallest currency unit (e.g. Stripe); divisor from `Intl` / ISO 4217. */
153
+ const formatMinorCurrencyAmount = (amountInMinorUnits, currency) => {
154
+ const code = (currency ?? "USD").toUpperCase();
155
+ const fractionDigits = new Intl.NumberFormat("en-US", {
156
+ style: "currency",
157
+ currency: code
158
+ }).resolvedOptions().maximumFractionDigits ?? 2;
159
+ const divisor = 10 ** fractionDigits;
160
+ return new Intl.NumberFormat("en-US", {
161
+ style: "currency",
162
+ currency: code,
163
+ minimumFractionDigits: fractionDigits,
164
+ maximumFractionDigits: fractionDigits
165
+ }).format(amountInMinorUnits / divisor);
166
+ };
166
167
  //#endregion
167
168
  //#region src/utils/categorizeRateCards.ts
168
169
  const categorizeRateCards = (rateCards, options) => {
@@ -212,7 +213,6 @@ const categorizeRateCards = (rateCards, options) => {
212
213
  features
213
214
  };
214
215
  };
215
-
216
216
  //#endregion
217
217
  //#region src/utils/formatBillingCycle.ts
218
218
  const formatBillingCycle = (duration) => {
@@ -222,7 +222,6 @@ const formatBillingCycle = (duration) => {
222
222
  if (duration === "day") return "daily";
223
223
  return `every ${duration}`;
224
224
  };
225
-
226
225
  //#endregion
227
226
  //#region src/utils/getPriceFromPlan.ts
228
227
  const getPriceFromPlan = (plan) => {
@@ -231,7 +230,6 @@ const getPriceFromPlan = (plan) => {
231
230
  yearly: plan.yearlyPrice != null ? parseFloat(plan.yearlyPrice) : 0
232
231
  };
233
232
  };
234
-
235
233
  //#endregion
236
234
  //#region src/utils/purchaseDetails.ts
237
235
  const getPlanFromPurchaseDetails = (response) => {
@@ -249,7 +247,6 @@ const getTaxLabelFromPurchaseDetails = (response) => {
249
247
  const isTaxInclusiveFromPurchaseDetails = (response) => {
250
248
  return response.tax?.taxInclusive === true;
251
249
  };
252
-
253
250
  //#endregion
254
251
  //#region src/ZuploMonetizationWrapper.tsx
255
252
  const DEFAULT_GATEWAY_URL = "https://api.zuploedge.com";
@@ -321,7 +318,6 @@ const ZuploMonetizationWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(Query
321
318
  children: /* @__PURE__ */ jsx(ClientOnly, { children: /* @__PURE__ */ jsx(Outlet, {}) })
322
319
  })
323
320
  });
324
-
325
321
  //#endregion
326
322
  //#region src/pages/CheckoutConfirmPage.tsx
327
323
  const CheckoutConfirmPage = () => {
@@ -418,8 +414,8 @@ const CheckoutConfirmPage = () => {
418
414
  children: formatPrice(price.monthly, selectedPlan?.currency)
419
415
  }),
420
416
  taxAmount != null && /* @__PURE__ */ jsx("div", {
421
- className: "text-xs font-normal mt-1",
422
- children: taxInclusive ? `${formatPriceTwoDecimals(taxAmount, selectedPlan?.currency)} ${taxLabel} included` : `+ ${formatPriceTwoDecimals(taxAmount, selectedPlan?.currency)} ${taxLabel}`
417
+ className: "text-sm font-normal mt-1",
418
+ children: taxInclusive ? `${formatMinorCurrencyAmount(taxAmount, selectedPlan?.currency)} ${taxLabel} included` : `+ ${formatMinorCurrencyAmount(taxAmount, selectedPlan?.currency)} ${taxLabel}`
423
419
  }),
424
420
  billingCycle && /* @__PURE__ */ jsxs("div", {
425
421
  className: "text-sm text-muted-foreground font-normal",
@@ -485,7 +481,6 @@ const CheckoutConfirmPage = () => {
485
481
  })
486
482
  });
487
483
  };
488
-
489
484
  //#endregion
490
485
  //#region src/components/RedirectPage.tsx
491
486
  const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
@@ -534,7 +529,6 @@ const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
534
529
  })
535
530
  });
536
531
  };
537
-
538
532
  //#endregion
539
533
  //#region src/hooks/useUrlUtils.ts
540
534
  const useUrlUtils = () => {
@@ -544,7 +538,6 @@ const useUrlUtils = () => {
544
538
  return joinUrl(window.location.origin, basePath, path, searchParams ? `?${new URLSearchParams(searchParams)}` : void 0);
545
539
  } };
546
540
  };
547
-
548
541
  //#endregion
549
542
  //#region src/pages/CheckoutPage.tsx
550
543
  const CheckoutPage = () => {
@@ -595,7 +588,6 @@ const CheckoutPage = () => {
595
588
  })
596
589
  });
597
590
  };
598
-
599
591
  //#endregion
600
592
  //#region src/pages/ManagePaymentPage.tsx
601
593
  const ManagePaymentPage = () => {
@@ -638,7 +630,6 @@ const ManagePaymentPage = () => {
638
630
  })
639
631
  });
640
632
  };
641
-
642
633
  //#endregion
643
634
  //#region src/hooks/usePlans.ts
644
635
  const usePlans = () => {
@@ -649,7 +640,6 @@ const usePlans = () => {
649
640
  meta: { context: auth.isAuthenticated ? zudoku : void 0 }
650
641
  });
651
642
  };
652
-
653
643
  //#endregion
654
644
  //#region src/pages/pricing/PricingCard.tsx
655
645
  const PhaseSection = ({ phase, currency, showName, billingCadence }) => {
@@ -756,7 +746,6 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
756
746
  ]
757
747
  });
758
748
  };
759
-
760
749
  //#endregion
761
750
  //#region src/pages/PricingPage.tsx
762
751
  const PricingPage = () => {
@@ -807,7 +796,6 @@ const PricingPage = () => {
807
796
  ]
808
797
  });
809
798
  };
810
-
811
799
  //#endregion
812
800
  //#region src/pages/PricingPageSkeleton.tsx
813
801
  const PricingPageSkeleton = () => /* @__PURE__ */ jsxs("div", {
@@ -838,7 +826,6 @@ const PricingPageSkeleton = () => /* @__PURE__ */ jsxs("div", {
838
826
  }, i))
839
827
  })]
840
828
  });
841
-
842
829
  //#endregion
843
830
  //#region src/pages/SubscriptionChangeConfirmPage.tsx
844
831
  const SubscriptionChangeConfirmPage = () => {
@@ -944,8 +931,8 @@ const SubscriptionChangeConfirmPage = () => {
944
931
  children: formatPrice(price.monthly, selectedPlan?.currency)
945
932
  }),
946
933
  taxAmount != null && /* @__PURE__ */ jsx("div", {
947
- className: "text-xs font-normal mt-1",
948
- children: taxInclusive ? `${formatPriceTwoDecimals(taxAmount, selectedPlan?.currency)} ${taxLabel} included` : `+ ${formatPriceTwoDecimals(taxAmount, selectedPlan?.currency)} ${taxLabel}`
934
+ className: "text-sm font-normal mt-1",
935
+ children: taxInclusive ? `${formatMinorCurrencyAmount(taxAmount, selectedPlan?.currency)} ${taxLabel} included` : `+ ${formatMinorCurrencyAmount(taxAmount, selectedPlan?.currency)} ${taxLabel}`
949
936
  }),
950
937
  billingCycle && /* @__PURE__ */ jsxs("div", {
951
938
  className: "text-sm text-muted-foreground font-normal",
@@ -1006,7 +993,6 @@ const SubscriptionChangeConfirmPage = () => {
1006
993
  })
1007
994
  });
1008
995
  };
1009
-
1010
996
  //#endregion
1011
997
  //#region src/hooks/useSubscriptions.ts
1012
998
  const useSubscriptions = (environmentName) => {
@@ -1024,7 +1010,6 @@ const useSubscriptions = (environmentName) => {
1024
1010
  })
1025
1011
  });
1026
1012
  };
1027
-
1028
1013
  //#endregion
1029
1014
  //#region src/pages/subscriptions/ConfirmDeleteKeyAlert.tsx
1030
1015
  const ConfirmDeleteKeyAlert = ({ children, onDelete }) => {
@@ -1036,10 +1021,9 @@ const ConfirmDeleteKeyAlert = ({ children, onDelete }) => {
1036
1021
  children: "Continue"
1037
1022
  })] })] })] });
1038
1023
  };
1039
-
1040
1024
  //#endregion
1041
1025
  //#region src/pages/subscriptions/ApiKey.tsx
1042
- const formatDate$1 = (dateString) => {
1026
+ const formatDate$2 = (dateString) => {
1043
1027
  if (!dateString) return "";
1044
1028
  return new Date(dateString).toLocaleDateString("en-US", {
1045
1029
  month: "short",
@@ -1050,8 +1034,7 @@ const formatDate$1 = (dateString) => {
1050
1034
  const getTimeAgo = (dateString) => {
1051
1035
  if (!dateString) return "Never";
1052
1036
  const date = new Date(dateString);
1053
- const now = /* @__PURE__ */ new Date();
1054
- const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1e3 * 60));
1037
+ const diffInMinutes = Math.floor(((/* @__PURE__ */ new Date()).getTime() - date.getTime()) / (1e3 * 60));
1055
1038
  if (diffInMinutes < 1) return "Just now";
1056
1039
  if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`;
1057
1040
  const diffInHours = Math.floor(diffInMinutes / 60);
@@ -1059,7 +1042,7 @@ const getTimeAgo = (dateString) => {
1059
1042
  const diffInDays = Math.floor(diffInHours / 24);
1060
1043
  if (diffInDays === 1) return "1 day ago";
1061
1044
  if (diffInDays < 30) return `${diffInDays} days ago`;
1062
- return formatDate$1(dateString);
1045
+ return formatDate$2(dateString);
1063
1046
  };
1064
1047
  const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label, onDelete }) => {
1065
1048
  const isExpiring = expiresOn && new Date(expiresOn) < new Date(Date.now() + 720 * 60 * 60 * 1e3);
@@ -1116,7 +1099,7 @@ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label
1116
1099
  children: [
1117
1100
  /* @__PURE__ */ jsxs("div", {
1118
1101
  className: "flex items-center gap-1.5",
1119
- children: [/* @__PURE__ */ jsx(ClockIcon, { className: "size-3" }), /* @__PURE__ */ jsxs("span", { children: ["Created ", formatDate$1(createdAt)] })]
1102
+ children: [/* @__PURE__ */ jsx(ClockIcon, { className: "size-3" }), /* @__PURE__ */ jsxs("span", { children: ["Created ", formatDate$2(createdAt)] })]
1120
1103
  }),
1121
1104
  /* @__PURE__ */ jsx("span", {
1122
1105
  className: "text-muted-foreground/40",
@@ -1131,7 +1114,7 @@ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label
1131
1114
  children: [
1132
1115
  isExpired ? "Expired" : "Expires",
1133
1116
  " on ",
1134
- formatDate$1(expiresOn)
1117
+ formatDate$2(expiresOn)
1135
1118
  ]
1136
1119
  })] })
1137
1120
  ]
@@ -1139,7 +1122,6 @@ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label
1139
1122
  })]
1140
1123
  });
1141
1124
  };
1142
-
1143
1125
  //#endregion
1144
1126
  //#region src/pages/subscriptions/ApiKeyInfo.tsx
1145
1127
  const ApiKeyInfo = () => /* @__PURE__ */ jsxs(DismissibleAlert, {
@@ -1159,7 +1141,6 @@ const ApiKeyInfo = () => /* @__PURE__ */ jsxs(DismissibleAlert, {
1159
1141
  /* @__PURE__ */ jsx(DismissibleAlertAction, {})
1160
1142
  ]
1161
1143
  });
1162
-
1163
1144
  //#endregion
1164
1145
  //#region src/pages/subscriptions/ConfirmRollKeyAlert.tsx
1165
1146
  const ConfirmRollKeyAlert = (props) => /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
@@ -1169,7 +1150,6 @@ const ConfirmRollKeyAlert = (props) => /* @__PURE__ */ jsxs(AlertDialog, { child
1169
1150
  onClick: props.onRollKey,
1170
1151
  children: "Continue"
1171
1152
  })] })] })] });
1172
-
1173
1153
  //#endregion
1174
1154
  //#region src/pages/subscriptions/ApiKeysList.tsx
1175
1155
  const PendingFirstPaymentAlert = ({ children }) => /* @__PURE__ */ jsxs("div", {
@@ -1249,7 +1229,7 @@ const ApiKeysList = ({ isPendingFirstPayment, apiKeys, deploymentName, consumerI
1249
1229
  /* @__PURE__ */ jsx(AlertTitle, { children: "API key was deleted" }),
1250
1230
  /* @__PURE__ */ jsx(AlertDescription, { children: (() => {
1251
1231
  const deletedKey = apiKeys.find((k) => k.id === deleteKeyMutation.variables?.keyId);
1252
- return deletedKey ? `API key created ${formatDate$1(deletedKey.createdOn)} has been removed.` : "The API key has been deleted.";
1232
+ return deletedKey ? `API key created ${formatDate$2(deletedKey.createdOn)} has been removed.` : "The API key has been deleted.";
1253
1233
  })() }),
1254
1234
  /* @__PURE__ */ jsx(DismissibleAlertAction, {})
1255
1235
  ]
@@ -1321,7 +1301,6 @@ const ApiKeysList = ({ isPendingFirstPayment, apiKeys, deploymentName, consumerI
1321
1301
  ]
1322
1302
  });
1323
1303
  };
1324
-
1325
1304
  //#endregion
1326
1305
  //#region src/pages/subscriptions/CancelSubscriptionDialog.tsx
1327
1306
  const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId, billingPeriodEnd }) => {
@@ -1361,7 +1340,7 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1361
1340
  /* @__PURE__ */ jsx(AlertTitle, { children: "Your plan will be canceled at the end of your billing cycle." }),
1362
1341
  /* @__PURE__ */ jsxs(AlertDescription, { children: [
1363
1342
  "You'll retain access until ",
1364
- formatDate$1(billingPeriodEnd),
1343
+ formatDate$2(billingPeriodEnd),
1365
1344
  ". After your billing period ends, this plan will not renew and you would need to subscribe again to continue."
1366
1345
  ] })
1367
1346
  ]
@@ -1374,7 +1353,7 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1374
1353
  /* @__PURE__ */ jsxs(AlertDescription, { children: [
1375
1354
  "If you change your mind you have until",
1376
1355
  " ",
1377
- formatDate$1(billingPeriodEnd),
1356
+ formatDate$2(billingPeriodEnd),
1378
1357
  " to remove this cancellation from Manage subscription."
1379
1358
  ] })
1380
1359
  ]
@@ -1428,7 +1407,6 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1428
1407
  })
1429
1408
  });
1430
1409
  };
1431
-
1432
1410
  //#endregion
1433
1411
  //#region src/pages/subscriptions/RestoreSubscriptionDialog.tsx
1434
1412
  const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId, billingPeriodEnd }) => {
@@ -1478,7 +1456,7 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
1478
1456
  className: "space-y-2",
1479
1457
  children: [/* @__PURE__ */ jsxs("p", { children: [
1480
1458
  "Your access stays in place until ",
1481
- formatDate$1(billingPeriodEnd),
1459
+ formatDate$2(billingPeriodEnd),
1482
1460
  " ",
1483
1461
  "either way."
1484
1462
  ] }), /* @__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." })]
@@ -1510,7 +1488,6 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
1510
1488
  })
1511
1489
  });
1512
1490
  };
1513
-
1514
1491
  //#endregion
1515
1492
  //#region src/pages/subscriptions/SwitchPlanModal.tsx
1516
1493
  const getAllKeysAcrossPhases = (plan, units) => {
@@ -1660,6 +1637,7 @@ const ChangeIndicator = ({ change }) => {
1660
1637
  if (change === "decrease" || change === "removed" || change === "downgraded") return /* @__PURE__ */ jsx(ArrowDownIcon, { className: "w-4 h-4 text-amber-600 shrink-0" });
1661
1638
  return /* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" });
1662
1639
  };
1640
+ const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
1663
1641
  const modeLabelMap = {
1664
1642
  upgrade: "Upgrade",
1665
1643
  downgrade: "Downgrade",
@@ -1850,12 +1828,29 @@ const SwitchPlanModal = ({ subscription, children }) => {
1850
1828
  });
1851
1829
  const currentPlan = plansData?.items.find((p) => p.key === subscription.plan.key);
1852
1830
  const { upgrades, downgrades, privatePlans } = useMemo(() => {
1853
- if (!plansData?.items || !currentPlan) return {
1831
+ if (!plansData?.items) return {
1854
1832
  upgrades: [],
1855
1833
  downgrades: [],
1856
1834
  privatePlans: []
1857
1835
  };
1858
- const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
1836
+ if (!currentPlan) {
1837
+ const currentIndex = -1;
1838
+ return {
1839
+ upgrades: plansData.items.map((plan, targetIndex) => comparePlans(void 0, plan, currentIndex, targetIndex, pricing?.units)).filter((c) => !isPrivatePlan(c.plan)),
1840
+ downgrades: [],
1841
+ privatePlans: []
1842
+ };
1843
+ }
1844
+ if (isPrivatePlan(currentPlan)) {
1845
+ const currentIndex = plansData.items.findIndex((p) => p.id === currentPlan.id);
1846
+ return {
1847
+ upgrades: plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => {
1848
+ return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan), pricing?.units);
1849
+ }).filter((c) => !isPrivatePlan(c.plan)),
1850
+ downgrades: [],
1851
+ privatePlans: []
1852
+ };
1853
+ }
1859
1854
  const currentIndex = plansData.items.findIndex((p) => p.id === currentPlan.id);
1860
1855
  const allComparisons = plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => {
1861
1856
  return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan), pricing?.units);
@@ -1905,6 +1900,13 @@ const SwitchPlanModal = ({ subscription, children }) => {
1905
1900
  children: currentPlan.name
1906
1901
  })] })
1907
1902
  }),
1903
+ !currentPlan && /* @__PURE__ */ jsx(Item, {
1904
+ variant: "outline",
1905
+ children: /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, { children: "Current Plan" }), /* @__PURE__ */ jsx(ItemDescription, {
1906
+ className: "text-lg font-bold",
1907
+ children: subscription.plan.name
1908
+ })] })
1909
+ }),
1908
1910
  upgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
1909
1911
  className: "flex items-center justify-between mb-3",
1910
1912
  children: [/* @__PURE__ */ jsxs("div", {
@@ -1976,7 +1978,6 @@ const SwitchPlanModal = ({ subscription, children }) => {
1976
1978
  }) })]
1977
1979
  });
1978
1980
  };
1979
-
1980
1981
  //#endregion
1981
1982
  //#region src/pages/subscriptions/ManageSubscription.tsx
1982
1983
  const ManageSubscription = ({ subscription, planName }) => {
@@ -2068,7 +2069,217 @@ const ManageSubscription = ({ subscription, planName }) => {
2068
2069
  })
2069
2070
  ] });
2070
2071
  };
2071
-
2072
+ //#endregion
2073
+ //#region src/pages/subscriptions/SubscriptionPlanDetails.tsx
2074
+ const detailLabelClassName = "text-sm font-semibold tracking-wide mb-1";
2075
+ const sectionLabelClassName = "text-base font-semibold tracking-wide mb-3 mt-2";
2076
+ const formatDate$1 = (dateString) => {
2077
+ return new Date(dateString).toLocaleDateString("en-US", {
2078
+ month: "short",
2079
+ day: "numeric",
2080
+ year: "numeric"
2081
+ });
2082
+ };
2083
+ const formatDateRange = (from, to) => `${formatDate$1(from)} – ${formatDate$1(to)}`;
2084
+ const formatNumber = (value) => value.toLocaleString("en-US");
2085
+ const getOveragePriceFromItem = (item, currency, units) => {
2086
+ const tiers = item.price?.tiers;
2087
+ if (!tiers || tiers.length === 0) return void 0;
2088
+ const amount = tiers.find((t) => {
2089
+ const amount = t.unitPrice?.amount;
2090
+ if (!amount) return false;
2091
+ const parsed = parseFloat(amount);
2092
+ return Number.isFinite(parsed) && parsed > 0;
2093
+ })?.unitPrice?.amount;
2094
+ if (!amount) return void 0;
2095
+ const parsed = parseFloat(amount);
2096
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
2097
+ const unitLabel = units?.[item.key] ?? units?.[item.featureKey] ?? "unit";
2098
+ return `${formatPrice(parsed, currency)}/${unitLabel}`;
2099
+ };
2100
+ const getEntitlementsFromItems = (items, currency, units, fallbackBillingCadence) => {
2101
+ const features = [];
2102
+ for (const item of items) {
2103
+ const entitlement = item.included?.entitlement;
2104
+ if (!entitlement) continue;
2105
+ if (entitlement.type === "metered" && entitlement.issueAfterReset != null) {
2106
+ const cadence = item.billingCadence ?? fallbackBillingCadence;
2107
+ features.push({
2108
+ entitlementType: "metered",
2109
+ key: item.featureKey ?? item.key,
2110
+ name: item.name ?? item.featureKey ?? item.key,
2111
+ limit: entitlement.issueAfterReset,
2112
+ period: cadence ? formatDuration(cadence) : "month",
2113
+ overagePrice: entitlement.isSoftLimit !== false ? getOveragePriceFromItem(item, currency, units) : void 0
2114
+ });
2115
+ continue;
2116
+ }
2117
+ if (entitlement.type === "boolean") {
2118
+ features.push({
2119
+ entitlementType: "boolean",
2120
+ key: item.featureKey ?? item.key,
2121
+ name: item.name ?? item.featureKey ?? item.key
2122
+ });
2123
+ continue;
2124
+ }
2125
+ if (entitlement.type === "static") {
2126
+ const base = {
2127
+ key: item.featureKey ?? item.key,
2128
+ name: item.name ?? item.featureKey ?? item.key
2129
+ };
2130
+ if (!entitlement.config) {
2131
+ features.push({
2132
+ entitlementType: "static",
2133
+ ...base
2134
+ });
2135
+ continue;
2136
+ }
2137
+ try {
2138
+ const parsed = JSON.parse(entitlement.config);
2139
+ features.push({
2140
+ entitlementType: "static",
2141
+ ...base,
2142
+ value: parsed?.value != null ? String(parsed.value) : void 0
2143
+ });
2144
+ } catch {
2145
+ features.push({
2146
+ entitlementType: "static",
2147
+ ...base
2148
+ });
2149
+ }
2150
+ }
2151
+ }
2152
+ return { features };
2153
+ };
2154
+ const getPhaseRows = (opts) => {
2155
+ const { subscription, currency, units } = opts;
2156
+ const phases = [...subscription.phases].sort((a, b) => new Date(a.activeFrom).getTime() - new Date(b.activeFrom).getTime());
2157
+ const featureRows = [];
2158
+ for (const phase of phases) {
2159
+ const { features } = getEntitlementsFromItems(phase.items ?? [], currency, units, subscription.billingCadence);
2160
+ for (const f of features) featureRows.push({
2161
+ key: f.key,
2162
+ name: f.name,
2163
+ entitlementType: f.entitlementType,
2164
+ limit: f.entitlementType === "metered" ? f.limit : void 0,
2165
+ period: f.entitlementType === "metered" ? f.period : void 0,
2166
+ overagePrice: f.entitlementType === "metered" ? f.overagePrice : void 0,
2167
+ value: f.entitlementType === "static" ? f.value : void 0,
2168
+ phaseId: phase.id,
2169
+ activeFrom: phase.activeFrom,
2170
+ activeTo: phase.activeTo
2171
+ });
2172
+ }
2173
+ return { featureRows };
2174
+ };
2175
+ const formatActiveRange = (activeFrom, activeTo) => {
2176
+ if (!activeTo) return `Starts ${formatDate$1(activeFrom)}`;
2177
+ return `${formatDate$1(activeFrom)} – ${formatDate$1(activeTo)}`;
2178
+ };
2179
+ const SubscriptionPlanDetails = ({ subscription }) => {
2180
+ const { pricing } = useMonetizationConfig();
2181
+ const plan = subscription.plan;
2182
+ const currency = subscription.currency ?? plan.currency;
2183
+ const priceInfo = getPriceFromPlan(plan);
2184
+ const primaryPrice = priceInfo.monthly === 0 && priceInfo.yearly === 0 ? /* @__PURE__ */ jsx("span", {
2185
+ className: "text-primary font-medium",
2186
+ children: "Free"
2187
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2188
+ className: "text-primary font-medium text-lg",
2189
+ children: formatPrice(priceInfo.monthly, currency)
2190
+ }), /* @__PURE__ */ jsxs("span", {
2191
+ className: "text-muted-foreground",
2192
+ children: [" / ", formatDuration(plan.billingCadence)]
2193
+ })] });
2194
+ const { featureRows } = getPhaseRows({
2195
+ subscription,
2196
+ currency,
2197
+ units: pricing?.units
2198
+ });
2199
+ return /* @__PURE__ */ jsxs("div", {
2200
+ className: "space-y-4",
2201
+ children: [/* @__PURE__ */ jsx(Heading, {
2202
+ level: 3,
2203
+ children: "Subscription Details"
2204
+ }), /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsx(CardTitle, {
2205
+ className: "text-lg font-semibold leading-tight",
2206
+ children: plan.name
2207
+ }), plan.description ? /* @__PURE__ */ jsx(CardDescription, { children: plan.description }) : null] }), /* @__PURE__ */ jsxs(CardContent, {
2208
+ className: "space-y-6",
2209
+ children: [/* @__PURE__ */ jsxs("dl", {
2210
+ className: "grid gap-4 sm:grid-cols-2 text-sm",
2211
+ children: [
2212
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2213
+ className: detailLabelClassName,
2214
+ children: "Subscription ID"
2215
+ }), /* @__PURE__ */ jsx("dd", {
2216
+ className: "text-foreground font-mono text-xs break-all",
2217
+ children: subscription.id
2218
+ })] }),
2219
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2220
+ className: detailLabelClassName,
2221
+ children: "Active since"
2222
+ }), /* @__PURE__ */ jsx("dd", {
2223
+ className: "text-foreground",
2224
+ children: formatDate$1(subscription.activeFrom)
2225
+ })] }),
2226
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2227
+ className: detailLabelClassName,
2228
+ children: "Price"
2229
+ }), /* @__PURE__ */ jsx("dd", {
2230
+ className: "flex flex-wrap items-baseline gap-1",
2231
+ children: primaryPrice
2232
+ })] }),
2233
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2234
+ className: detailLabelClassName,
2235
+ children: "Current period"
2236
+ }), /* @__PURE__ */ jsx("dd", {
2237
+ className: "text-foreground",
2238
+ children: subscription.alignment?.currentAlignedBillingPeriod ? formatDateRange(subscription.alignment.currentAlignedBillingPeriod.from, subscription.alignment.currentAlignedBillingPeriod.to) : "—"
2239
+ })] })
2240
+ ]
2241
+ }), featureRows.length > 0 ? /* @__PURE__ */ jsx("div", {
2242
+ className: "space-y-5 pt-2 border-t border-border",
2243
+ children: /* @__PURE__ */ jsxs("div", {
2244
+ className: "space-y-2",
2245
+ children: [/* @__PURE__ */ jsx("p", {
2246
+ className: cn(sectionLabelClassName, "mb-5"),
2247
+ children: "Entitlements"
2248
+ }), /* @__PURE__ */ jsx("ul", {
2249
+ className: "space-y-3",
2250
+ children: featureRows.map((row) => /* @__PURE__ */ jsxs("li", {
2251
+ className: "grid gap-1 text-sm sm:grid-cols-4 sm:items-center sm:gap-4",
2252
+ children: [
2253
+ /* @__PURE__ */ jsx("div", {
2254
+ className: "flex items-start gap-2 text-muted-foreground sm:col-span-2",
2255
+ children: /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsxs("span", {
2256
+ className: "text-foreground font-medium",
2257
+ children: [row.name, " "]
2258
+ }), row.entitlementType === "static" && row.value ? `: ${row.value}` : ""] })
2259
+ }),
2260
+ /* @__PURE__ */ jsx("div", {
2261
+ className: "text-muted-foreground sm:text-right",
2262
+ children: row.entitlementType === "metered" && row.limit != null ? /* @__PURE__ */ jsxs(Fragment, { children: [
2263
+ formatNumber(row.limit),
2264
+ row.period ? ` / ${row.period}` : "",
2265
+ row.overagePrice ? /* @__PURE__ */ jsxs("div", {
2266
+ className: "text-xs mt-0.5",
2267
+ children: ["Overage: ", row.overagePrice]
2268
+ }) : null
2269
+ ] }) : row.entitlementType === "static" && row.value ? row.value : "Included"
2270
+ }),
2271
+ /* @__PURE__ */ jsx("div", {
2272
+ className: "text-xs text-muted-foreground sm:text-right",
2273
+ children: formatActiveRange(row.activeFrom, row.activeTo)
2274
+ })
2275
+ ]
2276
+ }, `${row.key}:${row.phaseId}`))
2277
+ })]
2278
+ })
2279
+ }) : null]
2280
+ })] })]
2281
+ });
2282
+ };
2072
2283
  //#endregion
2073
2284
  //#region src/pages/subscriptions/Usage.tsx
2074
2285
  const isMeteredEntitlement = (entitlement) => {
@@ -2228,7 +2439,6 @@ const Usage = ({ usage, isFetching, currentItems, subscription, isPendingFirstPa
2228
2439
  ]
2229
2440
  });
2230
2441
  };
2231
-
2232
2442
  //#endregion
2233
2443
  //#region src/pages/subscriptions/ActiveSubscription.tsx
2234
2444
  const ActiveSubscription = ({ subscription, deploymentName }) => {
@@ -2252,6 +2462,7 @@ const ActiveSubscription = ({ subscription, deploymentName }) => {
2252
2462
  /* @__PURE__ */ jsx(DismissibleAlertAction, {})
2253
2463
  ]
2254
2464
  }),
2465
+ /* @__PURE__ */ jsx(SubscriptionPlanDetails, { subscription }),
2255
2466
  /* @__PURE__ */ jsx(Usage, {
2256
2467
  currentItems: activePhase?.items,
2257
2468
  usage: usageQuery.data,
@@ -2271,7 +2482,6 @@ const ActiveSubscription = ({ subscription, deploymentName }) => {
2271
2482
  })
2272
2483
  ] });
2273
2484
  };
2274
-
2275
2485
  //#endregion
2276
2486
  //#region src/pages/subscriptions/SubscriptionsList.tsx
2277
2487
  const formatDate = (dateString) => {
@@ -2345,7 +2555,6 @@ const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
2345
2555
  }, subscription.id)
2346
2556
  });
2347
2557
  };
2348
-
2349
2558
  //#endregion
2350
2559
  //#region src/pages/SubscriptionsPage.tsx
2351
2560
  const SubscriptionsPage = () => {
@@ -2390,7 +2599,6 @@ const SubscriptionsPage = () => {
2390
2599
  })]
2391
2600
  });
2392
2601
  };
2393
-
2394
2602
  //#endregion
2395
2603
  //#region src/pages/SubscriptionsPageSkeleton.tsx
2396
2604
  const SubscriptionsPageSkeleton = () => /* @__PURE__ */ jsx("div", {
@@ -2424,7 +2632,6 @@ const SubscriptionsPageSkeleton = () => /* @__PURE__ */ jsx("div", {
2424
2632
  ]
2425
2633
  })
2426
2634
  });
2427
-
2428
2635
  //#endregion
2429
2636
  //#region src/ZuploMonetizationPlugin.tsx
2430
2637
  const PRICING_PATH = "/pricing";
@@ -2511,6 +2718,5 @@ const zuploMonetizationPlugin = createPlugin((options = {}) => ({
2511
2718
  ];
2512
2719
  }
2513
2720
  }));
2514
-
2515
2721
  //#endregion
2516
- export { zuploMonetizationPlugin };
2722
+ export { zuploMonetizationPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuplo/zudoku-plugin-monetization",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
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.9",
31
- "react": "19.2.4",
32
- "react-dom": "19.2.4",
33
- "tsdown": "0.20.3",
34
- "zudoku": "0.75.0"
30
+ "happy-dom": "20.9.0",
31
+ "react": "19.2.5",
32
+ "react-dom": "19.2.5",
33
+ "tsdown": "0.21.9",
34
+ "zudoku": "0.76.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=19.2.0",