@zuplo/zudoku-plugin-monetization 0.0.31 → 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", {
@@ -171,7 +164,6 @@ const formatMinorCurrencyAmount = (amountInMinorUnits, currency) => {
171
164
  maximumFractionDigits: fractionDigits
172
165
  }).format(amountInMinorUnits / divisor);
173
166
  };
174
-
175
167
  //#endregion
176
168
  //#region src/utils/categorizeRateCards.ts
177
169
  const categorizeRateCards = (rateCards, options) => {
@@ -221,7 +213,6 @@ const categorizeRateCards = (rateCards, options) => {
221
213
  features
222
214
  };
223
215
  };
224
-
225
216
  //#endregion
226
217
  //#region src/utils/formatBillingCycle.ts
227
218
  const formatBillingCycle = (duration) => {
@@ -231,7 +222,6 @@ const formatBillingCycle = (duration) => {
231
222
  if (duration === "day") return "daily";
232
223
  return `every ${duration}`;
233
224
  };
234
-
235
225
  //#endregion
236
226
  //#region src/utils/getPriceFromPlan.ts
237
227
  const getPriceFromPlan = (plan) => {
@@ -240,7 +230,6 @@ const getPriceFromPlan = (plan) => {
240
230
  yearly: plan.yearlyPrice != null ? parseFloat(plan.yearlyPrice) : 0
241
231
  };
242
232
  };
243
-
244
233
  //#endregion
245
234
  //#region src/utils/purchaseDetails.ts
246
235
  const getPlanFromPurchaseDetails = (response) => {
@@ -258,7 +247,6 @@ const getTaxLabelFromPurchaseDetails = (response) => {
258
247
  const isTaxInclusiveFromPurchaseDetails = (response) => {
259
248
  return response.tax?.taxInclusive === true;
260
249
  };
261
-
262
250
  //#endregion
263
251
  //#region src/ZuploMonetizationWrapper.tsx
264
252
  const DEFAULT_GATEWAY_URL = "https://api.zuploedge.com";
@@ -330,7 +318,6 @@ const ZuploMonetizationWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(Query
330
318
  children: /* @__PURE__ */ jsx(ClientOnly, { children: /* @__PURE__ */ jsx(Outlet, {}) })
331
319
  })
332
320
  });
333
-
334
321
  //#endregion
335
322
  //#region src/pages/CheckoutConfirmPage.tsx
336
323
  const CheckoutConfirmPage = () => {
@@ -494,7 +481,6 @@ const CheckoutConfirmPage = () => {
494
481
  })
495
482
  });
496
483
  };
497
-
498
484
  //#endregion
499
485
  //#region src/components/RedirectPage.tsx
500
486
  const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
@@ -543,7 +529,6 @@ const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
543
529
  })
544
530
  });
545
531
  };
546
-
547
532
  //#endregion
548
533
  //#region src/hooks/useUrlUtils.ts
549
534
  const useUrlUtils = () => {
@@ -553,7 +538,6 @@ const useUrlUtils = () => {
553
538
  return joinUrl(window.location.origin, basePath, path, searchParams ? `?${new URLSearchParams(searchParams)}` : void 0);
554
539
  } };
555
540
  };
556
-
557
541
  //#endregion
558
542
  //#region src/pages/CheckoutPage.tsx
559
543
  const CheckoutPage = () => {
@@ -604,7 +588,6 @@ const CheckoutPage = () => {
604
588
  })
605
589
  });
606
590
  };
607
-
608
591
  //#endregion
609
592
  //#region src/pages/ManagePaymentPage.tsx
610
593
  const ManagePaymentPage = () => {
@@ -647,7 +630,6 @@ const ManagePaymentPage = () => {
647
630
  })
648
631
  });
649
632
  };
650
-
651
633
  //#endregion
652
634
  //#region src/hooks/usePlans.ts
653
635
  const usePlans = () => {
@@ -658,7 +640,6 @@ const usePlans = () => {
658
640
  meta: { context: auth.isAuthenticated ? zudoku : void 0 }
659
641
  });
660
642
  };
661
-
662
643
  //#endregion
663
644
  //#region src/pages/pricing/PricingCard.tsx
664
645
  const PhaseSection = ({ phase, currency, showName, billingCadence }) => {
@@ -765,7 +746,6 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
765
746
  ]
766
747
  });
767
748
  };
768
-
769
749
  //#endregion
770
750
  //#region src/pages/PricingPage.tsx
771
751
  const PricingPage = () => {
@@ -816,7 +796,6 @@ const PricingPage = () => {
816
796
  ]
817
797
  });
