@lukoweb/apitogo-plugin-dev-portal-billing 0.2.54 → 0.2.57

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.
Files changed (21) hide show
  1. package/dist/{AccountPlansPage-0PYGy1aV.mjs → AccountPlansPage-DBBVHGuw.mjs} +14 -8
  2. package/dist/{CheckoutPage-Bpa128i2.mjs → CheckoutPage-CZ9NXp8z.mjs} +7 -2
  3. package/dist/{ContactSalesPage-BslD3Yk4.mjs → ContactSalesPage-D4jX0rtt.mjs} +2 -2
  4. package/dist/{CredentialsPage-C4qpBVrh.mjs → CredentialsPage-CVecoIuQ.mjs} +4 -4
  5. package/dist/{PricingCard-YJIEwydG.mjs → PricingCard-CRlUGh0k.mjs} +9 -3
  6. package/dist/{PricingPage-DIhoFOd9.mjs → PricingPage-BM7NH4VQ.mjs} +15 -14
  7. package/dist/{SubscriptionsPage-D7hsYtTB.mjs → SubscriptionsPage-C58HnDmQ.mjs} +16 -10
  8. package/dist/{UsagePage-Di9kQ_3Z.mjs → UsagePage-C5opT1KD.mjs} +4 -4
  9. package/dist/{account-links-9h91uCGj.mjs → account-links-CpTOzb2V.mjs} +1 -1
  10. package/dist/{billing-state-Dgcc3_pT.mjs → billing-state-CJmy96zI.mjs} +1 -1
  11. package/dist/index.d.mts +9 -2
  12. package/dist/index.mjs +22 -20
  13. package/dist/pending-checkout-DpVaNnTu.mjs +45 -0
  14. package/dist/{useApiKeys-CEwMzqju.mjs → useApiKeys-CnyxipLc.mjs} +1 -1
  15. package/dist/{useDevPortalApiBaseUrl-BstWzITX.mjs → useDevPortalApiBaseUrl-BfpUBayO.mjs} +37 -19
  16. package/dist/{useMySubscriptions-DuVq2tXL.mjs → useMySubscriptions-C1UMb8rJ.mjs} +1 -1
  17. package/dist/{usePlans-BF7Hy2qJ.mjs → usePlans-D_Khso7B.mjs} +5 -4
  18. package/dist/{useUsage-CJ9VIBc7.mjs → useUsage-DeEDgDdj.mjs} +1 -1
  19. package/package.json +3 -3
  20. /package/dist/{page-state-BB-72pbw.mjs → page-state-BLL2JJBc.mjs} +0 -0
  21. /package/dist/{pricing-config-C44AIqql.mjs → pricing-config-Bc6eWFUO.mjs} +0 -0
