@zuplo/zudoku-plugin-monetization 0.0.24 → 0.0.26

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,14 +1,17 @@
1
1
  import * as zudoku from "zudoku";
2
+ import "react";
2
3
 
3
- //#region src/ZuploMonetizationPlugin.d.ts
4
- type ZudokuMonetizationPluginOptions = {
4
+ //#region src/MonetizationContext.d.ts
5
+ interface MonetizationConfig {
5
6
  pricing?: {
6
7
  subtitle?: string;
7
8
  title?: string;
8
- units?: Record<string, string>;
9
9
  showYearlyPrice?: boolean;
10
+ units?: Record<string, string>;
10
11
  };
11
- };
12
- declare const zuploMonetizationPlugin: (options?: ZudokuMonetizationPluginOptions | undefined) => zudoku.ZudokuPlugin;
12
+ }
13
+ //#endregion
14
+ //#region src/ZuploMonetizationPlugin.d.ts
15
+ declare const zuploMonetizationPlugin: (options?: MonetizationConfig | undefined) => zudoku.ZudokuPlugin;
13
16
  //#endregion
14
17
  export { zuploMonetizationPlugin };
package/dist/index.mjs CHANGED
@@ -1,16 +1,16 @@
1
- import { cn, createPlugin, joinUrl } from "zudoku";
1
+ import { cn, createPlugin, joinUrl, throwIfProblemJson } from "zudoku";
2
2
  import { AlertTriangleIcon, ArrowDownIcon, ArrowLeftRightIcon, ArrowUpIcon, CalendarIcon, CheckCheckIcon, CheckIcon, CircleAlert, CircleSlashIcon, ClockIcon, CreditCardIcon, Grid2x2XIcon, InfoIcon, Loader2Icon, LockIcon, MoreVerticalIcon, RefreshCcw, RefreshCwIcon, Settings, ShieldIcon, StarsIcon, Trash2Icon, XIcon } from "zudoku/icons";
3
3
  import { Button, ClientOnly, Head, Heading, Link, Slot } from "zudoku/components";
4
4
  import { useAuth, useZudoku } from "zudoku/hooks";
5
5
  import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient, useSuspenseQuery } from "zudoku/react-query";
6
- import { Link as Link$1, Outlet, useLocation, useNavigate, useParams, useSearchParams } from "zudoku/router";
6
+ import { Link as Link$1, Outlet, useLocation, useNavigate, useSearchParams } from "zudoku/router";
7
7
  import { Alert, AlertAction, AlertDescription, AlertTitle } from "zudoku/ui/Alert";
8
8
  import { Card, CardContent, CardHeader, CardTitle } from "zudoku/ui/Card";
9
9
  import { Separator } from "zudoku/ui/Separator";
10
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ import { createContext, use, useEffect, useMemo, useState } from "react";
11
12
  import { parse } from "tinyduration";
12
13
  import { Button as Button$1 } from "zudoku/ui/Button";
13
- import { useEffect, useMemo, useState } from "react";
14
14
  import { DismissibleAlert, DismissibleAlertAction } from "zudoku/ui/DismissibleAlert";
15
15
  import { ActionButton } from "zudoku/ui/ActionButton";
16
16
  import { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "zudoku/ui/Item";
@@ -90,6 +90,11 @@ const usePlans = () => {
90
90
  });
91
91
  };
92
92
 
93
+ //#endregion
94
+ //#region src/MonetizationContext.tsx
95
+ const MonetizationContext = createContext({});
96
+ const useMonetizationConfig = () => use(MonetizationContext);
97
+
93
98
  //#endregion
94
99
  //#region src/utils/formatDuration.ts
95
100
  const formatDuration = (iso) => {
@@ -124,6 +129,24 @@ const formatDurationInterval = (iso) => {
124
129
  return iso;
125
130
  }
126
131
  };
