@zuplo/zudoku-plugin-monetization 0.0.20 → 0.0.22

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
@@ -5,6 +5,7 @@ type ZudokuMonetizationPluginOptions = {
5
5
  pricing?: {
6
6
  subtitle?: string;
7
7
  title?: string;
8
+ units?: Record<string, string>;
8
9
  };
9
10
  };
10
11
  declare const zuploMonetizationPlugin: (options?: ZudokuMonetizationPluginOptions | undefined) => zudoku.ZudokuPlugin;
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { cn, createPlugin, joinUrl } from "zudoku";
2
- import { Button, ClientOnly, Head, Heading, Link } from "zudoku/components";
3
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";
4
- import { Link as Link$1, Outlet, useLocation, useNavigate, useParams, useSearchParams } from "zudoku/router";
3
+ import { Button, ClientOnly, Head, Heading, Link } from "zudoku/components";
5
4
  import { useAuth, useZudoku } from "zudoku/hooks";
6
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";
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";
@@ -127,15 +127,17 @@ const formatDurationInterval = (iso) => {
127
127
 
128
128
  //#endregion
129
129
  //#region src/utils/formatPrice.ts
130
- const formatPrice = (amount, currency) => new Intl.NumberFormat(void 0, {
130
+ const formatPrice = (amount, currency) => new Intl.NumberFormat("en-US", {
131
131
  style: "currency",
132
- currency: currency || "USD",
133
- minimumFractionDigits: 0
132
+ currency: currency ?? "USD",
133
+ minimumFractionDigits: 2,
134
+ maximumFractionDigits: 6,
135
+ trailingZeroDisplay: "stripIfInteger"
134
136
  }).format(amount);
135
137
 
136
138
  //#endregion
137
139
  //#region src/utils/categorizeRateCards.ts
138
- const categorizeRateCards = (rateCards, currency) => {
140
+ const categorizeRateCards = (rateCards, currency, units) => {
139
141
  const quotas = [];
140
142
  const features = [];
141
143
  for (const rc of rateCards) {
@@ -144,8 +146,12 @@ const categorizeRateCards = (rateCards, currency) => {
144
146
  if (et.type === "metered" && et.issueAfterReset != null) {
145
147
  let overagePrice;
146
148
  if (et.isSoftLimit !== false && rc.price?.type === "tiered" && rc.price.tiers) {
147
- const overageTier = rc.price.tiers.find((t) => t.unitPrice?.amount);
148
- if (overageTier?.unitPrice) overagePrice = `${formatPrice(parseFloat(overageTier.unitPrice.amount), currency)}/unit`;
149
+ const overageTier = rc.price.tiers.find((t) => t.unitPrice?.amount && parseFloat(t.unitPrice.amount) > 0);
150
+ if (overageTier?.unitPrice) {
151
+ const amount = parseFloat(overageTier.unitPrice.amount);
152
+ const unitLabel = units?.[rc.key] ?? units?.[rc.featureKey ?? ""] ?? "unit";
153
+ overagePrice = `${formatPrice(amount, currency)}/${unitLabel}`;
154
+ }
149
155
  }
150
156
  quotas.push({
151
157
  key: rc.featureKey ?? rc.key,
@@ -391,7 +397,8 @@ const CheckoutConfirmPage = () => {
391
397
  }), /* @__PURE__ */ jsx(Button, {
392
398
  variant: "ghost",
393
399
  className: "w-full",
394
- asChild: true,
400
+ disabled: createSubscriptionMutation.isPending,
401
+ asChild: !createSubscriptionMutation.isPending,
395
402
  children: /* @__PURE__ */ jsx(Link$1, {
396
403
  to: "/pricing",
397
404
  children: "Cancel"
@@ -572,13 +579,36 @@ const ManagePaymentPage = () => {
572
579
 
573
580
  //#endregion
574
581
  //#region src/pages/pricing/PricingCard.tsx
575
- const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
576
- const defaultPhase = plan.phases.at(-1);
577
- if (!defaultPhase) return null;
578
- const { quotas, features } = categorizeRateCards(defaultPhase.rateCards, plan.currency);
582
+ const PhaseSection = ({ phase, currency, showName, excludeKeys, units }) => {
583
+ const { quotas, features } = categorizeRateCards(phase.rateCards, currency, units);
584
+ const filteredQuotas = quotas.filter((q) => !excludeKeys.has(q.key));
585
+ const filteredFeatures = features.filter((f) => !excludeKeys.has(f.key));
586
+ if (filteredQuotas.length === 0 && filteredFeatures.length === 0) return null;
587
+ return /* @__PURE__ */ jsxs("div", {
588
+ className: "space-y-2",
589
+ children: [
590
+ showName && /* @__PURE__ */ jsxs("div", {
591
+ className: "text-sm font-medium text-card-foreground",
592
+ children: [phase.name, phase.duration && /* @__PURE__ */ jsxs("span", {
593
+ className: "text-muted-foreground font-normal",
594
+ children: [
595
+ " ",
596
+ "— ",
597
+ formatDuration(phase.duration)
598
+ ]
599
+ })]
600
+ }),
601
+ filteredQuotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, { quota }, quota.key)),
602
+ filteredFeatures.map((feature) => /* @__PURE__ */ jsx(FeatureItem, { feature }, feature.key))
603
+ ]
604
+ });
605
+ };
606
+ const PricingCard = ({ plan, isPopular = false, isSubscribed = false, units }) => {
607
+ if (plan.phases.length === 0) return null;
579
608
  const price = getPriceFromPlan(plan);
580
609
  const isFree = price.monthly === 0;
581
610
  const isCustom = plan.metadata?.isCustom === true;
611
+ const hasMultiplePhases = plan.phases.length > 1;
582
612
  return /* @__PURE__ */ jsxs("div", {
583
613
  className: cn("relative rounded-lg border p-6 flex flex-col", isPopular && "border-primary border-2"),
584
614
  children: [
@@ -621,15 +651,18 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
621
651
  })
622
652
  ]
623
653
  }),
624
- /* @__PURE__ */ jsxs("div", {
625
- className: "space-y-4 mb-6 grow",
626
- children: [quotas.length > 0 && /* @__PURE__ */ jsx("div", {
627
- className: "space-y-2",
628
- children: quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, { quota }, quota.key))
629
- }), features.length > 0 && /* @__PURE__ */ jsx("div", {
630
- className: "space-y-2",
631
- children: features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, { feature }, feature.key))
632
- })]
654
+ /* @__PURE__ */ jsx("div", {
655
+ className: "space-y-4 mb-6 grow",
656
+ children: plan.phases.map((phase, index) => {
657
+ const laterKeys = new Set(plan.phases.slice(index + 1).flatMap((p) => p.rateCards.map((rc) => rc.featureKey ?? rc.key)));
658
+ return /* @__PURE__ */ jsx(PhaseSection, {
659
+ phase,
660
+ currency: plan.currency,
661
+ showName: hasMultiplePhases,
662
+ excludeKeys: laterKeys,
663
+ units
664
+ }, phase.key);
665
+ })
633
666
  }),
634
667
  isSubscribed ? /* @__PURE__ */ jsx(Button, {
635
668
  variant: isPopular ? "default" : "secondary",
@@ -652,7 +685,7 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
652
685
 
653
686
  //#endregion
654
687
  //#region src/pages/PricingPage.tsx
655
- const PricingPage = ({ subtitle = "See our pricing options and choose the one that best suits your needs.", title = "Pricing" }) => {
688
+ const PricingPage = ({ subtitle = "See our pricing options and choose the one that best suits your needs.", title = "Pricing", units }) => {
656
689
  const zudoku = useZudoku();
657
690
  const deploymentName = useDeploymentName();
658
691
  const auth = useAuth();
@@ -685,6 +718,7 @@ const PricingPage = ({ subtitle = "See our pricing options and choose the one th
685
718
  className: "w-full grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(300px,max-content))] justify-center gap-6",
686
719
  children: pricingTable.items.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
687
720
  plan,
721
+ units,
688
722
  isPopular: plan.metadata?.zuplo_most_popular === "true",
689
723
  isSubscribed: subscriptions.items.some((subscription) => ["active", "canceled"].includes(subscription.status))
690
724
  }, plan.id))
@@ -1100,9 +1134,8 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1100
1134
 
1101
1135
  //#endregion
1102
1136
  //#region src/pages/subscriptions/SwitchPlanModal.tsx
1103
- const comparePlans = (currentPlan, targetPlan) => {
1104
- const currentPrice = currentPlan ? getPriceFromPlan(currentPlan).monthly : 0;
1105
- const isUpgrade = getPriceFromPlan(targetPlan).monthly > currentPrice;
1137
+ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex) => {
1138
+ const isUpgrade = targetIndex > currentIndex;
1106
1139
  const currentPhase = currentPlan?.phases.at(-1);
1107
1140
  const targetPhase = targetPlan.phases.at(-1);
1108
1141
  const { quotas: currentQuotas, features: currentFeatures } = currentPhase ? categorizeRateCards(currentPhase.rateCards, currentPlan?.currency) : {
@@ -1390,8 +1423,11 @@ const SwitchPlanModal = ({ subscription, children }) => {
1390
1423
  downgrades: [],
1391
1424
  privatePlans: []
1392
1425
  };
1393
- const isPrivatePlan = (plan) => plan.metadata?.zuplo_is_private === "true";
1394
- const allComparisons = plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => comparePlans(currentPlan, plan));
1426
+ const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
1427
+ const currentIndex = plansData.items.findIndex((p) => p.id === currentPlan.id);
1428
+ const allComparisons = plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => {
1429
+ return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan));
1430
+ });
1395
1431
  return {
1396
1432
  upgrades: allComparisons.filter((c) => c.isUpgrade && !isPrivatePlan(c.plan)),
1397
1433
  downgrades: allComparisons.filter((c) => !c.isUpgrade && !isPrivatePlan(c.plan)),
@@ -1901,16 +1937,12 @@ const SubscriptionsPage = () => {
1901
1937
  //#region src/ZuploMonetizationPlugin.tsx
1902
1938
  const PRICING_PATH = "/pricing";
1903
1939
  const zuploMonetizationPlugin = createPlugin((options) => ({
1904
- transformConfig: ({ merge }) => merge({
1940
+ transformConfig: ({ config, merge }) => merge({
1905
1941
  apiKeys: { enabled: false },
1906
- slots: { "head-navigation-start": () => /* @__PURE__ */ jsx(Button, {
1907
- asChild: true,
1908
- variant: "ghost",
1909
- children: /* @__PURE__ */ jsx(Link$1, {
1910
- to: PRICING_PATH,
1911
- children: "Pricing"
1912
- })
1913
- }) }
1942
+ header: { navigation: [...config.header?.navigation ?? [], {
1943
+ label: "Pricing",
1944
+ to: PRICING_PATH
1945
+ }] }
1914
1946
  }),
1915
1947
  getIdentities: async (context) => {
1916
1948
  const deploymentName = context.env.ZUPLO_PUBLIC_DEPLOYMENT_NAME;
@@ -1954,20 +1986,22 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
1954
1986
  {
1955
1987
  path: "/manage-payment",
1956
1988
  element: /* @__PURE__ */ jsx(ManagePaymentPage, {})
1989
+ },
1990
+ {
1991
+ path: PRICING_PATH,
1992
+ handle: { layout: "default" },
1993
+ element: /* @__PURE__ */ jsx(PricingPage, {
1994
+ subtitle: options?.pricing?.subtitle,
1995
+ title: options?.pricing?.title,
1996
+ units: options?.pricing?.units
1997
+ })
1998
+ },
1999
+ {
2000
+ handle: { layout: "default" },
2001
+ path: "/subscriptions/:subscriptionId?",
2002
+ element: /* @__PURE__ */ jsx(SubscriptionsPage, {})
1957
2003
  }
1958
2004
  ]
1959
- }, {
1960
- Component: ZuploMonetizationWrapper,
1961
- children: [{
1962
- path: "/pricing",
1963
- element: /* @__PURE__ */ jsx(PricingPage, {
1964
- subtitle: options?.pricing?.subtitle,
1965
- title: options?.pricing?.title
1966
- })
1967
- }, {
1968
- path: "/subscriptions/:subscriptionId?",
1969
- element: /* @__PURE__ */ jsx(SubscriptionsPage, {})
1970
- }]
1971
2005
  }];
1972
2006
  },
1973
2007
  getProtectedRoutes: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuplo/zudoku-plugin-monetization",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
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.6.1",
30
+ "happy-dom": "20.7.0",
31
31
  "react": "19.2.4",
32
32
  "react-dom": "19.2.4",
33
33
  "tsdown": "0.20.3",
34
- "zudoku": "0.70.2"
34
+ "zudoku": "0.70.4"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=19.2.0",