@zuplo/zudoku-plugin-monetization 0.0.46 → 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
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
@@ -550,11 +566,7 @@ const ManagePaymentPage = () => {
550
566
  };
551
567
  //#endregion
552
568
  //#region src/queries.ts
553
- const getDeploymentName = (context) => {
554
- const deploymentName = context.env.ZUPLO_PUBLIC_DEPLOYMENT_NAME;
555
- if (!deploymentName) throw new Error("ZUPLO_PUBLIC_DEPLOYMENT_NAME is not set");
556
- return deploymentName;
557
- };
569
+ const getDeploymentName = (context) => resolveDeploymentName(context.env.ZUPLO_PUBLIC_DEPLOYMENT_NAME);
558
570
  const pricingPageQuery = (context) => queryOptions({
559
571
  queryKey: [`/v3/zudoku-metering/${getDeploymentName(context)}/pricing-page`],
560
572
  meta: { context: context.getAuthState().isAuthenticated ? context : void 0 }
@@ -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;
@@ -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.46",
3
+ "version": "0.0.47",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",