132
+ /**
133
+ * Returns an adjective form suitable for possessive context
134
+ * e.g. "your monthly quota", "your weekly limit".
135
+ * Falls back to "billing period" for multi-unit cadences
136
+ * where "every 3 months" would be grammatically awkward.
137
+ */
138
+ const formatDurationAdjective = (iso) => {
139
+ try {
140
+ const d = parse(iso);
141
+ if (d.years === 1) return "yearly";
142
+ if (d.months === 1) return "monthly";
143
+ if (d.weeks === 1) return "weekly";
144
+ if (d.days === 1) return "daily";
145
+ return "billing period";
146
+ } catch {
147
+ return "billing period";
148
+ }
149
+ };
127
150
 
128
151
  //#endregion
129
152
  //#region src/utils/formatPrice.ts
@@ -137,7 +160,8 @@ const formatPrice = (amount, currency) => new Intl.NumberFormat("en-US", {
137
160
 
138
161
  //#endregion
139
162
  //#region src/utils/categorizeRateCards.ts
140
- const categorizeRateCards = (rateCards, currency, units) => {
163
+ const categorizeRateCards = (rateCards, options) => {
164
+ const { currency, units, planBillingCadence } = options ?? {};
141
165
  const quotas = [];
142
166
  const features = [];
143
167
  for (const rc of rateCards) {
@@ -157,7 +181,7 @@ const categorizeRateCards = (rateCards, currency, units) => {
157
181
  key: rc.featureKey ?? rc.key,
158
182
  name: rc.name,
159
183
  limit: et.issueAfterReset,
160
- period: et.usagePeriod ? formatDuration(et.usagePeriod) : "Month",
184
+ period: rc.billingCadence ? formatDuration(rc.billingCadence) : planBillingCadence ? formatDuration(planBillingCadence) : "month",
161
185
  overagePrice
162
186
  });
163
187
  } else if (et.type === "boolean") features.push({
@@ -227,13 +251,8 @@ const queryClient = new QueryClient({ defaultOptions: {
227
251
  }
228
252
  });
229
253
  const response = await fetch(q.meta?.context ? await q.meta.context.signRequest(request) : request);
230
- if (!response.ok) {
231
- if (response.headers.get("content-type")?.includes("application/problem+json")) {
232
- const data = await response.json();
233
- throw new Error(data.detail ?? data.title);
234
- }
235
- throw new Error("Failed to fetch request");
236
- }
254
+ await throwIfProblemJson(response);
255
+ if (!response.ok) throw new Error("Failed to fetch request");
237
256
  return response.json();
238
257
  }
239
258
  },
@@ -262,11 +281,8 @@ const queryClient = new QueryClient({ defaultOptions: {
262
281
  }
263
282
  });
264
283
  const response = await fetch(m.meta?.context ? await m.meta.context.signRequest(request) : request);
284
+ await throwIfProblemJson(response);
265
285
  if (!response.ok) {
266
- if (response.headers.get("content-type")?.includes("application/problem+json")) {
267
- const data = await response.json();
268
- throw new Error(data.detail ?? data.title);
269
- }
270
286
  const errorText = await response.text();
271
287
  throw new Error(`Request failed: ${response.status} ${errorText}`);
272
288
  }
@@ -275,26 +291,32 @@ const queryClient = new QueryClient({ defaultOptions: {
275
291
  }
276
292
  }
277
293
  } });
278
- const ZuploMonetizationWrapper = () => {
279
- return /* @__PURE__ */ jsx(QueryClientProvider, {
280
- client: queryClient,
294
+ const ZuploMonetizationWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(QueryClientProvider, {
295
+ client: queryClient,
296
+ children: /* @__PURE__ */ jsx(MonetizationContext, {
297
+ value: options,
281
298
  children: /* @__PURE__ */ jsx(ClientOnly, { children: /* @__PURE__ */ jsx(Outlet, {}) })
282
- });
283
- };
299
+ })
300
+ });
284
301
 
285
302
  //#endregion
286
303
  //#region src/pages/CheckoutConfirmPage.tsx
287
304
  const CheckoutConfirmPage = () => {
288
305
  const [search] = useSearchParams();
289
- const planId = search.get("plan");
306
+ const planId = search.get("planId");
290
307
  const zudoku = useZudoku();
291
308
  const deploymentName = useDeploymentName();
292
309
  const navigate = useNavigate();
293
310
  const { data: plans } = usePlans();
311
+ const { pricing } = useMonetizationConfig();
294
312
  const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
295
313
  if (!planId) throw new Error("Parameter `planId` missing");
296
314
  const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
297
- const { quotas, features } = categorizeRateCards(rateCards ?? [], selectedPlan?.currency);
315
+ const { quotas, features } = categorizeRateCards(rateCards ?? [], {
316
+ currency: selectedPlan?.currency,
317
+ units: pricing?.units,
318
+ planBillingCadence: selectedPlan?.billingCadence
319
+ });
298
320
  const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
299
321
  const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
300
322
  const createSubscriptionMutation = useMutation({
@@ -308,7 +330,7 @@ const CheckoutConfirmPage = () => {
308
330
  },
309
331
  onSuccess: async (subscription) => {
310
332
  await queryClient.invalidateQueries();
311
- navigate(`/subscriptions/${subscription.id}`);
333
+ navigate(`/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`);
312
334
  }
313
335
  });
314
336
  return /* @__PURE__ */ jsx("div", {
@@ -484,23 +506,22 @@ const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
484
506
  //#region src/hooks/useUrlUtils.ts
485
507
  const useUrlUtils = () => {
486
508
  const basePath = useZudoku().options.basePath;
487
- return { generateUrl: (path) => {
509
+ return { generateUrl: (path, { searchParams } = {}) => {
488
510
  if (!window.location.origin) throw new Error("Only works in browser environment");
489
- return joinUrl(window.location.origin, basePath, path);
511
+ return joinUrl(window.location.origin, basePath, path, searchParams ? `?${new URLSearchParams(searchParams)}` : void 0);
490
512
  } };
491
513
  };
492
514
 
493
515
  //#endregion
494
516
  //#region src/pages/CheckoutPage.tsx
495
517
  const CheckoutPage = () => {
496
- const { planId } = useParams();
518
+ const [searchParams] = useSearchParams();
519
+ const planId = searchParams.get("planId");
497
520
  const zudoku = useZudoku();
498
521
  const auth = useAuth();
499
522
  const { generateUrl } = useUrlUtils();
500
523
  const deploymentName = useDeploymentName();
501
524
  if (!planId) throw new Error(`missing planId in URL`);
502
- const successUrl = new URL(generateUrl("/checkout-confirm"));
503
- successUrl.searchParams.set("plan", planId);
504
525
  const checkoutLink = useQuery({
505
526
  queryKey: [
506
527
  `/v3/zudoku-metering/${deploymentName}/stripe/checkout`,
@@ -513,7 +534,7 @@ const CheckoutPage = () => {
513
534
  method: "POST",
514
535
  body: JSON.stringify({
515
536
  planId,
516
- successURL: successUrl.toString(),
537
+ successURL: generateUrl("/checkout-confirm", { searchParams: { planId } }),
517
538
  cancelURL: generateUrl("/pricing")
518
539
  })
519
540
  }
@@ -587,11 +608,14 @@ const ManagePaymentPage = () => {
587
608
 
588
609
  //#endregion
589
610
  //#region src/pages/pricing/PricingCard.tsx
590
- const PhaseSection = ({ phase, currency, showName, excludeKeys, units }) => {
591
- const { quotas, features } = categorizeRateCards(phase.rateCards, currency, units);
592
- const filteredQuotas = quotas.filter((q) => !excludeKeys.has(q.key));
593
- const filteredFeatures = features.filter((f) => !excludeKeys.has(f.key));
594
- if (filteredQuotas.length === 0 && filteredFeatures.length === 0) return null;
611
+ const PhaseSection = ({ phase, currency, showName, billingCadence }) => {
612
+ const { pricing } = useMonetizationConfig();
613
+ const { quotas, features } = categorizeRateCards(phase.rateCards, {
614
+ currency,
615
+ units: pricing?.units,
616
+ planBillingCadence: billingCadence
617
+ });
618
+ if (quotas.length === 0 && features.length === 0) return null;
595
619
  return /* @__PURE__ */ jsxs("div", {
596
620
  className: "space-y-2",
597
621
  children: [
@@ -606,17 +630,19 @@ const PhaseSection = ({ phase, currency, showName, excludeKeys, units }) => {
606
630
  ]
607
631
  })]
608
632
  }),
609
- filteredQuotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, { quota }, quota.key)),
610
- filteredFeatures.map((feature) => /* @__PURE__ */ jsx(FeatureItem, { feature }, feature.key))
633
+ quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, { quota }, quota.key)),
634
+ features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, { feature }, feature.key))
611
635
  ]
612
636
  });
613
637
  };
614
- const PricingCard = ({ plan, isPopular = false, isSubscribed = false, showYearlyPrice = true, units }) => {
638
+ const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
639
+ const { pricing } = useMonetizationConfig();
615
640
  if (plan.phases.length === 0) return null;
616
641
  const price = getPriceFromPlan(plan);
617
642
  const isFree = price.monthly === 0;
618
643
  const isCustom = plan.metadata?.isCustom === true;
619
644
  const hasMultiplePhases = plan.phases.length > 1;
645
+ const billingInterval = formatDuration(plan.billingCadence);
620
646
  return /* @__PURE__ */ jsxs("div", {
621
647
  className: cn("relative rounded-lg border p-6 flex flex-col", isPopular && "border-primary border-2"),
622
648
  children: [
@@ -645,10 +671,10 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false, showYearly
645
671
  })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
646
672
  className: "text-3xl font-bold text-card-foreground",
647
673
  children: isFree ? "Free" : formatPrice(price.monthly, plan.currency)
648
- }), !isFree && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
674
+ }), !isFree && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
649
675
  className: "text-muted-foreground text-sm",
