@zuplo/zudoku-plugin-monetization 0.0.45 → 0.0.47

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.
@@ -148,6 +148,13 @@ const formatTieredPriceBreakdown = (opts) => {
148
148
  const tierHasPositivePrice = (tier) => parseFloat(tier.flatPrice?.amount ?? "0") > 0 || parseFloat(tier.unitPrice?.amount ?? "0") > 0;
149
149
  //#endregion
150
150
  //#region src/utils/categorizeRateCards.ts
151
+ const formatInlinePrice = (opts) => {
152
+ const unit = parseFloat(opts.unitAmount ?? "0");
153
+ const flat = parseFloat(opts.flatAmount ?? "0");
154
+ const flatPart = flat > 0 ? formatPrice(flat, opts.currency) : "";
155
+ const unitPart = unit > 0 ? `${formatPrice(unit, opts.currency)}/${opts.unitLabel}` : "";
156
+ return (flatPart && unitPart ? `${flatPart} + ${unitPart}` : flatPart || unitPart) || void 0;
157
+ };
151
158
  const categorizeRateCards = (rateCards, options) => {
152
159
  const { currency, units, planBillingCadence } = options ?? {};
153
160
  const quotas = [];
@@ -163,8 +170,10 @@ const categorizeRateCards = (rateCards, options) => {
163
170
  return "month";
164
171
  };
165
172
  const firstTier = rc.price?.type === "tiered" && rc.price.tiers.length > 0 ? rc.price.tiers[0] : void 0;
166
- const firstTierIsPriced = !!firstTier && tierHasPositivePrice(firstTier);
167
- if (et.type === "metered" && et.issueAfterReset != null && !firstTierIsPriced) {
173
+ const billsFromFirstUnit = !!firstTier && tierHasPositivePrice(firstTier) || rc.price?.type === "unit" && parseFloat(rc.price.amount) > 0;
174
+ const includedQuota = et.type === "metered" ? et.issueAfterReset ?? 0 : 0;
175
+ const isHardCap = et.type === "metered" && et.isSoftLimit === false;
176
+ if (et.type === "metered" && (isHardCap || includedQuota > 0 && !billsFromFirstUnit)) {
168
177
  let tierPrices;
169
178
  if (rc.price?.type === "tiered" && rc.price.tiers) tierPrices = formatTieredPriceBreakdown({
170
179
  tiers: rc.price.tiers.map((t) => ({
@@ -176,12 +185,24 @@ const categorizeRateCards = (rateCards, options) => {
176
185
  unitLabel: unitLabelFor(rc),
177
186
  includedLabel: "Included"
178
187
  });
188
+ const inlinePrice = rc.price?.type === "unit" ? formatInlinePrice({
189
+ unitAmount: rc.price.amount,
190
+ currency,
191
+ unitLabel: unitLabelFor(rc)
192
+ }) : rc.price?.type === "tiered" && rc.price.tiers.length === 1 ? formatInlinePrice({
193
+ flatAmount: rc.price.tiers[0].flatPrice?.amount,
194
+ unitAmount: rc.price.tiers[0].unitPrice?.amount,
195
+ currency,
196
+ unitLabel: unitLabelFor(rc)
197
+ }) : void 0;
179
198
  quotas.push({
180
199
  key: rc.featureKey ?? rc.key,
181
200
  name: rc.name,
182
- limit: et.issueAfterReset,
201
+ limit: includedQuota,
183
202
  period: periodFor(rc),
184
- tierPrices
203
+ tierPrices,
204
+ ...inlinePrice ? { unitPrice: inlinePrice } : {},
205
+ ...isHardCap ? { isHardCap: true } : {}
185
206
  });
186
207
  } else if (et.type === "metered" && rc.type === "usage_based" && rc.price) {
187
208
  const unitLabel = unitLabelFor(rc);
@@ -195,11 +216,12 @@ const categorizeRateCards = (rateCards, options) => {
195
216
  continue;
196
217
  }
197
218
  if (tiers.length === 1) {
198
- const unit = parseFloat(tiers[0].unitPrice?.amount ?? "0");
199
- const flat = parseFloat(tiers[0].flatPrice?.amount ?? "0");
200
- const flatPart = flat > 0 ? formatPrice(flat, currency) : "";
201
- const unitPart = unit > 0 ? `${formatPrice(unit, currency)}/${unitLabel}` : "";
202
- const pricePart = flatPart && unitPart ? `${flatPart} + ${unitPart}` : flatPart || unitPart;
219
+ const pricePart = formatInlinePrice({
220
+ flatAmount: tiers[0].flatPrice?.amount,
221
+ unitAmount: tiers[0].unitPrice?.amount,
222
+ currency,
223
+ unitLabel
224
+ });
203
225
  if (pricePart) {
204
226
  quotas.push({
205
227
  key: rc.featureKey ?? rc.key,
@@ -249,7 +271,11 @@ const categorizeRateCards = (rateCards, options) => {
249
271
  key: rc.featureKey ?? rc.key,
250
272
  name: rc.name
251
273
  });
252
- } else if (et.type === "boolean") features.push({
274
+ } else if (et.type === "metered") features.push({
275
+ key: rc.featureKey ?? rc.key,
276
+ name: rc.name
277
+ });
278
+ else if (et.type === "boolean") features.push({
253
279
  key: rc.featureKey ?? rc.key,
254
280
  name: rc.name
255
281
  });
@@ -520,7 +546,7 @@ const FeatureItem = ({ feature, className }) => {
520
546
  //#region src/pricing-ui/QuotaItem.tsx
521
547
  const QuotaItem = ({ quota, className }) => {
522
548
  const hasTierBreakdown = !!quota.tierPrices && quota.tierPrices.length > 0;
523
- const showQuotaLine = !quota.isPayg && !hasTierBreakdown;
549
+ const showQuotaLine = !quota.isPayg && (quota.isHardCap || !hasTierBreakdown);
524
550
  return /* @__PURE__ */ jsxs("div", {
525
551
  className: cn("flex items-start gap-2", className),
526
552
  children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsxs("div", {
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { D as formatDurationAdjective, E as formatDuration, O as formatDurationInterval, T as formatPrice, _ as formatPlanPrice, a as planHasDefaultTaxBehavior, b as sameEntitlementSet, c as getPlanPriceSchedule, d as PlanEntitlements, f as PlanPhaseHeader, l as PlanPriceTag, o as subscriptionTaxLegendSentence, p as EntitlementList, r as isCustomPlan, t as PricingTable, u as PlanPriceSchedule, w as formatMinorCurrencyAmount, x as categorizeRateCards, y as comparePlanEntitlements } from "./PricingTable-WkG2n7V-.mjs";
1
+ import { D as formatDurationAdjective, E as formatDuration, O as formatDurationInterval, T as formatPrice, _ as formatPlanPrice, a as planHasDefaultTaxBehavior, b as sameEntitlementSet, c as getPlanPriceSchedule, d as PlanEntitlements, f as PlanPhaseHeader, l as PlanPriceTag, o as subscriptionTaxLegendSentence, p as EntitlementList, r as isCustomPlan, t as PricingTable, u as PlanPriceSchedule, w as formatMinorCurrencyAmount, x as categorizeRateCards, y as comparePlanEntitlements } from "./PricingTable-Ec-BAGAS.mjs";
2
2
  import { cn, createPlugin, joinUrl, throwIfProblemJson } from "zudoku";
3
3
  import { AlertTriangleIcon, ArrowDownIcon, ArrowLeftRightIcon, ArrowUpIcon, BadgePercentIcon, CalendarIcon, CheckCheckIcon, CheckIcon, ChevronDownIcon, CircleSlashIcon, ClockIcon, CreditCardIcon, Grid2x2XIcon, HistoryIcon, InfoIcon, Loader2Icon, LockIcon, MoreVerticalIcon, RefreshCcw, RefreshCwIcon, Settings, ShieldIcon, StarsIcon, Trash2Icon, XIcon } from "zudoku/icons";
4
- import { Link, Outlet, useLocation, useNavigate, useSearchParams } from "zudoku/router";
4
+ import { Link, Navigate, Outlet, useLocation, useNavigate, useSearchParams } from "zudoku/router";
5
5
  import { useAuth, useZudoku } from "zudoku/hooks";
6
6
  import { QueryClient, QueryClientProvider, queryOptions, useMutation, useQuery, useQueryClient, useSuspenseQuery } from "zudoku/react-query";
7
- import { Button, ClientOnly, Head, Heading, Link as Link$1, Slot } from "zudoku/components";
7
+ import { Button, ClientOnly, Head, Heading, Link as Link$1, Slot, ZudokuError } from "zudoku/components";
8
8
  import { createContext, use, useEffect, useMemo, useState } from "react";
9
9
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
10
10
  import { Alert, AlertAction, AlertDescription, AlertTitle } from "zudoku/ui/Alert";
@@ -40,11 +40,27 @@ const isTaxInclusiveFromPurchaseDetails = (response) => {
40
40
  return response.tax?.taxInclusive === true;
41
41
  };
42
42
  //#endregion
43
+ //#region src/deploymentName.ts
44
+ /**
45
+ * Resolves the Zuplo deployment name from the environment.
46
+ *
47
+ * `ZUPLO_PUBLIC_DEPLOYMENT_NAME` is populated by `zuplo link`, so a missing
48
+ * value almost always means the project hasn't been linked yet (the common
49
+ * case when running the docs locally). Throw a `ZudokuError` with actionable
50
+ * guidance rather than a bare "not set" so the UI can surface the fix.
51
+ */
52
+ const resolveDeploymentName = (deploymentName) => {
53
+ const trimmed = deploymentName?.trim();
54
+ if (!trimmed) throw new ZudokuError("This project is not linked to a Zuplo deployment, so the monetization plugin can't load pricing or subscriptions.", {
55
+ title: "Not linked to a Zuplo deployment",
56
+ developerHint: "Run `zuplo link` to connect this project to your Zuplo deployment. This sets the `ZUPLO_PUBLIC_DEPLOYMENT_NAME` environment variable that the monetization plugin needs."
57
+ });
58
+ return trimmed;
59
+ };
60
+ //#endregion
43
61
  //#region src/hooks/useDeploymentName.ts
44
62
  const useDeploymentName = () => {
45
- const deploymentName = useZudoku().env.ZUPLO_PUBLIC_DEPLOYMENT_NAME;
46
- if (!deploymentName) throw new Error("ZUPLO_PUBLIC_DEPLOYMENT_NAME is not set");
47
- return deploymentName;
63
+ return resolveDeploymentName(useZudoku().env.ZUPLO_PUBLIC_DEPLOYMENT_NAME);
48
64
  };
49
65
  //#endregion
50
66
  //#region src/hooks/usePurchaseDetails.ts
@@ -98,7 +114,8 @@ const queryClient = new QueryClient({ defaultOptions: {
98
114
  ...q.meta?.request?.headers
99
115
  }
100
116
  });
101
- const response = await fetch(q.meta?.context ? await q.meta.context.signRequest(request) : request);
117
+ const queryContext = q.meta?.context;
118
+ const response = await fetch(queryContext?.getAuthState().isAuthenticated ? await queryContext.signRequest(request) : request);
102
119
  await throwIfProblemJson(response);
103
120
  if (!response.ok) throw new Error("Failed to fetch request");
104
121
  return response.json();
@@ -128,7 +145,8 @@ const queryClient = new QueryClient({ defaultOptions: {
128
145
  ...init.headers
129
146
  }
130
147
  });
131
- const response = await fetch(m.meta?.context ? await m.meta.context.signRequest(request) : request);
148
+ const mutationContext = m.meta?.context;
149
+ const response = await fetch(mutationContext?.getAuthState().isAuthenticated ? await mutationContext.signRequest(request) : request);
132
150
  await throwIfProblemJson(response);
133
151
  if (!response.ok) {
134
152
  const errorText = await response.text();
@@ -451,17 +469,13 @@ const useUrlUtils = () => {
451
469
  };
452
470
  //#endregion
453
471
  //#region src/pages/CheckoutPage.tsx
454
- const CheckoutPage = () => {
455
- const [searchParams] = useSearchParams();
456
- const planId = searchParams.get("planId");
472
+ const CheckoutRedirect = ({ planId }) => {
457
473
  const zudoku = useZudoku();
458
474
  const auth = useAuth();
459
475
  const { generateUrl } = useUrlUtils();
460
- const deploymentName = useDeploymentName();
461
- if (!planId) throw new Error(`missing planId in URL`);
462
476
  const checkoutLink = useQuery({
463
477
  queryKey: [
464
- `/v3/zudoku-metering/${deploymentName}/stripe/checkout`,
478
+ `/v3/zudoku-metering/${useDeploymentName()}/stripe/checkout`,
465
479
  planId,
466
480
  auth.profile?.sub
467
481
  ],
@@ -499,6 +513,15 @@ const CheckoutPage = () => {
499
513
  })
500
514
  });
501
515
  };
516
+ const CheckoutPage = () => {
517
+ const [searchParams] = useSearchParams();
518
+ const planId = searchParams.get("planId");
519
+ if (!planId) return /* @__PURE__ */ jsx(Navigate, {
520
+ to: "/pricing",
521
+ replace: true
522
+ });
523
+ return /* @__PURE__ */ jsx(CheckoutRedirect, { planId });
524
+ };
502
525
  //#endregion
503
526
  //#region src/pages/ManagePaymentPage.tsx
504
527
  const ManagePaymentPage = () => {
@@ -543,11 +566,7 @@ const ManagePaymentPage = () => {
543
566
  };
544
567
  //#endregion
545
568
  //#region src/queries.ts
546
- const getDeploymentName = (context) => {
547
- const deploymentName = context.env.ZUPLO_PUBLIC_DEPLOYMENT_NAME;
548
- if (!deploymentName) throw new Error("ZUPLO_PUBLIC_DEPLOYMENT_NAME is not set");
549
- return deploymentName;
550
- };
569
+ const getDeploymentName = (context) => resolveDeploymentName(context.env.ZUPLO_PUBLIC_DEPLOYMENT_NAME);
551
570
  const pricingPageQuery = (context) => queryOptions({
552
571
  queryKey: [`/v3/zudoku-metering/${getDeploymentName(context)}/pricing-page`],
553
572
  meta: { context: context.getAuthState().isAuthenticated ? context : void 0 }
@@ -2745,6 +2764,9 @@ const zuploMonetizationPlugin = createPlugin((options = {}) => ({
2745
2764
  queryClient.prefetchQuery(pricingPageQuery(context));
2746
2765
  if (context.getAuthState().isAuthenticated) queryClient.prefetchQuery(subscriptionsQuery(context));
2747
2766
  },
2767
+ events: { auth: ({ prev, next }) => {
2768
+ if (prev.isAuthenticated && !next.isAuthenticated) queryClient.removeQueries();
2769
+ } },
2748
2770
  getIdentities: async (context) => {
2749
2771
  if (!context.getAuthState().isAuthenticated) return [];
2750
2772
  return (await queryClient.fetchQuery(subscriptionsQuery(context))).items.flatMap((sub) => sub.status !== "active" ? [] : sub.consumer.apiKeys.flatMap((apiKey) => apiKey.expiresOn && new Date(apiKey.expiresOn) < /* @__PURE__ */ new Date() ? [] : {
@@ -87,6 +87,7 @@ interface Quota {
87
87
  tierPrices?: string[];
88
88
  isPayg?: boolean;
89
89
  unitPrice?: string;
90
+ isHardCap?: boolean;
90
91
  }
91
92
  interface Feature {
92
93
  key: string;
@@ -139,7 +140,7 @@ declare const FeatureItem: ({
139
140
  }: {
140
141
  feature: Feature;
141
142
  className?: string;
142
- }) => import("react/jsx-runtime").JSX.Element;
143
+ }) => import("react").JSX.Element;
143
144
  //#endregion
144
145
  //#region src/pricing-ui/PlanEntitlements.d.ts
145
146
  /**
@@ -161,7 +162,7 @@ declare const PlanEntitlements: ({
161
162
  billingCadence?: string;
162
163
  units?: Record<string, string>;
163
164
  itemClassName?: string;
164
- }) => import("react/jsx-runtime").JSX.Element;
165
+ }) => import("react").JSX.Element;
165
166
  //#endregion
166
167
  //#region src/utils/formatPlanPrice.d.ts
167
168
  type PlanPriceLabel = {
@@ -225,7 +226,7 @@ declare const PlanPriceSchedule: ({
225
226
  billingCadence?: string;
226
227
  size?: "sm" | "lg";
227
228
  className?: string;
228
- }) => import("react/jsx-runtime").JSX.Element;
229
+ }) => import("react").JSX.Element;
229
230
  //#endregion
230
231
  //#region src/pricing-ui/PlanPriceTag.d.ts
231
232
  /**
@@ -253,7 +254,7 @@ declare const PlanPriceTag: ({
253
254
  billingCadence?: string;
254
255
  description?: boolean;
255
256
  size?: "inline" | "lg";
256
- }) => import("react/jsx-runtime").JSX.Element;
257
+ }) => import("react").JSX.Element;
257
258
  //#endregion
258
259
  //#region src/pricing-ui/PricingCard.d.ts
259
260
  type PricingCardProps = {
@@ -269,7 +270,7 @@ declare const PricingCard: ({
269
270
  units,
270
271
  action,
271
272
  className
272
- }: PricingCardProps) => import("react/jsx-runtime").JSX.Element | null;
273
+ }: PricingCardProps) => import("react").JSX.Element | null;
273
274
  //#endregion
274
275
  //#region src/pricing-ui/PricingTable.d.ts
275
276
  type PricingTableProps = {
@@ -319,7 +320,7 @@ declare const PricingTable: ({
319
320
  showTaxLegend,
320
321
  className,
321
322
  cardClassName
322
- }: PricingTableProps) => import("react/jsx-runtime").JSX.Element;
323
+ }: PricingTableProps) => import("react").JSX.Element;
323
324
  //#endregion
324
325
  //#region src/pricing-ui/QuotaItem.d.ts
325
326
  declare const QuotaItem: ({
@@ -328,7 +329,7 @@ declare const QuotaItem: ({
328
329
  }: {
329
330
  quota: Quota;
330
331
  className?: string;
331
- }) => import("react/jsx-runtime").JSX.Element;
332
+ }) => import("react").JSX.Element;
332
333
  //#endregion
333
334
  //#region src/utils/categorizeRateCards.d.ts
334
335
  declare const categorizeRateCards: (rateCards: RateCard[], options?: {
@@ -1,2 +1,2 @@
1
- import { C as formatStaticEntitlementConfig, D as formatDurationAdjective, E as formatDuration, O as formatDurationInterval, S as formatTieredPriceBreakdown, T as formatPrice, _ as formatPlanPrice, a as planHasDefaultTaxBehavior, c as getPlanPriceSchedule, d as PlanEntitlements, g as getPhasePriceLabel, h as FeatureItem, i as collectDefaultTaxBehaviors, l as PlanPriceTag, m as QuotaItem, n as PricingCard, o as subscriptionTaxLegendSentence, s as taxBehaviorLegendSentence, t as PricingTable, u as PlanPriceSchedule, v as getPlanPrice, w as formatMinorCurrencyAmount, x as categorizeRateCards } from "./PricingTable-WkG2n7V-.mjs";
1
+ import { C as formatStaticEntitlementConfig, D as formatDurationAdjective, E as formatDuration, O as formatDurationInterval, S as formatTieredPriceBreakdown, T as formatPrice, _ as formatPlanPrice, a as planHasDefaultTaxBehavior, c as getPlanPriceSchedule, d as PlanEntitlements, g as getPhasePriceLabel, h as FeatureItem, i as collectDefaultTaxBehaviors, l as PlanPriceTag, m as QuotaItem, n as PricingCard, o as subscriptionTaxLegendSentence, s as taxBehaviorLegendSentence, t as PricingTable, u as PlanPriceSchedule, v as getPlanPrice, w as formatMinorCurrencyAmount, x as categorizeRateCards } from "./PricingTable-Ec-BAGAS.mjs";
2
2
  export { FeatureItem, PlanEntitlements, PlanPriceSchedule, PlanPriceTag, PricingCard, PricingTable, QuotaItem, categorizeRateCards, collectDefaultTaxBehaviors, formatDuration, formatDurationAdjective, formatDurationInterval, formatMinorCurrencyAmount, formatPlanPrice, formatPrice, formatStaticEntitlementConfig, formatTieredPriceBreakdown, getPhasePriceLabel, getPlanPrice, getPlanPriceSchedule, planHasDefaultTaxBehavior, subscriptionTaxLegendSentence, taxBehaviorLegendSentence };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuplo/zudoku-plugin-monetization",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
@@ -31,13 +31,14 @@
31
31
  "@testing-library/dom": "10.4.1",
32
32
  "@testing-library/jest-dom": "6.9.1",
33
33
  "@testing-library/react": "16.3.2",
34
- "@types/react": "19.2.14",
34
+ "@types/react": "19.2.17",
35
35
  "@types/react-dom": "19.2.3",
36
+ "@typescript/native-preview": "7.0.0-dev.20260624.1",
36
37
  "happy-dom": "20.10.2",
37
- "react": "19.2.5",
38
- "react-dom": "19.2.5",
38
+ "react": "19.2.7",
39
+ "react-dom": "19.2.7",
39
40
  "tsdown": "0.22.0",
40
- "zudoku": "0.81.0"
41
+ "zudoku": "0.82.3"
41
42
  },
42
43
  "peerDependencies": {
43
44
  "react": ">=19.2.0",
@@ -52,6 +53,6 @@
52
53
  "scripts": {
53
54
  "build": "tsdown",
54
55
  "dev": "tsdown --watch",
55
- "typecheck": "tsc --project tsconfig.json"
56
+ "typecheck": "tsgo --project tsconfig.json"
56
57
  }
57
58
  }