@@ -1,13 +1,14 @@
1
- import { f as parseManifestPlans, n as useDevPortalPreviewMode, r as useIsDemoSession, s as isActiveSubscription, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
- import { t as PricingCard } from "./PricingCard-YJIEwydG.mjs";
1
+ import { f as parseManifestPlans, n as useDevPortalPreviewMode, r as useIsDemoSession, s as isActiveSubscription, t as useDevPortalApiBaseUrl, y as withPlanFeatures } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
2
+ import { t as PricingCard } from "./PricingCard-CRlUGh0k.mjs";
3
3
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
4
  import { n as readProblemDetail } from "./problem-details-n20-dy3w.mjs";
5
- import { t as useMySubscriptions } from "./useMySubscriptions-DuVq2tXL.mjs";
6
- import { t as usePlans } from "./usePlans-BF7Hy2qJ.mjs";
5
+ import { t as useMySubscriptions } from "./useMySubscriptions-C1UMb8rJ.mjs";
6
+ import { t as usePlans } from "./usePlans-D_Khso7B.mjs";
7
+ import { n as readPendingCheckout, t as clearPendingCheckout } from "./pending-checkout-DpVaNnTu.mjs";
7
8
  import { Head } from "@lukoweb/apitogo/components";
8
9
  import { CreditCardIcon } from "@lukoweb/apitogo/icons";
9
10
  import { useSearchParams } from "@lukoweb/apitogo/router";
10
- import { useState } from "react";
11
+ import { useEffect, useState } from "react";
11
12
  import { jsx, jsxs } from "react/jsx-runtime";
12
13
  import { useMutation, useQueryClient } from "@lukoweb/apitogo/react-query";
13
14
 
@@ -59,17 +60,21 @@ var AccountPlansPage_exports = /* @__PURE__ */ __exportAll({ default: () => Acco
59
60
  const AccountPlansPage = () => {
60
61
  const [searchParams] = useSearchParams();
61
62
  const checkoutCancelled = searchParams.get("checkout") === "cancelled";
62
- const { previewPlans } = useDevPortalBillingConfig();
63
+ const { previewPlans, planFeatures } = useDevPortalBillingConfig();
63
64
  const isPreview = useDevPortalPreviewMode();
64
65
  const isDemoSession = useIsDemoSession();
65
66
  const livePlans = usePlans();
66
- const catalog = isPreview ? parseManifestPlans(previewPlans) : livePlans.data;
67
+ const catalog = isPreview ? withPlanFeatures(parseManifestPlans(previewPlans), planFeatures) : livePlans.data;
67
68
  const isLoading = isPreview ? false : livePlans.isLoading;
68
69
  const isError = isPreview ? false : livePlans.isError;
69
70
  const { data: me } = useMySubscriptions();
70
71
  const cancelMutation = useCancelSubscription();
71
72
  const [cancelingSku, setCancelingSku] = useState(null);
72
73
  const subscriptionBySku = new Map((me?.subscriptions ?? []).map((sub) => [sub.planSku, sub]));
74
+ useEffect(() => {
75
+ if (checkoutCancelled) clearPendingCheckout();
76
+ }, [checkoutCancelled]);
77
+ const pendingSku = checkoutCancelled ? void 0 : readPendingCheckout();
73
78
  const handleUnsubscribe = async (planSku) => {
74
79
  setCancelingSku(planSku);
75
80
  try {
@@ -133,7 +138,8 @@ const AccountPlansPage = () => {
133
138
  isSubscribed,
134
139
  onUnsubscribe: isPreview || isDemoSession ? void 0 : handleUnsubscribe,
135
140
  isCanceling: cancelingSku === plan.sku,
136
- cancelDisabled: isPreview || isDemoSession
141
+ cancelDisabled: isPreview || isDemoSession,
142
+ isConfirming: !isSubscribed && pendingSku === plan.sku
137
143
  }, plan.sku);
138
144
  })
139
145
  }) : !isLoading && !isError ? /* @__PURE__ */ jsx("div", {
@@ -1,7 +1,8 @@
1
1
  import { n as ALREADY_SUBSCRIBED_PARAM, s as PRICING_PATH, t as ACCOUNT_PLANS_PATH } from "./paths-DaVyTIDJ.mjs";
2
- import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
3
3
  import { n as readProblemDetail, t as isAlreadySubscribedMessage } from "./problem-details-n20-dy3w.mjs";
4
- import { t as FREE_CHECKOUT_MARKER } from "./billing-state-Dgcc3_pT.mjs";
4
+ import { r as rememberPendingCheckout } from "./pending-checkout-DpVaNnTu.mjs";
5
+ import { t as FREE_CHECKOUT_MARKER } from "./billing-state-CJmy96zI.mjs";
5
6
  import { ShieldIcon } from "@lukoweb/apitogo/icons";
6
7
  import { Link, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
7
8
  import { useEffect } from "react";
@@ -87,6 +88,10 @@ const CheckoutPage = () => {
87
88
  }
88
89
  });
89
90
  const freeProvisioned = checkoutQuery.data?.checkoutUrl == null && checkoutQuery.data?.entitlementProvisioned === true;
91
+ const checkoutUrl = checkoutQuery.data?.checkoutUrl;
92
+ useEffect(() => {
93
+ if (checkoutUrl) rememberPendingCheckout(planSku);
94
+ }, [checkoutUrl, planSku]);
90
95
  const alreadySubscribed = checkoutQuery.isError && isAlreadySubscribedMessage(checkoutQuery.error.message);
91
96
  useEffect(() => {
92
97
  if (alreadySubscribed) navigate(`/account/subscriptions?${ALREADY_SUBSCRIBED_PARAM}=${encodeURIComponent(planSku)}`, { replace: true });
@@ -1,8 +1,8 @@
1
1
  import { s as PRICING_PATH } from "./paths-DaVyTIDJ.mjs";
2
- import { l as isPlanCustomPriced, n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
+ import { l as isPlanCustomPriced, n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
3
3
  import "./DevPortalBillingContext-CR83s4LO.mjs";
4
4
  import { n as readProblemDetail } from "./problem-details-n20-dy3w.mjs";
5
- import { t as usePlans } from "./usePlans-BF7Hy2qJ.mjs";
5
+ import { t as usePlans } from "./usePlans-D_Khso7B.mjs";
6
6
  import { Link, useSearchParams } from "@lukoweb/apitogo/router";
7
7
  import { useState } from "react";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -1,10 +1,10 @@
1
1
  import { t as ACCOUNT_PLANS_PATH } from "./paths-DaVyTIDJ.mjs";
2
- import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
3
3
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
4
  import { r as createDemoCredentials, t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-yK9qEqV-.mjs";
5
- import { n as useApiKeys } from "./useApiKeys-CEwMzqju.mjs";
6
- import { n as apiReferencePath, r as ApiKeysPanel } from "./account-links-9h91uCGj.mjs";
7
- import { i as ChoosePlanNotice, r as keyPlanNote, t as apiKeysPageState } from "./page-state-BB-72pbw.mjs";
5
+ import { n as useApiKeys } from "./useApiKeys-CnyxipLc.mjs";
6
+ import { n as apiReferencePath, r as ApiKeysPanel } from "./account-links-CpTOzb2V.mjs";
7
+ import { i as ChoosePlanNotice, r as keyPlanNote, t as apiKeysPageState } from "./page-state-BLL2JJBc.mjs";
8
8
  import { Head } from "@lukoweb/apitogo/components";
9
9
  import { KeyRoundIcon } from "@lukoweb/apitogo/icons";
10
10
  import { Link as Link$1 } from "@lukoweb/apitogo/router";
@@ -1,5 +1,5 @@
1
1
  import { i as CONTACT_SALES_PATH, r as CHECKOUT_PATH } from "./paths-DaVyTIDJ.mjs";
2
- import { d as listPlanFeatureLines, i as formatPlanDisplayPrice, l as isPlanCustomPriced, o as formatPlanPriceUnit, s as isActiveSubscription, u as isPlanFree } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
+ import { d as listPlanFeatureLines, i as formatPlanDisplayPrice, l as isPlanCustomPriced, o as formatPlanPriceUnit, s as isActiveSubscription, u as isPlanFree } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
3
3
  import { Button } from "@lukoweb/apitogo/components";
4
4
  import { Link as Link$1 } from "@lukoweb/apitogo/router";
5
5
  import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@lukoweb/apitogo/ui/AlertDialog.js";
@@ -18,7 +18,7 @@ const accountCtaLabel = (plan) => {
18
18
  };
19
19
  /** Custom-priced plans skip checkout and open the contact-sales form. */
20
20
  const planCtaPath = (plan) => `${isPlanCustomPriced(plan) ? CONTACT_SALES_PATH : CHECKOUT_PATH}?planSku=${encodeURIComponent(plan.sku)}`;
21
- const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, onSwitch, isSwitching = false, isCanceling = false, cancelDisabled = false, featured = false }) => {
21
+ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, onSwitch, isSwitching = false, isCanceling = false, cancelDisabled = false, featured = false, isConfirming = false }) => {
22
22
  const isActive = isSubscribed || isActiveSubscription(subscriptionStatus ?? "");
23
23
  const features = listPlanFeatureLines(plan);
24
24
  const displayPrice = formatPlanDisplayPrice(plan);
@@ -83,7 +83,13 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
83
83
  className: "text-center text-xs text-muted-foreground",
84
84
  children: "Unsubscribe is available after the portal is published."
85
85
  }) : null
86
- ] }) : onSwitch && !isPlanCustomPriced(plan) ? /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
86
+ ] }) : isConfirming ? /* @__PURE__ */ jsx(Button, {
87
+ variant: "secondary",
88
+ className: "w-full",
89
+ disabled: true,
90
+ "data-testid": `confirming-${plan.sku}`,
91
+ children: "Confirming payment…"
92
+ }) : onSwitch && !isPlanCustomPriced(plan) ? /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
87
93
  asChild: true,
