@zuplo/zudoku-plugin-monetization 0.0.21 → 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,
@@ -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, 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: [
@@ -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 }) => {
657
689
  const zudoku = useZudoku();
658
690
  const deploymentName = useDeploymentName();
659
691
  const auth = useAuth();
@@ -686,6 +718,7 @@ 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,
689
722
  isPopular: plan.metadata?.zuplo_most_popular === "true",
690
723
  isSubscribed: subscriptions.items.some((subscription) => ["active", "canceled"].includes(subscription.status))
691
724
  }, plan.id))
@@ -1101,9 +1134,8 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1101
1134
 
1102
1135
  //#endregion
1103
1136
  //#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;
1137
+ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex) => {
1138
+ const isUpgrade = targetIndex > currentIndex;
1107
1139
  const currentPhase = currentPlan?.phases.at(-1);
1108
1140
  const targetPhase = targetPlan.phases.at(-1);
1109
1141
  const { quotas: currentQuotas, features: currentFeatures } = currentPhase ? categorizeRateCards(currentPhase.rateCards, currentPlan?.currency) : {
@@ -1391,8 +1423,11 @@ const SwitchPlanModal = ({ subscription, children }) => {
1391
1423
  downgrades: [],
1392
1424
  privatePlans: []
1393
1425
  };
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));
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
+ });
1396
1431
  return {
1397
1432
  upgrades: allComparisons.filter((c) => c.isUpgrade && !isPrivatePlan(c.plan)),
1398
1433
  downgrades: allComparisons.filter((c) => !c.isUpgrade && !isPrivatePlan(c.plan)),
@@ -1902,16 +1937,12 @@ const SubscriptionsPage = () => {
1902
1937
  //#region src/ZuploMonetizationPlugin.tsx
1903
1938
  const PRICING_PATH = "/pricing";
1904
1939
  const zuploMonetizationPlugin = createPlugin((options) => ({
1905
- transformConfig: ({ merge }) => merge({
1940
+ transformConfig: ({ config, merge }) => merge({
1906
1941
  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
- }) }
1942
+ header: { navigation: [...config.header?.navigation ?? [], {
1943
+ label: "Pricing",
1944
+ to: PRICING_PATH
1945
+ }] }
1915
1946
  }),
1916
1947
  getIdentities: async (context) => {
1917
1948
  const deploymentName = context.env.ZUPLO_PUBLIC_DEPLOYMENT_NAME;
@@ -1961,7 +1992,8 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
1961
1992
  handle: { layout: "default" },
1962
1993
  element: /* @__PURE__ */ jsx(PricingPage, {
1963
1994
  subtitle: options?.pricing?.subtitle,
1964
- title: options?.pricing?.title
1995
+ title: options?.pricing?.title,
1996
+ units: options?.pricing?.units
1965
1997
  })
1966
1998
  },
1967
1999
  {
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.22",
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.70.3"
34
+ "zudoku": "0.70.4"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=19.2.0",