@zuplo/zudoku-plugin-monetization 0.0.21 → 0.0.23

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,8 @@ type ZudokuMonetizationPluginOptions = {
5
5
  pricing?: {
6
6
  subtitle?: string;
7
7
  title?: string;
8
+ units?: Record<string, string>;
9
+ showYearlyPrice?: boolean;
8
10
  };
9
11
  };
10
12
  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, Slot } 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,
@@ -573,13 +579,36 @@ const ManagePaymentPage = () => {
573
579
 
574
580
  //#endregion
575
581
  //#region src/pages/pricing/PricingCard.tsx
576
- const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
577
- const defaultPhase = plan.phases.at(-1);
578
- if (!defaultPhase) return null;
579
- 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, showYearlyPrice = true, units }) => {
607
+ if (plan.phases.length === 0) return null;
580
608
  const price = getPriceFromPlan(plan);
581
609
  const isFree = price.monthly === 0;
582
610
  const isCustom = plan.metadata?.isCustom === true;
611
+ const hasMultiplePhases = plan.phases.length > 1;
583
612
  return /* @__PURE__ */ jsxs("div", {
584
613
  className: cn("relative rounded-lg border p-6 flex flex-col", isPopular && "border-primary border-2"),
585
614
  children: [
@@ -611,7 +640,7 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
611
640
  }), !isFree && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
612
641
  className: "text-muted-foreground text-sm",
613
642
  children: "/mo"
614
- }), /* @__PURE__ */ jsxs("div", {
643
+ }), showYearlyPrice && /* @__PURE__ */ jsxs("div", {
615
644
  className: "w-full text-sm text-muted-foreground mt-1",
616
645
  children: [formatPrice(price.yearly, plan.currency), "/year"]
617
646
  })] })] })
@@ -622,15 +651,18 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
622
651
  })
623
652
  ]
624
653
  }),
625
- /* @__PURE__ */ jsxs("div", {
626
- className: "space-y-4 mb-6 grow",
627
- children: [quotas.length > 0 && /* @__PURE__ */ jsx("div", {
628
- className: "space-y-2",
629
- children: quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, { quota }, quota.key))
630
- }), features.length > 0 && /* @__PURE__ */ jsx("div", {
631
- className: "space-y-2",
632
- children: features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, { feature }, feature.key))
633
- })]
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
+ })
634
666
  }),
635
667
  isSubscribed ? /* @__PURE__ */ jsx(Button, {
636
668
  variant: isPopular ? "default" : "secondary",
@@ -653,7 +685,7 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
653
685
 
654
686
  //#endregion
655
687
  //#region src/pages/PricingPage.tsx
656
- 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, showYearlyPrice = true }) => {
657
689
  const zudoku = useZudoku();
658
690
  const deploymentName = useDeploymentName();
659
691
  const auth = useAuth();
@@ -686,10 +718,13 @@ const PricingPage = ({ subtitle = "See our pricing options and choose the one th
686
718
  className: "w-full grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(300px,max-content))] justify-center gap-6",
687
719
  children: pricingTable.items.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
688
720
  plan,
721
+ units,
722
+ showYearlyPrice,
689
723
  isPopular: plan.metadata?.zuplo_most_popular === "true",
690
724
  isSubscribed: subscriptions.items.some((subscription) => ["active", "canceled"].includes(subscription.status))
691
725
  }, plan.id))
692
- })
726
+ }),
727
+ /* @__PURE__ */ jsx(Slot.Target, { name: "pricing-page-after" })
693
728
  ]
694
729
  });
695
730
  };
@@ -1101,9 +1136,8 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1101
1136
 
1102
1137
  //#endregion
1103
1138
  //#region src/pages/subscriptions/SwitchPlanModal.tsx
1104
- const comparePlans = (currentPlan, targetPlan) => {
1105
- const currentPrice = currentPlan ? getPriceFromPlan(currentPlan).monthly : 0;
1106
- const isUpgrade = getPriceFromPlan(targetPlan).monthly > currentPrice;
1139
+ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex) => {
1140
+ const isUpgrade = targetIndex > currentIndex;
1107
1141
  const currentPhase = currentPlan?.phases.at(-1);
1108
1142
  const targetPhase = targetPlan.phases.at(-1);
1109
1143
  const { quotas: currentQuotas, features: currentFeatures } = currentPhase ? categorizeRateCards(currentPhase.rateCards, currentPlan?.currency) : {
@@ -1391,8 +1425,11 @@ const SwitchPlanModal = ({ subscription, children }) => {
1391
1425
  downgrades: [],
1392
1426
  privatePlans: []
1393
1427
  };
1394
- const isPrivatePlan = (plan) => plan.metadata?.zuplo_is_private === "true";
1395
- const allComparisons = plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => comparePlans(currentPlan, plan));
1428
+ const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
1429
+ const currentIndex = plansData.items.findIndex((p) => p.id === currentPlan.id);
1430
+ const allComparisons = plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => {
1431
+ return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan));
1432
+ });
1396
1433
  return {
1397
1434
  upgrades: allComparisons.filter((c) => c.isUpgrade && !isPrivatePlan(c.plan)),
1398
1435
  downgrades: allComparisons.filter((c) => !c.isUpgrade && !isPrivatePlan(c.plan)),
@@ -1902,16 +1939,12 @@ const SubscriptionsPage = () => {
1902
1939
  //#region src/ZuploMonetizationPlugin.tsx
1903
1940
  const PRICING_PATH = "/pricing";
1904
1941
  const zuploMonetizationPlugin = createPlugin((options) => ({
1905
- transformConfig: ({ merge }) => merge({
1942
+ transformConfig: ({ config, merge }) => merge({
1906
1943
  apiKeys: { enabled: false },
1907
- slots: { "head-navigation-start": () => /* @__PURE__ */ jsx(Button, {
1908
- asChild: true,
1909
- variant: "ghost",
1910
- children: /* @__PURE__ */ jsx(Link$1, {
1911
- to: PRICING_PATH,
1912
- children: "Pricing"
1913
- })
1914
- }) }
1944
+ header: { navigation: [...config.header?.navigation ?? [], {
1945
+ label: "Pricing",
1946
+ to: PRICING_PATH
1947
+ }] }
1915
1948
  }),
1916
1949
  getIdentities: async (context) => {
1917
1950
  const deploymentName = context.env.ZUPLO_PUBLIC_DEPLOYMENT_NAME;
@@ -1961,7 +1994,9 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
1961
1994
  handle: { layout: "default" },
1962
1995
  element: /* @__PURE__ */ jsx(PricingPage, {
1963
1996
  subtitle: options?.pricing?.subtitle,
1964
- title: options?.pricing?.title
1997
+ title: options?.pricing?.title,
1998
+ units: options?.pricing?.units,
1999
+ showYearlyPrice: options?.pricing?.showYearlyPrice
1965
2000
  })
1966
2001
  },
1967
2002
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuplo/zudoku-plugin-monetization",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
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.7.0",
30
+ "happy-dom": "20.8.3",
31
31
  "react": "19.2.4",
32
32
  "react-dom": "19.2.4",
33
33
  "tsdown": "0.20.3",
34
- "zudoku": "0.70.3"
34
+ "zudoku": "0.71.2"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=19.2.0",