88
94
  children: /* @__PURE__ */ jsx(Button, {
89
95
  variant: featured ? "default" : "outline",
@@ -1,9 +1,9 @@
1
- import { f as parseManifestPlans, h as pickFeaturedPlanSku, n as useDevPortalPreviewMode } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
- import { t as PricingCard } from "./PricingCard-YJIEwydG.mjs";
1
+ import { f as parseManifestPlans, h as pickFeaturedPlanSku, n as useDevPortalPreviewMode, y as withPlanFeatures } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
2
+ import { t as PricingCard } from "./PricingCard-CRlUGh0k.mjs";
3
3
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
- import { t as usePlans } from "./usePlans-BF7Hy2qJ.mjs";
5
- import { t as isPreviewPricingEnabled } from "./pricing-config-C44AIqql.mjs";
6
- import { a as isPricingLoading, n as buildCompareRows } from "./billing-state-Dgcc3_pT.mjs";
4
+ import { t as usePlans } from "./usePlans-D_Khso7B.mjs";
5
+ import { t as isPreviewPricingEnabled } from "./pricing-config-Bc6eWFUO.mjs";
6
+ import { a as isPricingLoading, n as buildCompareRows } from "./billing-state-CJmy96zI.mjs";
7
7
  import { Head, Heading } from "@lukoweb/apitogo/components";
8
8
  import { Navigate } from "@lukoweb/apitogo/router";
9
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -66,19 +66,20 @@ const DEFAULT_COMPARE_ROWS = [
66
66
  ]
67
67
  }
68
68
  ];
69
+ /** Only what the platform itself does, so the defaults never promise anything for the tenant (API-124). */
69
70
  const DEFAULT_FAQS = [{
70
- question: "What happens if I exceed my quota?",
71
- answer: "Requests over your monthly quota return a 429 response so you can test rate-limit handling instead of being cut off unexpectedly."
71
+ question: "What happens if I go over my plan's limits?",
72
+ answer: "Calls over the rate limit or the quota are refused with a 429 response, and its Retry-After header says when to try again."
72
73
  }, {
73
- question: "Can I change plans later?",
74
- answer: "Yes, upgrade or downgrade any time from your billing page. Changes apply on your next billing cycle."
74
+ question: "Can I have more than one plan?",
75
+ answer: "Yes. Each plan has its own API keys and limits: create a key for a plan on the API keys page and use it in your app."
75
76
  }];