818
798
  };
819
-
820
799
  //#endregion
821
800
  //#region src/pages/PricingPageSkeleton.tsx
822
801
  const PricingPageSkeleton = () => /* @__PURE__ */ jsxs("div", {
@@ -847,7 +826,6 @@ const PricingPageSkeleton = () => /* @__PURE__ */ jsxs("div", {
847
826
  }, i))
848
827
  })]
849
828
  });
850
-
851
829
  //#endregion
852
830
  //#region src/pages/SubscriptionChangeConfirmPage.tsx
853
831
  const SubscriptionChangeConfirmPage = () => {
@@ -1015,7 +993,6 @@ const SubscriptionChangeConfirmPage = () => {
1015
993
  })
1016
994
  });
1017
995
  };
1018
-
1019
996
  //#endregion
1020
997
  //#region src/hooks/useSubscriptions.ts
1021
998
  const useSubscriptions = (environmentName) => {
@@ -1033,7 +1010,6 @@ const useSubscriptions = (environmentName) => {
1033
1010
  })
1034
1011
  });
1035
1012
  };
1036
-
1037
1013
  //#endregion
1038
1014
  //#region src/pages/subscriptions/ConfirmDeleteKeyAlert.tsx
1039
1015
  const ConfirmDeleteKeyAlert = ({ children, onDelete }) => {
@@ -1045,10 +1021,9 @@ const ConfirmDeleteKeyAlert = ({ children, onDelete }) => {
1045
1021
  children: "Continue"
1046
1022
  })] })] })] });
1047
1023
  };
1048
-
1049
1024
  //#endregion
1050
1025
  //#region src/pages/subscriptions/ApiKey.tsx