650
- children: "/mo"
651
- }), showYearlyPrice && /* @__PURE__ */ jsxs("div", {
676
+ children: ["/", billingInterval]
677
+ }), pricing?.showYearlyPrice !== false && price.yearly > 0 && /* @__PURE__ */ jsxs("div", {
652
678
  className: "w-full text-sm text-muted-foreground mt-1",
653
679
  children: [formatPrice(price.yearly, plan.currency), "/year"]
654
680
  })] })] })
@@ -661,16 +687,12 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false, showYearly
661
687
  }),
662
688
  /* @__PURE__ */ jsx("div", {
663
689
  className: "space-y-4 mb-6 grow",
664
- children: plan.phases.map((phase, index) => {
665
- const laterKeys = new Set(plan.phases.slice(index + 1).flatMap((p) => p.rateCards.map((rc) => rc.featureKey ?? rc.key)));
666
- return /* @__PURE__ */ jsx(PhaseSection, {
667
- phase,
668
- currency: plan.currency,
669
- showName: hasMultiplePhases,
670
- excludeKeys: laterKeys,
671
- units
672
- }, phase.key);
673
- })
690
+ children: plan.phases.map((phase) => /* @__PURE__ */ jsx(PhaseSection, {
691
+ phase,
692
+ currency: plan.currency,
693
+ showName: hasMultiplePhases,
694
+ billingCadence: plan.billingCadence
695
+ }, phase.key))
674
696
  }),