76
77
  const PricingPage = () => {
77
- const { pricing, previewPlans } = useDevPortalBillingConfig();
78
+ const { pricing, previewPlans, featuredPlanSku, planFeatures } = useDevPortalBillingConfig();
78
79
  const isPreview = useDevPortalPreviewMode();
79
80
  const pricingEnabled = isPreviewPricingEnabled() || isPreview;
80
81
  const livePlans = usePlans();
81
- const catalog = isPreview ? parseManifestPlans(previewPlans) : livePlans.data;
82
+ const catalog = isPreview ? withPlanFeatures(parseManifestPlans(previewPlans), planFeatures) : livePlans.data;
82
83
  const { isPending: authPending } = useAuth();
83
84
  const isLoading = isPricingLoading({
84
85
  isPreview,
@@ -92,10 +93,10 @@ const PricingPage = () => {
92
93
  to: "/",
93
94
  replace: true
94
95
  });
95
- const title = pricing?.title ?? "Pay for what you call";
96
- const subtitle = pricing?.subtitle ?? "Every plan includes the full endpoint catalog. Higher tiers raise your request quota, rate limit, and support level.";
96
+ const title = pricing?.title ?? "Plans and pricing";
97
+ const subtitle = pricing?.subtitle ?? "Choose the plan that fits how you use the API. Each plan lists its limits and what it includes.";
97
98
  const plans = catalog?.plans ?? [];
98
- const featuredSku = pickFeaturedPlanSku(plans);
99
+ const featuredSku = pickFeaturedPlanSku(plans, featuredPlanSku);
99
100
  const compareRows = buildCompareRows({
100
101
  plans,
101
102
  configuredRows: pricing?.compareRows,
@@ -1,12 +1,13 @@
1
1
  import { a as CREDENTIALS_PATH, n as ALREADY_SUBSCRIBED_PARAM, t as ACCOUNT_PLANS_PATH } from "./paths-DaVyTIDJ.mjs";
2
- import { a as formatPlanPrice, p as parseMonthlyQuotaCalls, s as isActiveSubscription, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
+ import { a as formatPlanPrice, p as parseMonthlyQuotaCalls, s as isActiveSubscription, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
3
3
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
- import { t as useMySubscriptions } from "./useMySubscriptions-DuVq2tXL.mjs";
5
- import { t as usePlans } from "./usePlans-BF7Hy2qJ.mjs";
6
- import { n as useApiKeys } from "./useApiKeys-CEwMzqju.mjs";
7
- import { t as useUsage } from "./useUsage-CJ9VIBc7.mjs";
8
- import { i as hasPaymentProvider, r as checkoutConfirmingText } from "./billing-state-Dgcc3_pT.mjs";
9
- import { i as ChoosePlanNotice, n as billingPageState, r as keyPlanNote } from "./page-state-BB-72pbw.mjs";
4
+ import { t as useMySubscriptions } from "./useMySubscriptions-C1UMb8rJ.mjs";
5
+ import { t as usePlans } from "./usePlans-D_Khso7B.mjs";
6
+ import { n as readPendingCheckout, t as clearPendingCheckout } from "./pending-checkout-DpVaNnTu.mjs";
7
+ import { n as useApiKeys } from "./useApiKeys-CnyxipLc.mjs";
8
+ import { t as useUsage } from "./useUsage-DeEDgDdj.mjs";
9
+ import { i as hasPaymentProvider, r as checkoutConfirmingText } from "./billing-state-CJmy96zI.mjs";
10
+ import { i as ChoosePlanNotice, n as billingPageState, r as keyPlanNote } from "./page-state-BLL2JJBc.mjs";
10
11
  import { Head, Spinner } from "@lukoweb/apitogo/components";
11
12
  import { Link as Link$1, useSearchParams } from "@lukoweb/apitogo/router";
12
13
  import { useEffect } from "react";
@@ -71,7 +72,12 @@ const SubscriptionsPage = () => {
71
72
  const earlierSubscriptions = subscriptions.filter((sub) => !isActiveSubscription(sub.status));
72
73
  const hasActivePlan = activeSubscriptions.length > 0;
73
74
  const singlePlan = activeSubscriptions.length === 1 ? planBySku.get(activeSubscriptions[0].planSku) : void 0;
74
- const confirmingPayment = checkoutSuccess && !isLoading && !hasActivePlan;
75
+ const pendingSku = checkoutSuccess ? readPendingCheckout() : void 0;
76
+ const pendingActive = pendingSku !== void 0 && activeSubscriptions.some((sub) => sub.planSku === pendingSku);
77
+ const confirmingPayment = checkoutSuccess && !isLoading && (pendingSku !== void 0 ? !pendingActive : !hasActivePlan);
78
+ useEffect(() => {
79
+ if (pendingActive) clearPendingCheckout();
80
+ }, [pendingActive]);
75
81
  useEffect(() => {
76
82
  if (!confirmingPayment) return;
77
83
  let attempts = 0;
@@ -120,7 +126,7 @@ const SubscriptionsPage = () => {
120
126
  className: "mb-6 rounded-xl border border-border bg-card p-4 text-sm",
121
127
  children: checkoutConfirmingText(checkoutPaid)
122
128
  }) : null,
123
- checkoutSuccess && hasActivePlan ? /* @__PURE__ */ jsx("div", {
129
+ checkoutSuccess && hasActivePlan && !confirmingPayment ? /* @__PURE__ */ jsx("div", {
124
130
  className: "mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-sm",
125
131
  children: planNote ? /* @__PURE__ */ jsxs(Fragment, { children: [
126
132
  planNote.text,
@@ -207,7 +213,7 @@ const SubscriptionsPage = () => {
207
213
  })
208
214
  })]
209
215
  }) : null,
210
- billingState === "empty" ? /* @__PURE__ */ jsx(ChoosePlanNotice, {
216
+ billingState === "empty" && !confirmingPayment ? /* @__PURE__ */ jsx(ChoosePlanNotice, {
211
217
  className: "mb-8 rounded-xl border border-dashed p-8 text-center",
212
218
  body: "You do not have a plan yet. Choose one to start using the API; your billing details appear here afterwards."
213
219
  }) : null,
@@ -1,8 +1,8 @@
1
- import { m as parseQuotaPeriodDays, p as parseMonthlyQuotaCalls, s as isActiveSubscription } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
1
+ import { m as parseQuotaPeriodDays, p as parseMonthlyQuotaCalls, s as isActiveSubscription } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
2
2
  import "./DevPortalBillingContext-CR83s4LO.mjs";
3
- import { t as useMySubscriptions } from "./useMySubscriptions-DuVq2tXL.mjs";
4
- import { t as usePlans } from "./usePlans-BF7Hy2qJ.mjs";
5
- import { n as useUsageRecent, t as useUsage } from "./useUsage-CJ9VIBc7.mjs";
3
+ import { t as useMySubscriptions } from "./useMySubscriptions-C1UMb8rJ.mjs";
4
+ import { t as usePlans } from "./usePlans-D_Khso7B.mjs";
5
+ import { n as useUsageRecent, t as useUsage } from "./useUsage-DeEDgDdj.mjs";
6
6
  import { Head } from "@lukoweb/apitogo/components";
7
7
  import { Link as Link$1 } from "@lukoweb/apitogo/router";
8
8
  import { useState } from "react";
@@ -1,4 +1,4 @@
1
- import { i as useRevokeApiKey, r as useCreateApiKey, t as API_KEY_CAP_REACHED } from "./useApiKeys-CEwMzqju.mjs";
1
+ import { i as useRevokeApiKey, r as useCreateApiKey, t as API_KEY_CAP_REACHED } from "./useApiKeys-CnyxipLc.mjs";
2
2
  import { Spinner } from "@lukoweb/apitogo/components";
3
3
  import { KeyRoundIcon, TrashIcon } from "@lukoweb/apitogo/icons";
4
4
  import { useState } from "react";
@@ -1,4 +1,4 @@
1
- import { _ as rateLimitCell, g as quotaCell } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
1
+ import { _ as rateLimitCell, g as quotaCell } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
2
2
 
3
3
  //#region src/utils/billing-state.ts
4
4
  /** Whether the pricing page has no plans to show yet because the answer is still on its way. */
package/dist/index.d.mts CHANGED
@@ -58,7 +58,14 @@ type DevPortalBillingConfig = {
58
58
  * The header API users send their key in, as the portal build knows it from the gateway config. Wins over the plans
59
59
  * backend's answer, which only knows the platform default.
60
60
  */
61
- keyHeader?: string; /** Local preview plan catalog (from `apitogo.config.json` `plans`). */
61
+ keyHeader?: string; /** The plan shown as "Popular"; none unless the tenant marked one (API-124). */
62
+ featuredPlanSku?: string; /** Lines each plan's card lists, by lowercased sku, from `features` in plans.json (API-129). */
63
+ planFeatures?: Record<string, string[]>;
64
+ /**
65
+ * The gateway checks tokens (`gateway.auth`), so Try it offers "Signed-in user". False for a gateway that checks keys,
66
+ * where the token only gets a 401 (API-129). Absent (an older wrapper): offered, as before.
67
+ */
68
+ tokenIdentity?: boolean; /** Local preview plan catalog (from `apitogo.config.json` `plans`). */
62
69
  previewPlans?: Array<{
63
70
  sku: string;
64
71
  displayName: string;
@@ -266,7 +273,7 @@ type PlansPreviewItem = {
266
273
  featured?: boolean;
267
274
  };
268
275
  /** Maps plans to landing `PlansPreviewSection` cards, highlighting the same plan as /pricing. */
269
- declare const toPlansPreviewItems: (plans: DevPortalPlan[]) => PlansPreviewItem[];
276
+ declare const toPlansPreviewItems: (plans: DevPortalPlan[], featuredPlanSku?: string) => PlansPreviewItem[];
270
277
  /**
271
278
  * Plan cards for the landing `PlansPreviewSection`, sourced like the /pricing page: the dev
272
279
  * portal backend when live, `modules.userPanel.plans.items` from `apitogo.config.json` in
package/dist/index.mjs CHANGED
@@ -1,15 +1,16 @@
1
- import { t as AccountPlansPage } from "./AccountPlansPage-0PYGy1aV.mjs";
1
+ import { t as AccountPlansPage } from "./AccountPlansPage-DBBVHGuw.mjs";
2
2
  import { a as CREDENTIALS_PATH, c as SUBSCRIPTIONS_PATH, i as CONTACT_SALES_PATH, l as USAGE_PATH, o as LOGIN_NAV_PATH, r as CHECKOUT_PATH, s as PRICING_PATH, t as ACCOUNT_PLANS_PATH } from "./paths-DaVyTIDJ.mjs";
3
- import { d as listPlanFeatureLines, f as parseManifestPlans, h as pickFeaturedPlanSku, i as formatPlanDisplayPrice, o as formatPlanPriceUnit, s as isActiveSubscription, v as resolveDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
4
- import "./PricingCard-YJIEwydG.mjs";
3
+ import { d as listPlanFeatureLines, f as parseManifestPlans, h as pickFeaturedPlanSku, i as formatPlanDisplayPrice, o as formatPlanPriceUnit, s as isActiveSubscription, v as resolveDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
4
+ import "./PricingCard-CRlUGh0k.mjs";
5
5
  import { n as useDevPortalBillingConfig, t as DevPortalBillingProvider } from "./DevPortalBillingContext-CR83s4LO.mjs";
6
6
  import { t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-yK9qEqV-.mjs";
7
- import { t as useMySubscriptions } from "./useMySubscriptions-DuVq2tXL.mjs";
8
- import { n as startPlansPrefetch, t as usePlans } from "./usePlans-BF7Hy2qJ.mjs";
9
- import { a as playgroundKeyIdentities, i as useRevokeApiKey, n as useApiKeys, r as useCreateApiKey, t as API_KEY_CAP_REACHED } from "./useApiKeys-CEwMzqju.mjs";
10
- import { n as useUsageRecent, t as useUsage } from "./useUsage-CJ9VIBc7.mjs";
11
- import { r as ApiKeysPanel, t as accountLayoutProps } from "./account-links-9h91uCGj.mjs";
12
- import { t as isPreviewPricingEnabled } from "./pricing-config-C44AIqql.mjs";
7
+ import { t as useMySubscriptions } from "./useMySubscriptions-C1UMb8rJ.mjs";
8
+ import { n as startPlansPrefetch, t as usePlans } from "./usePlans-D_Khso7B.mjs";
9
+ import "./pending-checkout-DpVaNnTu.mjs";
10
+ import { a as playgroundKeyIdentities, i as useRevokeApiKey, n as useApiKeys, r as useCreateApiKey, t as API_KEY_CAP_REACHED } from "./useApiKeys-CnyxipLc.mjs";
11
+ import { n as useUsageRecent, t as useUsage } from "./useUsage-DeEDgDdj.mjs";
12
+ import { r as ApiKeysPanel, t as accountLayoutProps } from "./account-links-CpTOzb2V.mjs";
13
+ import { t as isPreviewPricingEnabled } from "./pricing-config-Bc6eWFUO.mjs";
13
14
  import { Head, Link } from "@lukoweb/apitogo/components";
14
15
  import { ArrowRightIcon, KeyRoundIcon, StarsIcon } from "@lukoweb/apitogo/icons";
15
16
  import { Outlet } from "@lukoweb/apitogo/router";
@@ -366,13 +367,13 @@ async function getSignedInIdentities(apiBaseUrl, fetchImpl = fetch) {
366
367
 
367
368
  //#endregion
368
369
  //#region src/DevPortalBillingPlugin.tsx
369
- const AccountPlansPage$1 = lazy(() => import("./AccountPlansPage-0PYGy1aV.mjs").then((n) => n.n));
370
- const CheckoutPage = lazy(() => import("./CheckoutPage-Bpa128i2.mjs"));
371
- const ContactSalesPage = lazy(() => import("./ContactSalesPage-BslD3Yk4.mjs"));
372
- const CredentialsPage = lazy(() => import("./CredentialsPage-C4qpBVrh.mjs"));
373
- const PricingPage = lazy(() => import("./PricingPage-DIhoFOd9.mjs"));
374
- const SubscriptionsPage = lazy(() => import("./SubscriptionsPage-D7hsYtTB.mjs"));
375
- const UsagePage = lazy(() => import("./UsagePage-Di9kQ_3Z.mjs"));
370
+ const AccountPlansPage$1 = lazy(() => import("./AccountPlansPage-DBBVHGuw.mjs").then((n) => n.n));
371
+ const CheckoutPage = lazy(() => import("./CheckoutPage-CZ9NXp8z.mjs"));
372
+ const ContactSalesPage = lazy(() => import("./ContactSalesPage-D4jX0rtt.mjs"));
373
+ const CredentialsPage = lazy(() => import("./CredentialsPage-CVecoIuQ.mjs"));
374
+ const PricingPage = lazy(() => import("./PricingPage-BM7NH4VQ.mjs"));
375
+ const SubscriptionsPage = lazy(() => import("./SubscriptionsPage-C58HnDmQ.mjs"));
376
+ const UsagePage = lazy(() => import("./UsagePage-C5opT1KD.mjs"));
376
377
  function resolvePricingEnabled(config) {
377
378
  if (config.__meta?.pricingEnabled !== void 0) return config.__meta.pricingEnabled;
378
379
  return isPreviewPricingEnabled();
@@ -397,7 +398,7 @@ const devPortalBillingPlugin = createPlugin((options = {}) => {
397
398
  }
398
399
  });
399
400
  },
400
- getIdentities: async () => [...playgroundKeyIdentities(), ...await getSignedInIdentities(resolveDevPortalApiBaseUrl(options.apiBaseUrl))],
401
+ getIdentities: async () => [...playgroundKeyIdentities(), ...options.tokenIdentity === false ? [] : await getSignedInIdentities(resolveDevPortalApiBaseUrl(options.apiBaseUrl))],
401
402
  getProfileMenuItems: () => [{
402
403
  label: "Billing",
403
404
  path: SUBSCRIPTIONS_PATH,
@@ -495,8 +496,8 @@ function useGatewayCall() {
495
496
  //#endregion
496
497
  //#region src/hooks/usePlansPreview.ts
497
498
  /** Maps plans to landing `PlansPreviewSection` cards, highlighting the same plan as /pricing. */
498
- const toPlansPreviewItems = (plans) => {
499
- const featuredSku = pickFeaturedPlanSku(plans);
499
+ const toPlansPreviewItems = (plans, featuredPlanSku) => {
500
+ const featuredSku = pickFeaturedPlanSku(plans, featuredPlanSku);
500
501
  return plans.map((plan) => ({
501
502
  name: plan.displayName,
502
503
  price: formatPlanDisplayPrice(plan),
@@ -513,7 +514,8 @@ const toPlansPreviewItems = (plans) => {
513
514
  */
514
515
  const usePlansPreview = () => {
515
516
  const { data } = usePlans();
516
- return toPlansPreviewItems(data?.plans ?? parseManifestPlans().plans);
517
+ const { featuredPlanSku } = useDevPortalBillingConfig();
518
+ return toPlansPreviewItems(data?.plans ?? parseManifestPlans().plans, featuredPlanSku);
517
519
  };
518
520
 
519
521
  //#endregion
@@ -0,0 +1,45 @@
1
+ //#region src/utils/pending-checkout.ts
2
+ const STORAGE_KEY = "apitogo-pending-checkout";
3
+ /** How long a checkout counts as waiting for its payment to be confirmed. Stripe usually confirms within seconds. */
4
+ const PENDING_FOR_MS = 600 * 1e3;
5
+ const storage = () => {
6
+ try {
7
+ return typeof sessionStorage === "undefined" ? void 0 : sessionStorage;
8
+ } catch {
9
+ return;
10
+ }
11
+ };
12
+ /**
13
+ * Remembers the plan the customer is about to pay for, as checkout sends them to Stripe. Back in the portal the payment
14
+ * is confirmed a few seconds later; until then the pages must not offer that plan again (API-128), even when the
15
+ * customer already holds another plan. Only this browser tab knows it.
16
+ */
17
+ function rememberPendingCheckout(sku, now = Date.now()) {
18
+ try {
19
+ storage()?.setItem(STORAGE_KEY, JSON.stringify({
20
+ sku,
21
+ at: now
22
+ }));
23
+ } catch {}
24
+ }
25
+ /** The plan still waiting for its payment to be confirmed, or undefined when none is (or it is too old). */
26
+ function readPendingCheckout(now = Date.now()) {
27
+ try {
28
+ const raw = storage()?.getItem(STORAGE_KEY);
29
+ if (!raw) return void 0;
30
+ const pending = JSON.parse(raw);
31
+ if (typeof pending.sku !== "string" || typeof pending.at !== "number") return void 0;
32
+ return now - pending.at <= PENDING_FOR_MS ? pending.sku : void 0;
33
+ } catch {
34
+ return;
35
+ }
36
+ }
37
+ /** The payment was confirmed (the plan is active) or given up on. */
38
+ function clearPendingCheckout() {
39
+ try {
40
+ storage()?.removeItem(STORAGE_KEY);
41
+ } catch {}
42
+ }
43
+
44
+ //#endregion
45
+ export { readPendingCheckout as n, rememberPendingCheckout as r, clearPendingCheckout as t };
@@ -1,4 +1,4 @@
1
- import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
1
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
2
2
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
3
3
  import { n as readProblemDetail } from "./problem-details-n20-dy3w.mjs";
4
4
  import { n as createDemoApiKeys, t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-yK9qEqV-.mjs";
@@ -115,13 +115,20 @@ function formatPlanDisplayPrice(plan) {
115
115
  function formatPlanPriceUnit(plan) {
116
116
  return isPlanCustomPriced(plan) ? "talk to us" : formatBillingPeriod(plan.billingPeriod);
117
117
  }
118
- /** SKU of the plan to highlight: `developer` if present, else the first low-priced paid plan. */
119
- function pickFeaturedPlanSku(plans) {
120
- return plans.find((p) => p.sku === "developer")?.sku ?? plans.find((p) => p.priceAmount > 0 && p.priceAmount < 50)?.sku ?? plans.find((p) => p.priceAmount > 0)?.sku;
118
+ /**
119
+ * SKU of the plan to highlight: the one the tenant marked `featured`, when the catalog has it. It used to be guessed
120
+ * (a plan called "developer", or the first cheap paid one), which put "Popular" on a plan the tenant never chose
121
+ * (API-124).
122
+ */
123
+ function pickFeaturedPlanSku(plans, featuredPlanSku) {
124
+ const wanted = featuredPlanSku?.trim().toLowerCase();
125
+ return wanted ? plans.find((p) => p.sku.toLowerCase() === wanted)?.sku : void 0;
121
126
  }
122
127
  function formatRateLimit(rate) {
123
128
  if (!rate || typeof rate !== "object") return null;
124
- const { calls, renewalPeriodSeconds } = rate;
129
+ const limit = rate;
130
+ const calls = limit.calls;
131
+ const renewalPeriodSeconds = limit.renewalPeriodSeconds ?? limit.periodSeconds;
125
132
  if (calls == null || renewalPeriodSeconds == null) return null;
126
133
  if (renewalPeriodSeconds === 1) return `${calls} req/sec rate limit`;
127
134
  const window = renewalPeriodSeconds === 60 ? "minute" : `${renewalPeriodSeconds} seconds`;
@@ -129,7 +136,9 @@ function formatRateLimit(rate) {
129
136
  }
130
137
  function formatQuota(quota) {
131
138
  if (!quota || typeof quota !== "object") return null;
132
- const { calls, renewalPeriodDays } = quota;
139
+ const limit = quota;
140
+ const calls = limit.calls;
141
+ const renewalPeriodDays = limit.renewalPeriodDays ?? limit.periodDays;
133
142
  if (calls == null) return null;
134
143
  if (calls < 0) return "Unlimited requests";
135
144
  const formatted = calls.toLocaleString();
@@ -172,19 +181,27 @@ function parseLimits(limitsJson) {
172
181
  }
173
182
  }
174
183
  /** Marketing bullets keyed by common sample SKUs when limitsJson is sparse. */
175
- const SAMPLE_PLAN_FEATURE_EXTRAS = {
176
- free: ["All endpoints"],
177
- developer: ["Webhooks included", "Email support"],
178
- team: ["5 team seats", "Priority support"],
179
- enterprise: [
180
- "Unlimited requests",
181
- "Custom rate limits",
182
- "SSO & audit logs",
183
- "Dedicated SLA"
184
- ]
185
- };
184
+ /**
185
+ * Lines a plan gets from its name alone. Empty on purpose: "SSO & audit logs" or "Dedicated SLA" under a plan called
186
+ * Enterprise were promises the tenant never made (API-124). A plan shows its limits, its endpoints and what the
187
+ * tenant wrote.
188
+ */
189
+ const SAMPLE_PLAN_FEATURE_EXTRAS = {};
190
+ /** Gives each plan the feature lines plans.json names for its sku (API-129). Plans without any keep their own. */
191
+ function withPlanFeatures(catalog, planFeatures) {
192
+ if (!planFeatures || Object.keys(planFeatures).length === 0) return catalog;
193
+ return {
194
+ ...catalog,
195
+ plans: catalog.plans.map((plan) => {
196
+ const features = planFeatures[plan.sku.trim().toLowerCase()];
197
+ return features?.length ? {
198
+ ...plan,
199
+ features
200
+ } : plan;
201
+ })
202
+ };
203
+ }
186
204
  function listPlanFeatureLines(plan) {
187
- if (plan.features && plan.features.length > 0) return plan.features;
188
205
  const lines = [];
189
206
  if (plan.limitsJson?.trim()) try {
190
207
  const limits = JSON.parse(plan.limitsJson);
@@ -193,7 +210,8 @@ function listPlanFeatureLines(plan) {
193
210
  const rateLine = formatRateLimit(limits.rateLimit);
194
211
  if (rateLine) lines.push(rateLine);
195
212
  } catch {}
196
- const extras = SAMPLE_PLAN_FEATURE_EXTRAS[plan.sku.toLowerCase()];
213
+ for (const feature of plan.features ?? []) if (!lines.includes(feature)) lines.push(feature);
214
+ const extras = plan.features?.length ? void 0 : SAMPLE_PLAN_FEATURE_EXTRAS[plan.sku.toLowerCase()];
197
215
  if (extras) {
198
216
  for (const extra of extras) if (!lines.includes(extra)) lines.push(extra);
199
217
  }
@@ -250,4 +268,4 @@ function useDevPortalPreviewMode() {
250
268
  }
251
269
 
252
270
  //#endregion
253
- export { rateLimitCell as _, formatPlanPrice as a, isPlaceholderDevPortalApiUrl as c, listPlanFeatureLines as d, parseManifestPlans as f, quotaCell as g, pickFeaturedPlanSku as h, formatPlanDisplayPrice as i, isPlanCustomPriced as l, parseQuotaPeriodDays as m, useDevPortalPreviewMode as n, formatPlanPriceUnit as o, parseMonthlyQuotaCalls as p, useIsDemoSession as r, isActiveSubscription as s, useDevPortalApiBaseUrl as t, isPlanFree as u, resolveDevPortalApiBaseUrl as v };
271
+ export { rateLimitCell as _, formatPlanPrice as a, isPlaceholderDevPortalApiUrl as c, listPlanFeatureLines as d, parseManifestPlans as f, quotaCell as g, pickFeaturedPlanSku as h, formatPlanDisplayPrice as i, isPlanCustomPriced as l, parseQuotaPeriodDays as m, useDevPortalPreviewMode as n, formatPlanPriceUnit as o, parseMonthlyQuotaCalls as p, useIsDemoSession as r, isActiveSubscription as s, useDevPortalApiBaseUrl as t, isPlanFree as u, resolveDevPortalApiBaseUrl as v, withPlanFeatures as y };
@@ -1,4 +1,4 @@
1
- import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
1
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
2
2
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
3
3
  import { i as createDemoMeResponse } from "./demo-session-yK9qEqV-.mjs";
4
4
  import { useQuery } from "@lukoweb/apitogo/react-query";
@@ -1,4 +1,4 @@
1
- import { c as isPlaceholderDevPortalApiUrl, f as parseManifestPlans, n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
1
+ import { c as isPlaceholderDevPortalApiUrl, f as parseManifestPlans, n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl, y as withPlanFeatures } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
2
2
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
3
3
  import { useQuery } from "@lukoweb/apitogo/react-query";
4
4
 
@@ -37,17 +37,18 @@ function usePlans() {
37
37
  const apiBaseUrl = useDevPortalApiBaseUrl();
38
38
  const isPreview = useDevPortalPreviewMode();
39
39
  const isDemoSession = useIsDemoSession();
40
- const { previewPlans } = useDevPortalBillingConfig();
40
+ const { previewPlans, planFeatures } = useDevPortalBillingConfig();
41
41
  const liveQuery = useQuery({
42
42
  queryKey: ["dev-portal-plans", apiBaseUrl],
43
43
  enabled: !isPreview && !isDemoSession && Boolean(apiBaseUrl),
44
44
  queryFn: async () => {
45
45
  if (!apiBaseUrl) return { plans: [] };
46
46
  return loadPlans(apiBaseUrl);
47
- }
47
+ },
48
+ select: (catalog) => withPlanFeatures(catalog, planFeatures)
48
49
  });
49
50
  if (isPreview || isDemoSession) {
50
- const data = parseManifestPlans(previewPlans);
51
+ const data = withPlanFeatures(parseManifestPlans(previewPlans), planFeatures);
51
52
  return {
52
53
  ...liveQuery,
53
54
  data,
@@ -1,4 +1,4 @@
1
- import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
1
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BfpUBayO.mjs";
2
2
  import { useQuery } from "@lukoweb/apitogo/react-query";
3
3
  import { useAuth } from "@lukoweb/apitogo/hooks";
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo-plugin-dev-portal-billing",
3
- "version": "0.2.54",
3
+ "version": "0.2.57",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
@@ -46,8 +46,8 @@
46
46
  "react": "19.2.4",
47
47
  "react-dom": "19.2.4",
48
48
  "tsdown": "0.20.3",
49
- "@lukoweb/apitogo": "0.2.54",
50
- "@lukoweb/apitogo-module-user-panel": "0.2.54"
49
+ "@lukoweb/apitogo": "0.2.57",
50
+ "@lukoweb/apitogo-module-user-panel": "0.2.57"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": ">=19.2.0",