1051
- const formatDate$1 = (dateString) => {
1026
+ const formatDate$2 = (dateString) => {
1052
1027
  if (!dateString) return "";
1053
1028
  return new Date(dateString).toLocaleDateString("en-US", {
1054
1029
  month: "short",
@@ -1059,8 +1034,7 @@ const formatDate$1 = (dateString) => {
1059
1034
  const getTimeAgo = (dateString) => {
1060
1035
  if (!dateString) return "Never";
1061
1036
  const date = new Date(dateString);
1062
- const now = /* @__PURE__ */ new Date();
1063
- const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1e3 * 60));
1037
+ const diffInMinutes = Math.floor(((/* @__PURE__ */ new Date()).getTime() - date.getTime()) / (1e3 * 60));
1064
1038
  if (diffInMinutes < 1) return "Just now";
1065
1039
  if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`;
1066
1040
  const diffInHours = Math.floor(diffInMinutes / 60);
@@ -1068,7 +1042,7 @@ const getTimeAgo = (dateString) => {
1068
1042
  const diffInDays = Math.floor(diffInHours / 24);
1069
1043
  if (diffInDays === 1) return "1 day ago";
1070
1044
  if (diffInDays < 30) return `${diffInDays} days ago`;
1071
- return formatDate$1(dateString);
1045
+ return formatDate$2(dateString);
1072
1046
  };
1073
1047
  const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label, onDelete }) => {
1074
1048
  const isExpiring = expiresOn && new Date(expiresOn) < new Date(Date.now() + 720 * 60 * 60 * 1e3);
@@ -1125,7 +1099,7 @@ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label
1125
1099
  children: [
1126
1100
  /* @__PURE__ */ jsxs("div", {
1127
1101
  className: "flex items-center gap-1.5",
1128
- 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)] })]
1129
1103
  }),
1130
1104
  /* @__PURE__ */ jsx("span", {
1131
1105
  className: "text-muted-foreground/40",
@@ -1140,7 +1114,7 @@ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label
1140
1114
  children: [
1141
1115
  isExpired ? "Expired" : "Expires",
1142
1116
  " on ",
1143
- formatDate$1(expiresOn)
1117
+ formatDate$2(expiresOn)
1144
1118
  ]
1145
1119
  })] })
1146
1120
  ]
@@ -1148,7 +1122,6 @@ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label
1148
1122
  })]
1149
1123
  });
1150
1124
  };
1151
-
1152
1125
  //#endregion
1153
1126
  //#region src/pages/subscriptions/ApiKeyInfo.tsx
1154
1127
  const ApiKeyInfo = () => /* @__PURE__ */ jsxs(DismissibleAlert, {
@@ -1168,7 +1141,6 @@ const ApiKeyInfo = () => /* @__PURE__ */ jsxs(DismissibleAlert, {
1168
1141
  /* @__PURE__ */ jsx(DismissibleAlertAction, {})
1169
1142
  ]
1170
1143
  });
1171
-
1172
1144
  //#endregion
1173
1145
  //#region src/pages/subscriptions/ConfirmRollKeyAlert.tsx
1174
1146
  const ConfirmRollKeyAlert = (props) => /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
@@ -1178,7 +1150,6 @@ const ConfirmRollKeyAlert = (props) => /* @__PURE__ */ jsxs(AlertDialog, { child
1178
1150
  onClick: props.onRollKey,
1179
1151
  children: "Continue"
1180
1152
  })] })] })] });
1181
-
1182
1153
  //#endregion
1183
1154
  //#region src/pages/subscriptions/ApiKeysList.tsx
1184
1155
  const PendingFirstPaymentAlert = ({ children }) => /* @__PURE__ */ jsxs("div", {
@@ -1258,7 +1229,7 @@ const ApiKeysList = ({ isPendingFirstPayment, apiKeys, deploymentName, consumerI
1258
1229
  /* @__PURE__ */ jsx(AlertTitle, { children: "API key was deleted" }),
1259
1230
  /* @__PURE__ */ jsx(AlertDescription, { children: (() => {
1260
1231
  const deletedKey = apiKeys.find((k) => k.id === deleteKeyMutation.variables?.keyId);
1261
- 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.";
1262
1233
  })() }),
1263
1234
  /* @__PURE__ */ jsx(DismissibleAlertAction, {})
1264
1235
  ]
@@ -1330,7 +1301,6 @@ const ApiKeysList = ({ isPendingFirstPayment, apiKeys, deploymentName, consumerI
1330
1301
  ]
1331
1302
  });
1332
1303
  };
1333
-
1334
1304
  //#endregion
1335
1305
  //#region src/pages/subscriptions/CancelSubscriptionDialog.tsx
1336
1306
  const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId, billingPeriodEnd }) => {
@@ -1370,7 +1340,7 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1370
1340
  /* @__PURE__ */ jsx(AlertTitle, { children: "Your plan will be canceled at the end of your billing cycle." }),
1371
1341
  /* @__PURE__ */ jsxs(AlertDescription, { children: [
1372
1342
  "You'll retain access until ",
1373
- formatDate$1(billingPeriodEnd),
1343
+ formatDate$2(billingPeriodEnd),
1374
1344
  ". After your billing period ends, this plan will not renew and you would need to subscribe again to continue."
1375
1345
  ] })
1376
1346
  ]
@@ -1383,7 +1353,7 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1383
1353
  /* @__PURE__ */ jsxs(AlertDescription, { children: [
1384
1354
  "If you change your mind you have until",
1385
1355
  " ",
1386
- formatDate$1(billingPeriodEnd),
1356
+ formatDate$2(billingPeriodEnd),
1387
1357
  " to remove this cancellation from Manage subscription."
1388
1358
  ] })
1389
1359
  ]
@@ -1437,7 +1407,6 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1437
1407
  })
1438
1408
  });
1439
1409
  };
1440
-
1441
1410
  //#endregion
1442
1411
  //#region src/pages/subscriptions/RestoreSubscriptionDialog.tsx
1443
1412
  const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId, billingPeriodEnd }) => {
@@ -1487,7 +1456,7 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
1487
1456
  className: "space-y-2",
1488
1457
  children: [/* @__PURE__ */ jsxs("p", { children: [
1489
1458
  "Your access stays in place until ",
1490
- formatDate$1(billingPeriodEnd),
1459
+ formatDate$2(billingPeriodEnd),
1491
1460
  " ",
1492
1461
  "either way."
1493
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." })]
@@ -1519,7 +1488,6 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
1519
1488
  })
1520
1489
  });
1521
1490
  };
1522
-
1523
1491
  //#endregion
1524
1492
  //#region src/pages/subscriptions/SwitchPlanModal.tsx
1525
1493
  const getAllKeysAcrossPhases = (plan, units) => {
@@ -1669,6 +1637,7 @@ const ChangeIndicator = ({ change }) => {
1669
1637
  if (change === "decrease" || change === "removed" || change === "downgraded") return /* @__PURE__ */ jsx(ArrowDownIcon, { className: "w-4 h-4 text-amber-600 shrink-0" });
1670
1638
  return /* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" });
1671
1639
  };
1640
+ const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
1672
1641
  const modeLabelMap = {
1673
1642
  upgrade: "Upgrade",
1674
1643
  downgrade: "Downgrade",
@@ -1859,12 +1828,29 @@ const SwitchPlanModal = ({ subscription, children }) => {
1859
1828
  });
1860
1829
  const currentPlan = plansData?.items.find((p) => p.key === subscription.plan.key);
1861
1830
  const { upgrades, downgrades, privatePlans } = useMemo(() => {
1862
- if (!plansData?.items || !currentPlan) return {
1831
+ if (!plansData?.items) return {
1863
1832
  upgrades: [],
1864
1833
  downgrades: [],
1865
1834
  privatePlans: []
1866
1835
  };
1867
- 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
+ }
1868
1854
  const currentIndex = plansData.items.findIndex((p) => p.id === currentPlan.id);
1869
1855
  const allComparisons = plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => {
1870
1856
  return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan), pricing?.units);
@@ -1914,6 +1900,13 @@ const SwitchPlanModal = ({ subscription, children }) => {
1914
1900
  children: currentPlan.name
1915
1901
  })] })
1916
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
+ }),
1917
1910
  upgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
1918
1911
  className: "flex items-center justify-between mb-3",
1919
1912
  children: [/* @__PURE__ */ jsxs("div", {
@@ -1985,7 +1978,6 @@ const SwitchPlanModal = ({ subscription, children }) => {
1985
1978
  }) })]
1986
1979
  });
1987
1980
  };
1988
-
1989
1981
  //#endregion
1990
1982
  //#region src/pages/subscriptions/ManageSubscription.tsx
1991
1983
  const ManageSubscription = ({ subscription, planName }) => {
@@ -2077,7 +2069,217 @@ const ManageSubscription = ({ subscription, planName }) => {
2077
2069
  })
2078
2070
  ] });
2079
2071
  };
2080
-
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
+ };
2081
2283
  //#endregion
2082
2284
  //#region src/pages/subscriptions/Usage.tsx
2083
2285
  const isMeteredEntitlement = (entitlement) => {
@@ -2237,7 +2439,6 @@ const Usage = ({ usage, isFetching, currentItems, subscription, isPendingFirstPa
2237
2439
  ]
2238
2440
  });
2239
2441
  };
2240
-
2241
2442
  //#endregion
2242
2443
  //#region src/pages/subscriptions/ActiveSubscription.tsx
2243
2444
  const ActiveSubscription = ({ subscription, deploymentName }) => {
@@ -2261,6 +2462,7 @@ const ActiveSubscription = ({ subscription, deploymentName }) => {
2261
2462
  /* @__PURE__ */ jsx(DismissibleAlertAction, {})
2262
2463
  ]
2263
2464
  }),
2465
+ /* @__PURE__ */ jsx(SubscriptionPlanDetails, { subscription }),
2264
2466
  /* @__PURE__ */ jsx(Usage, {
2265
2467
  currentItems: activePhase?.items,
2266
2468
  usage: usageQuery.data,
@@ -2280,7 +2482,6 @@ const ActiveSubscription = ({ subscription, deploymentName }) => {
2280
2482
  })
2281
2483
  ] });
2282
2484
  };
2283
-
2284
2485
  //#endregion
2285
2486
  //#region src/pages/subscriptions/SubscriptionsList.tsx
2286
2487
  const formatDate = (dateString) => {
@@ -2354,7 +2555,6 @@ const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
2354
2555
  }, subscription.id)
2355
2556
  });
2356
2557
  };
2357
-
2358
2558
  //#endregion
2359
2559
  //#region src/pages/SubscriptionsPage.tsx
2360
2560
  const SubscriptionsPage = () => {
@@ -2399,7 +2599,6 @@ const SubscriptionsPage = () => {
2399
2599
  })]
2400
2600
  });
2401
2601
  };
2402
-
2403
2602
  //#endregion
2404
2603
  //#region src/pages/SubscriptionsPageSkeleton.tsx
2405
2604
  const SubscriptionsPageSkeleton = () => /* @__PURE__ */ jsx("div", {
@@ -2433,7 +2632,6 @@ const SubscriptionsPageSkeleton = () => /* @__PURE__ */ jsx("div", {
2433
2632
  ]
2434
2633
  })
2435
2634
  });
2436
-
2437
2635
  //#endregion
2438
2636
  //#region src/ZuploMonetizationPlugin.tsx
2439
2637
  const PRICING_PATH = "/pricing";
@@ -2520,6 +2718,5 @@ const zuploMonetizationPlugin = createPlugin((options = {}) => ({
2520
2718
  ];
2521
2719
  }
2522
2720
  }));
2523
-
2524
2721
  //#endregion
2525
- 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.31",
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",