675
697
  isSubscribed ? /* @__PURE__ */ jsx(Button, {
676
698
  variant: isPopular ? "default" : "secondary",
@@ -683,7 +705,7 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false, showYearly
683
705
  variant: isPopular ? "default" : "secondary",
684
706
  asChild: true,
685
707
  children: /* @__PURE__ */ jsx(Link$1, {
686
- to: `/checkout/${plan.id}`,
708
+ to: `/checkout?planId=${encodeURIComponent(plan.id)}`,
687
709
  children: "Subscribe"
688
710
  })
689
711
  })
@@ -693,7 +715,8 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false, showYearly
693
715
 
694
716
  //#endregion
695
717
  //#region src/pages/PricingPage.tsx
696
- const PricingPage = ({ subtitle = "See our pricing options and choose the one that best suits your needs.", title = "Pricing", units, showYearlyPrice = true }) => {
718
+ const PricingPage = () => {
719
+ const { pricing } = useMonetizationConfig();
697
720
  const zudoku = useZudoku();
698
721
  const deploymentName = useDeploymentName();
699
722
  const auth = useAuth();
@@ -706,28 +729,26 @@ const PricingPage = ({ subtitle = "See our pricing options and choose the one th
706
729
  return /* @__PURE__ */ jsxs("div", {
707
730
  className: "w-full px-4 pt-(--padding-content-top) pb-(--padding-content-bottom)",
708
731
  children: [
709
- /* @__PURE__ */ jsxs(Head, { children: [/* @__PURE__ */ jsx("title", { children: title }), /* @__PURE__ */ jsx("meta", {
732
+ /* @__PURE__ */ jsxs(Head, { children: [/* @__PURE__ */ jsx("title", { children: pricing?.title ?? "Pricing" }), /* @__PURE__ */ jsx("meta", {
710
733
  name: "description",
711
- content: subtitle
734
+ content: pricing?.subtitle ?? "See our pricing options and choose the one that best suits your needs."
712
735
  })] }),
713
736
  /* @__PURE__ */ jsxs("div", {
714
737
  className: "text-center space-y-4 mb-12",
715
738
  children: [/* @__PURE__ */ jsx(Heading, {
716
739
  level: 1,
717
740
  "data-testid": "title",
718
- children: title
741
+ children: pricing?.title ?? "Pricing"
719
742
  }), /* @__PURE__ */ jsx("p", {
720
743
  className: "text-muted-foreground",
721
744
  "data-testid": "subtitle",
722
- children: subtitle
745
+ children: pricing?.subtitle ?? "See our pricing options and choose the one that best suits your needs."
723
746
  })]
724
747
  }),
725
748
  /* @__PURE__ */ jsx("div", {
726
749
  className: "w-full grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(300px,max-content))] justify-center gap-6",
727
750
  children: pricingTable.items.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
728
751
  plan,
729
- units,
730
- showYearlyPrice,
731
752
  isPopular: plan.metadata?.zuplo_most_popular === "true",
732
753
  isSubscribed: subscriptions.items.some((subscription) => ["active", "canceled"].includes(subscription.status))
733
754
  }, plan.id))
@@ -747,11 +768,16 @@ const SubscriptionChangeConfirmPage = () => {
747
768
  const deploymentName = useDeploymentName();
748
769
  const navigate = useNavigate();
749
770
  const { data: plans } = usePlans();
771
+ const { pricing } = useMonetizationConfig();
750
772
  const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
751
773
  if (!planId) throw new Error("Parameter `planId` missing");
752
774
  if (!subscriptionId) throw new Error("Parameter `subscriptionId` missing");
753
775
  const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
754
- const { quotas, features } = categorizeRateCards(rateCards ?? [], selectedPlan?.currency);
776
+ const { quotas, features } = categorizeRateCards(rateCards ?? [], {
777
+ currency: selectedPlan?.currency,
778
+ units: pricing?.units,
779
+ planBillingCadence: selectedPlan?.billingCadence
780
+ });
755
781
  const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
756
782
  const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
757
783
  const changeMutation = useMutation({
@@ -765,7 +791,7 @@ const SubscriptionChangeConfirmPage = () => {
765
791
  },
766
792
  onSuccess: async (subscription) => {
767
793
  await queryClient.invalidateQueries();
768
- navigate(`/subscriptions/${subscription.id}`, { state: { planSwitched: { newPlanName: selectedPlan?.name } } });
794
+ navigate(`/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`, { state: { planSwitched: { newPlanName: selectedPlan?.name } } });
769
795
  }
770
796
  });
771
797
  return /* @__PURE__ */ jsx("div", {
@@ -863,7 +889,7 @@ const SubscriptionChangeConfirmPage = () => {
863
889
  disabled: changeMutation.isPending,
864
890
  asChild: !changeMutation.isPending,
865
891
  children: /* @__PURE__ */ jsx(Link$1, {
866
- to: `/subscriptions/${subscriptionId}`,
892
+ to: `/subscriptions?${new URLSearchParams({ subscriptionId: subscriptionId ?? "" })}`,
867
893
  children: "Cancel"
868
894
  })
869
895
  })]
@@ -1288,15 +1314,23 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1288
1314
 
1289
1315
  //#endregion
1290
1316
  //#region src/pages/subscriptions/SwitchPlanModal.tsx
1291
- const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex) => {
1317
+ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units) => {
1292
1318
  const isUpgrade = targetIndex > currentIndex;
1293
1319
  const currentPhase = currentPlan?.phases.at(-1);
1294
1320
  const targetPhase = targetPlan.phases.at(-1);
1295
- const { quotas: currentQuotas, features: currentFeatures } = currentPhase ? categorizeRateCards(currentPhase.rateCards, currentPlan?.currency) : {
1321
+ const { quotas: currentQuotas, features: currentFeatures } = currentPhase ? categorizeRateCards(currentPhase.rateCards, {
1322
+ currency: currentPlan?.currency,
1323
+ units,
1324
+ planBillingCadence: currentPlan?.billingCadence
1325
+ }) : {
1296
1326
  quotas: [],
1297
1327
  features: []
1298
1328
  };
1299
- const { quotas: targetQuotas, features: targetFeatures } = targetPhase ? categorizeRateCards(targetPhase.rateCards, targetPlan.currency) : {
1329
+ const { quotas: targetQuotas, features: targetFeatures } = targetPhase ? categorizeRateCards(targetPhase.rateCards, {
1330
+ currency: targetPlan.currency,
1331
+ units,
1332
+ planBillingCadence: targetPlan.billingCadence
1333
+ }) : {
1300
1334
  quotas: [],
1301
1335
  features: []
1302
1336
  };
@@ -1403,7 +1437,11 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
1403
1437
  children: "Free"
1404
1438
  }) : /* @__PURE__ */ jsxs("span", {
1405
1439
  className: "text-primary font-medium text-lg",
1406
- children: [formatPrice(displayPrice, comparison.plan.currency), "/ mo"]
1440
+ children: [
1441
+ formatPrice(displayPrice, comparison.plan.currency),
1442
+ "/",
1443
+ formatDuration(comparison.plan.billingCadence)
1444
+ ]
1407
1445
  })]
1408
1446
  }), isCustom ? /* @__PURE__ */ jsx(Button$1, {
1409
1447
  variant: "default",
@@ -1515,9 +1553,6 @@ const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
1515
1553
  const deploymentName = useDeploymentName();
1516
1554
  const context = useZudoku();
1517
1555
  const { generateUrl } = useUrlUtils();
1518
- const successUrl = new URL(generateUrl("/subscription-change-confirm"));
1519
- successUrl.searchParams.set("planId", switchTo.plan.id);
1520
- successUrl.searchParams.set("subscriptionId", switchTo.subscriptionId);
1521
1556
  const mutation = useMutation({
1522
1557
  mutationKey: [`/v3/zudoku-metering/${deploymentName}/stripe/checkout`],
1523
1558
  meta: {
@@ -1526,8 +1561,11 @@ const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
1526
1561
  method: "POST",
1527
1562
  body: JSON.stringify({
1528
1563
  planId: switchTo.plan.id,
1529
- successURL: successUrl.toString(),
1530
- cancelURL: generateUrl(`/subscriptions/${switchTo.subscriptionId}`)
1564
+ successURL: generateUrl(`/subscription-change-confirm`, { searchParams: {
1565
+ planId: switchTo.plan.id,
1566
+ subscriptionId: switchTo.subscriptionId
1567
+ } }),
1568
+ cancelURL: generateUrl("/subscriptions", { searchParams: { subscriptionId: switchTo.subscriptionId } })
1531
1569
  })
1532
1570
  }
1533
1571
  },
@@ -1567,6 +1605,7 @@ const SwitchPlanModal = ({ subscription, children }) => {
1567
1605
  const [open, setOpen] = useState(false);
1568
1606
  const { data: plansData } = usePlans();
1569
1607
  const [switchTo, setSwitchTo] = useState(null);
1608
+ const { pricing } = useMonetizationConfig();
1570
1609
  const currentPlan = plansData?.items.find((p) => p.id === subscription.plan.id);
1571
1610
  const { upgrades, downgrades, privatePlans } = useMemo(() => {
1572
1611
  if (!plansData?.items || !currentPlan) return {
@@ -1577,14 +1616,18 @@ const SwitchPlanModal = ({ subscription, children }) => {
1577
1616
  const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
1578
1617
  const currentIndex = plansData.items.findIndex((p) => p.id === currentPlan.id);
1579
1618
  const allComparisons = plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => {
1580
- return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan));
1619
+ return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan), pricing?.units);
1581
1620
  });
1582
1621
  return {
1583
1622
  upgrades: allComparisons.filter((c) => c.isUpgrade && !isPrivatePlan(c.plan)),
1584
1623
  downgrades: allComparisons.filter((c) => !c.isUpgrade && !isPrivatePlan(c.plan)),
1585
1624
  privatePlans: allComparisons.filter((c) => isPrivatePlan(c.plan))
1586
1625
  };
1587
- }, [plansData?.items, currentPlan]);
1626
+ }, [
1627
+ plansData?.items,
1628
+ currentPlan,
1629
+ pricing?.units
1630
+ ]);
1588
1631
  return /* @__PURE__ */ jsxs(Fragment, { children: [switchTo !== null && /* @__PURE__ */ jsx(ConfirmSwitchAlert, {
1589
1632
  switchTo,
1590
1633
  onRequestClose: () => setSwitchTo(null)
@@ -1772,6 +1815,8 @@ const isMeteredEntitlement = (entitlement) => {
1772
1815
  return "balance" in entitlement;
1773
1816
  };
1774
1817
  const UsageItem = ({ meter, item, subscription }) => {
1818
+ const cadence = item?.billingCadence ?? subscription?.billingCadence;
1819
+ const billingPeriod = cadence ? formatDurationAdjective(cadence) : "monthly";
1775
1820
  const isSoftLimit = item?.included?.entitlement?.isSoftLimit ?? true;
1776
1821
  const rate = (item?.price?.tiers?.find((t) => !t.upToAmount) ?? item?.price?.tiers?.at(-1))?.unitPrice?.amount;
1777
1822
  const hasOverage = meter.overage > 0;
@@ -1786,7 +1831,11 @@ const UsageItem = ({ meter, item, subscription }) => {
1786
1831
  className: "mb-4",
1787
1832
  children: [
1788
1833
  /* @__PURE__ */ jsx(AlertTriangleIcon, { className: "size-4 text-red-600 shrink-0" }),
1789
- /* @__PURE__ */ jsx(AlertTitle, { children: "You've exceeded your monthly quota" }),
1834
+ /* @__PURE__ */ jsxs(AlertTitle, { children: [
1835
+ "You've exceeded your ",
1836
+ billingPeriod,
1837
+ " quota"
1838
+ ] }),
1790
1839
  /* @__PURE__ */ jsxs(AlertDescription, { children: [
1791
1840
  "Additional usage is being charged at the overage rate",
1792
1841
  rate ? ` ($${Number(rate).toFixed(2)}/call)` : "",
@@ -1807,7 +1856,11 @@ const UsageItem = ({ meter, item, subscription }) => {
1807
1856
  className: "mb-4",
1808
1857
  children: [
1809
1858
  /* @__PURE__ */ jsx(AlertTriangleIcon, { className: "size-4 text-red-600 shrink-0" }),
1810
- /* @__PURE__ */ jsx(AlertTitle, { children: "You've reached your monthly limit" }),
1859
+ /* @__PURE__ */ jsxs(AlertTitle, { children: [
1860
+ "You've reached your ",
1861
+ billingPeriod,
1862
+ " limit"
1863
+ ] }),
1811
1864
  /* @__PURE__ */ jsx(AlertDescription, { children: "Requests beyond your quota are blocked. Upgrade to a higher plan for more usage." }),
1812
1865
  subscription && /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(SwitchPlanModal, {
1813
1866
  subscription,
@@ -1857,7 +1910,7 @@ const UsageItem = ({ meter, item, subscription }) => {
1857
1910
  }),
1858
1911
  /* @__PURE__ */ jsxs("p", {
1859
1912
  className: "text-xs text-muted-foreground",
1860
- children: [meter.balance.toLocaleString(), " remaining this month"]
1913
+ children: [meter.balance.toLocaleString(), " remaining this billing period"]
1861
1914
  })
1862
1915
  ]
1863
1916
  })]
@@ -2004,7 +2057,7 @@ const SubscriptionsList = ({ subscriptions, activeSubscriptionId }) => {
2004
2057
  const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
2005
2058
  const willExpire = subscription.activeTo && new Date(subscription.activeTo) > /* @__PURE__ */ new Date();
2006
2059
  return /* @__PURE__ */ jsx(Link$1, {
2007
- to: `/subscriptions/${subscription.id}`,
2060
+ to: `/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`,
2008
2061
  children: /* @__PURE__ */ jsx(Item, {
2009
2062
  size: "sm",
2010
2063
  variant: "outline",
@@ -2029,7 +2082,7 @@ const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
2029
2082
  ]
2030
2083
  })] })
2031
2084
  }, subscription.id)
2032
- }, subscription.id);
2085
+ });
2033
2086
  };
2034
2087
 
2035
2088
  //#endregion
@@ -2037,7 +2090,8 @@ const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
2037
2090
  const SubscriptionsPage = () => {
2038
2091
  const deploymentName = useDeploymentName();
2039
2092
  const { data } = useSubscriptions(deploymentName);
2040
- const { subscriptionId } = useParams();
2093
+ const [searchParams] = useSearchParams();
2094
+ const subscriptionId = searchParams.get("subscriptionId");
2041
2095
  const subscriptions = data?.items ?? [];
2042
2096
  const activeSubscription = useMemo(() => {
2043
2097
  if (subscriptions.length === 0) return null;
@@ -2079,7 +2133,7 @@ const SubscriptionsPage = () => {
2079
2133
  //#endregion
2080
2134
  //#region src/ZuploMonetizationPlugin.tsx
2081
2135
  const PRICING_PATH = "/pricing";
2082
- const zuploMonetizationPlugin = createPlugin((options) => ({
2136
+ const zuploMonetizationPlugin = createPlugin((options = {}) => ({
2083
2137
  transformConfig: ({ config, merge }) => merge({
2084
2138
  apiKeys: { enabled: false },
2085
2139
  header: { navigation: [...config.header?.navigation ?? [], {
@@ -2115,11 +2169,11 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
2115
2169
  }],
2116
2170
  getRoutes: () => {
2117
2171
  return [{
2118
- Component: ZuploMonetizationWrapper,
2172
+ element: /* @__PURE__ */ jsx(ZuploMonetizationWrapper, { options }),
2119
2173
  handle: { layout: "none" },
2120
2174
  children: [
2121
2175
  {
2122
- path: "/checkout/:planId?",
2176
+ path: "/checkout",
2123
2177
  element: /* @__PURE__ */ jsx(CheckoutPage, {})
2124
2178
  },
2125
2179
  {
@@ -2137,16 +2191,11 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
2137
2191
  {
2138
2192
  path: PRICING_PATH,
2139
2193
  handle: { layout: "default" },
2140
- element: /* @__PURE__ */ jsx(PricingPage, {
2141
- subtitle: options?.pricing?.subtitle,
2142
- title: options?.pricing?.title,
2143
- units: options?.pricing?.units,
2144
- showYearlyPrice: options?.pricing?.showYearlyPrice
2145
- })
2194
+ element: /* @__PURE__ */ jsx(PricingPage, {})
2146
2195
  },
2147
2196
  {
2148
2197
  handle: { layout: "default" },
2149
- path: "/subscriptions/:subscriptionId?",
2198
+ path: "/subscriptions",
2150
2199
  element: /* @__PURE__ */ jsx(SubscriptionsPage, {})
2151
2200
  }
2152
2201
  ]
@@ -2154,10 +2203,10 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
2154
2203
  },
2155
2204
  getProtectedRoutes: () => {
2156
2205
  return [
2157
- "/checkout/*",
2206
+ "/checkout",
2158
2207
  "/checkout-confirm",
2159
2208
  "/subscription-change-confirm",
2160
- "/subscriptions/*",
2209
+ "/subscriptions",
2161
2210
  "/manage-payment"
2162
2211
  ];
2163
2212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuplo/zudoku-plugin-monetization",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
@@ -31,7 +31,7 @@
31
31
  "react": "19.2.4",
32
32
  "react-dom": "19.2.4",
33
33
  "tsdown": "0.20.3",
34
- "zudoku": "0.71.3"
34
+ "zudoku": "0.71.10"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=19.2.0",