@lukoweb/apitogo-plugin-dev-portal-billing 0.2.53 → 0.2.56

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 (26) hide show
  1. package/dist/{AccountPlansPage-DvRFe3di.mjs → AccountPlansPage-BuM7fe82.mjs} +14 -10
  2. package/dist/{CheckoutPage-CwSElFUk.mjs → CheckoutPage-BSiADcI6.mjs} +32 -15
  3. package/dist/{ContactSalesPage-q7Dxxtx6.mjs → ContactSalesPage-BqqDmorF.mjs} +4 -11
  4. package/dist/{CredentialsPage-B2jPo6Go.mjs → CredentialsPage-DjNvBBLb.mjs} +28 -19
  5. package/dist/{PricingCard-CKdq1oWn.mjs → PricingCard-DFuQEw7Y.mjs} +10 -4
  6. package/dist/{PricingPage-DnQQfy9L.mjs → PricingPage-SpL81vpd.mjs} +14 -13
  7. package/dist/SubscriptionsPage-CwsBtNld.mjs +274 -0
  8. package/dist/{UsagePage-BF4QgrTc.mjs → UsagePage-Dw5Uepg3.mjs} +61 -19
  9. package/dist/{account-links-D_vS0GJn.mjs → account-links-Drofs-f-.mjs} +1 -1
  10. package/dist/{billing-state-DbU5Lpoz.mjs → billing-state-DBQR2tad.mjs} +1 -1
  11. package/dist/index.d.mts +3 -2
  12. package/dist/index.mjs +25 -23
  13. package/dist/{key-plan-note-DTWeFhn2.mjs → page-state-BLL2JJBc.mjs} +22 -2
  14. package/dist/paths-DaVyTIDJ.mjs +14 -0
  15. package/dist/pending-checkout-DpVaNnTu.mjs +45 -0
  16. package/dist/problem-details-n20-dy3w.mjs +18 -0
  17. package/dist/{useApiKeys-CVgXfUop.mjs → useApiKeys-BOY3vIOr.mjs} +3 -9
  18. package/dist/{useDevPortalApiBaseUrl-BstWzITX.mjs → useDevPortalApiBaseUrl-BiI2jFjU.mjs} +14 -14
  19. package/dist/{useMySubscriptions-BtxAXA1m.mjs → useMySubscriptions-BYVIIWbe.mjs} +2 -2
  20. package/dist/{usePlans-DPGFWcVo.mjs → usePlans-HyhIIuJZ.mjs} +1 -1
  21. package/dist/{useUsage--5sZub3M.mjs → useUsage-B5x9YuKa.mjs} +1 -1
  22. package/package.json +3 -3
  23. package/dist/SubscriptionsPage-msO31jv-.mjs +0 -223
  24. package/dist/paths-ujy9wjVK.mjs +0 -12
  25. /package/dist/{demo-session-rN2Gj4lM.mjs → demo-session-yK9qEqV-.mjs} +0 -0
  26. /package/dist/{pricing-config-02m4dyFf.mjs → pricing-config-Bc6eWFUO.mjs} +0 -0
@@ -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-BiI2jFjU.mjs";
2
2
  import "./DevPortalBillingContext-CR83s4LO.mjs";
3
- import { t as useMySubscriptions } from "./useMySubscriptions-BtxAXA1m.mjs";
4
- import { t as usePlans } from "./usePlans-DPGFWcVo.mjs";
5
- import { t as useUsage } from "./useUsage--5sZub3M.mjs";
3
+ import { t as useMySubscriptions } from "./useMySubscriptions-BYVIIWbe.mjs";
4
+ import { t as usePlans } from "./usePlans-HyhIIuJZ.mjs";
5
+ import { n as useUsageRecent, t as useUsage } from "./useUsage-B5x9YuKa.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";
@@ -35,6 +35,27 @@ function quotaSummary(used, limit, periodDays, periodEnd) {
35
35
  };
36
36
  }
37
37
 
38
+ //#endregion
39
+ //#region src/utils/usage-display.ts
40
+ /**
41
+ * How the usage page shows its numbers (API-126).
42
+ */
43
+ /** "GET /p<project id>/hello" becomes "GET /hello": the shared gateway's project prefix means nothing to an API user. */
44
+ function endpointLabel(label) {
45
+ return label.replace(/(^|\s)\/p[0-9a-f]{32}(\/|$)/i, "$1/");
46
+ }
47
+ /** Calls the gateway refused because the plan's rate limit or quota was reached (counted as "429"). */
48
+ function blockedCount(byStatus) {
49
+ return byStatus.filter((item) => item.label === "429").reduce((sum, item) => sum + item.count, 0);
50
+ }
51
+ /** The middle latency of the given calls, or null without any. A slow outlier moves it less than an average. */
52
+ function medianLatency(latencies) {
53
+ const values = latencies.filter((value) => typeof value === "number").sort((a, b) => a - b);
54
+ if (values.length === 0) return null;
55
+ const middle = Math.floor(values.length / 2);
56
+ return values.length % 2 === 1 ? values[middle] : Math.round((values[middle - 1] + values[middle]) / 2);
57
+ }
58
+
38
59
  //#endregion
39
60
  //#region src/pages/UsagePage.tsx
40
61
  const RANGES = [
@@ -65,6 +86,9 @@ const UsagePage = () => {
65
86
  const auth = useAuth();
66
87
  const [range, setRange] = useState("30d");
67
88
  const { data, isLoading, isError } = useUsage(range);
89
+ const { data: recent } = useUsageRecent(50);
90
+ const median = medianLatency((recent?.items ?? []).map((item) => item.latencyMs));
91
+ const blocked = data ? blockedCount(data.byStatus) : 0;
68
92
  const { data: quotaUsage } = useUsage("30d");
69
93
  const { data: mine } = useMySubscriptions();
70
94
  const { data: catalog } = usePlans();
@@ -129,23 +153,41 @@ const UsagePage = () => {
129
153
  children: [
130
154
  /* @__PURE__ */ jsxs("div", {
131
155
  className: "rounded-xl border border-border bg-card p-4",
132
- children: [/* @__PURE__ */ jsx("div", {
133
- className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
134
- children: "Requests"
135
- }), /* @__PURE__ */ jsx("div", {
136
- className: "mt-1.5 text-2xl font-bold",
137
- children: data ? formatNumber(data.requestCount) : "—"
138
- })]
156
+ children: [
157
+ /* @__PURE__ */ jsx("div", {
158
+ className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
159
+ children: "Requests"
160
+ }),
161
+ /* @__PURE__ */ jsx("div", {
162
+ className: "mt-1.5 text-2xl font-bold",
163
+ children: data ? formatNumber(data.requestCount) : "—"
164
+ }),
165
+ blocked > 0 ? /* @__PURE__ */ jsxs("div", {
166
+ className: "mt-1 text-xs text-muted-foreground",
167
+ children: ["Blocked by rate limit: ", formatNumber(blocked)]
168
+ }) : null
169
+ ]
139
170
  }),
140
171
  /* @__PURE__ */ jsxs("div", {
141
172
  className: "rounded-xl border border-border bg-card p-4",
142
- children: [/* @__PURE__ */ jsx("div", {
143
- className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
144
- children: "Avg latency"
145
- }), /* @__PURE__ */ jsx("div", {
146
- className: "mt-1.5 text-2xl font-bold",
147
- children: data?.avgLatencyMs != null ? `${data.avgLatencyMs} ms` : "—"
148
- })]
173
+ children: [
174
+ /* @__PURE__ */ jsx("div", {
175
+ className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
176
+ children: median != null ? "Median latency" : "Avg latency"
177
+ }),
178
+ /* @__PURE__ */ jsx("div", {
179
+ className: "mt-1.5 text-2xl font-bold",
180
+ children: median != null ? `${median} ms` : data?.avgLatencyMs != null ? `${data.avgLatencyMs} ms` : "—"
181
+ }),
182
+ median != null ? /* @__PURE__ */ jsxs("div", {
183
+ className: "mt-1 text-xs text-muted-foreground",
184
+ children: [
185
+ "Of the latest ",
186
+ recent?.items.length ?? 0,
187
+ " calls"
188
+ ]
189
+ }) : null
190
+ ]
149
191
  }),
150
192
  /* @__PURE__ */ jsxs("div", {
151
193
  className: "rounded-xl border border-border bg-card p-4",
@@ -222,7 +264,7 @@ const UsagePage = () => {
222
264
  className: "mb-1 flex justify-between text-[13px]",
223
265
  children: [/* @__PURE__ */ jsx("code", {
224
266
  className: "font-mono text-foreground",
225
- children: item.path
267
+ children: endpointLabel(item.path)
226
268
  }), /* @__PURE__ */ jsxs("span", {
227
269
  className: "text-muted-foreground",
228
270
  children: [pct, "%"]
@@ -1,4 +1,4 @@
1
- import { i as useRevokeApiKey, r as useCreateApiKey, t as API_KEY_CAP_REACHED } from "./useApiKeys-CVgXfUop.mjs";
1
+ import { i as useRevokeApiKey, r as useCreateApiKey, t as API_KEY_CAP_REACHED } from "./useApiKeys-BOY3vIOr.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-BiI2jFjU.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,8 @@ 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; /** Local preview plan catalog (from `apitogo.config.json` `plans`). */
62
63
  previewPlans?: Array<{
63
64
  sku: string;
64
65
  displayName: string;
@@ -266,7 +267,7 @@ type PlansPreviewItem = {
266
267
  featured?: boolean;
267
268
  };
268
269
  /** Maps plans to landing `PlansPreviewSection` cards, highlighting the same plan as /pricing. */
269
- declare const toPlansPreviewItems: (plans: DevPortalPlan[]) => PlansPreviewItem[];
270
+ declare const toPlansPreviewItems: (plans: DevPortalPlan[], featuredPlanSku?: string) => PlansPreviewItem[];
270
271
  /**
271
272
  * Plan cards for the landing `PlansPreviewSection`, sourced like the /pricing page: the dev
272
273
  * 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-DvRFe3di.mjs";
2
- import { a as LOGIN_NAV_PATH, c as USAGE_PATH, i as CREDENTIALS_PATH, n as CHECKOUT_PATH, o as PRICING_PATH, r as CONTACT_SALES_PATH, s as SUBSCRIPTIONS_PATH, t as ACCOUNT_PLANS_PATH } from "./paths-ujy9wjVK.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-CKdq1oWn.mjs";
1
+ import { t as AccountPlansPage } from "./AccountPlansPage-BuM7fe82.mjs";
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-BiI2jFjU.mjs";
4
+ import "./PricingCard-DFuQEw7Y.mjs";
5
5
  import { n as useDevPortalBillingConfig, t as DevPortalBillingProvider } from "./DevPortalBillingContext-CR83s4LO.mjs";
6
- import { t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-rN2Gj4lM.mjs";
7
- import { t as useMySubscriptions } from "./useMySubscriptions-BtxAXA1m.mjs";
8
- import { n as startPlansPrefetch, t as usePlans } from "./usePlans-DPGFWcVo.mjs";
9
- import { a as playgroundKeyIdentities, i as useRevokeApiKey, n as useApiKeys, r as useCreateApiKey, t as API_KEY_CAP_REACHED } from "./useApiKeys-CVgXfUop.mjs";
10
- import { n as useUsageRecent, t as useUsage } from "./useUsage--5sZub3M.mjs";
11
- import { r as ApiKeysPanel, t as accountLayoutProps } from "./account-links-D_vS0GJn.mjs";
12
- import { t as isPreviewPricingEnabled } from "./pricing-config-02m4dyFf.mjs";
6
+ import { t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-yK9qEqV-.mjs";
7
+ import { t as useMySubscriptions } from "./useMySubscriptions-BYVIIWbe.mjs";
8
+ import { n as startPlansPrefetch, t as usePlans } from "./usePlans-HyhIIuJZ.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-BOY3vIOr.mjs";
11
+ import { n as useUsageRecent, t as useUsage } from "./useUsage-B5x9YuKa.mjs";
12
+ import { r as ApiKeysPanel, t as accountLayoutProps } from "./account-links-Drofs-f-.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-DvRFe3di.mjs").then((n) => n.n));
370
- const CheckoutPage = lazy(() => import("./CheckoutPage-CwSElFUk.mjs"));
371
- const ContactSalesPage = lazy(() => import("./ContactSalesPage-q7Dxxtx6.mjs"));
372
- const CredentialsPage = lazy(() => import("./CredentialsPage-B2jPo6Go.mjs"));
373
- const PricingPage = lazy(() => import("./PricingPage-DnQQfy9L.mjs"));
374
- const SubscriptionsPage = lazy(() => import("./SubscriptionsPage-msO31jv-.mjs"));
375
- const UsagePage = lazy(() => import("./UsagePage-BF4QgrTc.mjs"));
370
+ const AccountPlansPage$1 = lazy(() => import("./AccountPlansPage-BuM7fe82.mjs").then((n) => n.n));
371
+ const CheckoutPage = lazy(() => import("./CheckoutPage-BSiADcI6.mjs"));
372
+ const ContactSalesPage = lazy(() => import("./ContactSalesPage-BqqDmorF.mjs"));
373
+ const CredentialsPage = lazy(() => import("./CredentialsPage-DjNvBBLb.mjs"));
374
+ const PricingPage = lazy(() => import("./PricingPage-SpL81vpd.mjs"));
375
+ const SubscriptionsPage = lazy(() => import("./SubscriptionsPage-CwsBtNld.mjs"));
376
+ const UsagePage = lazy(() => import("./UsagePage-Dw5Uepg3.mjs"));
376
377
  function resolvePricingEnabled(config) {
377
378
  if (config.__meta?.pricingEnabled !== void 0) return config.__meta.pricingEnabled;
378
379
  return isPreviewPricingEnabled();
@@ -399,13 +400,13 @@ const devPortalBillingPlugin = createPlugin((options = {}) => {
399
400
  },
400
401
  getIdentities: async () => [...playgroundKeyIdentities(), ...await getSignedInIdentities(resolveDevPortalApiBaseUrl(options.apiBaseUrl))],
401
402
  getProfileMenuItems: () => [{
402
- label: "My Subscriptions",
403
+ label: "Billing",
403
404
  path: SUBSCRIPTIONS_PATH,
404
405
  icon: StarsIcon,
405
406
  category: "middle",
406
407
  weight: 25
407
408
  }, {
408
- label: "API credentials",
409
+ label: "API keys",
409
410
  path: CREDENTIALS_PATH,
410
411
  icon: KeyRoundIcon,
411
412
  category: "middle",
@@ -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
@@ -1,4 +1,4 @@
1
- import { t as ACCOUNT_PLANS_PATH } from "./paths-ujy9wjVK.mjs";
1
+ import { t as ACCOUNT_PLANS_PATH } from "./paths-DaVyTIDJ.mjs";
2
2
  import { Link } from "@lukoweb/apitogo/router";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
4
  import { Button } from "@lukoweb/apitogo/ui/Button.js";
@@ -50,4 +50,24 @@ function keyPlanNote(plans) {
50
50
  }
51
51
 
52
52
  //#endregion
53
- export { ChoosePlanNotice as n, keyPlanNote as t };
53
+ //#region src/utils/page-state.ts
54
+ /** True for the error the keys endpoint gives a user without any plan (HTTP 404). */
55
+ const isNoEntitlement = (error) => error instanceof Error && error.message === "NO_ENTITLEMENT";
56
+ /**
57
+ * The API keys page. The key list is what the page shows, so it decides: the credentials request only adds the gateway
58
+ * address, which the key list also carries, and its quick 404 right after sign-up must not show "no plan" while the
59
+ * keys are still loading.
60
+ */
61
+ function apiKeysPageState(keys) {
62
+ if (keys.data) return keys.data.plans.length > 0 ? "ready" : "empty";
63
+ if (keys.error) return isNoEntitlement(keys.error) ? "empty" : "failed";
64
+ return "loading";
65
+ }
66
+ /** The Billing page: subscriptions decide; until they arrive the page is loading. */
67
+ function billingPageState(me) {
68
+ if (me.data) return (me.data.subscriptions ?? []).length > 0 ? "ready" : "empty";
69
+ return me.error ? "failed" : "loading";
70
+ }
71
+
72
+ //#endregion
73
+ export { ChoosePlanNotice as i, billingPageState as n, keyPlanNote as r, apiKeysPageState as t };
@@ -0,0 +1,14 @@
1
+ //#region src/paths.ts
2
+ const PRICING_PATH = "/pricing";
3
+ const ACCOUNT_PLANS_PATH = "/account/plans";
4
+ const CHECKOUT_PATH = "/checkout";
5
+ const CONTACT_SALES_PATH = "/contact-sales";
6
+ const SUBSCRIPTIONS_PATH = "/account/subscriptions";
7
+ const CREDENTIALS_PATH = "/account/credentials";
8
+ const USAGE_PATH = "/account/usage";
9
+ const LOGIN_NAV_PATH = "/signin";
10
+ /** Billing opens with this query parameter, naming the plan the user tried to buy again (API-123). */
11
+ const ALREADY_SUBSCRIBED_PARAM = "alreadySubscribed";
12
+
13
+ //#endregion
14
+ export { CREDENTIALS_PATH as a, SUBSCRIPTIONS_PATH as c, CONTACT_SALES_PATH as i, USAGE_PATH as l, ALREADY_SUBSCRIBED_PARAM as n, LOGIN_NAV_PATH as o, CHECKOUT_PATH as r, PRICING_PATH as s, ACCOUNT_PLANS_PATH as t };
@@ -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 };
@@ -0,0 +1,18 @@
1
+ //#region src/utils/problem-details.ts
2
+ /**
3
+ * The sentence an RFC 7807 Problem Details response carries for a person: the first field error, else `detail`, else
4
+ * `title`. Pages show this, never the raw JSON (API-123). Null when the body is not Problem Details.
5
+ */
6
+ async function readProblemDetail(response) {
7
+ try {
8
+ const body = await response.json();
9
+ return (body.errors ? Object.values(body.errors).flat()[0] : void 0)?.trim() || body.detail?.trim() || body.title?.trim() || null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+ /** The plans service's answer when a user starts checkout for a plan they already hold. */
15
+ const isAlreadySubscribedMessage = (message) => /already have an active subscription/i.test(message);
16
+
17
+ //#endregion
18
+ export { readProblemDetail as n, isAlreadySubscribedMessage as t };
@@ -1,6 +1,7 @@
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-BiI2jFjU.mjs";
2
2
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
3
- import { n as createDemoApiKeys, t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-rN2Gj4lM.mjs";
3
+ import { n as readProblemDetail } from "./problem-details-n20-dy3w.mjs";
4
+ import { n as createDemoApiKeys, t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-yK9qEqV-.mjs";
4
5
  import { useMutation, useQuery, useQueryClient } from "@lukoweb/apitogo/react-query";
5
6
  import { useAuth } from "@lukoweb/apitogo/hooks";
6
7
 
@@ -154,13 +155,6 @@ function useRevokeApiKey() {
154
155
  }
155
156
  });
156
157
  }
157
- async function readProblemDetail(response) {
158
- try {
159
- return (await response.json()).detail?.trim() || null;
160
- } catch {
161
- return null;
162
- }
163
- }
164
158
 
165
159
  //#endregion
166
160
  export { playgroundKeyIdentities as a, useRevokeApiKey as i, useApiKeys as n, useCreateApiKey as r, API_KEY_CAP_REACHED as t };
@@ -115,9 +115,14 @@ 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;
@@ -172,17 +177,12 @@ function parseLimits(limitsJson) {
172
177
  }
173
178
  }
174
179
  /** 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
- };
180
+ /**
181
+ * Lines a plan gets from its name alone. Empty on purpose: "SSO & audit logs" or "Dedicated SLA" under a plan called
182
+ * Enterprise were promises the tenant never made (API-124). A plan shows its limits, its endpoints and what the
183
+ * tenant wrote.
184
+ */
185
+ const SAMPLE_PLAN_FEATURE_EXTRAS = {};
186
186
  function listPlanFeatureLines(plan) {
187
187
  if (plan.features && plan.features.length > 0) return plan.features;
188
188
  const lines = [];
@@ -1,6 +1,6 @@
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-BiI2jFjU.mjs";
2
2
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
3
- import { i as createDemoMeResponse } from "./demo-session-rN2Gj4lM.mjs";
3
+ import { i as createDemoMeResponse } from "./demo-session-yK9qEqV-.mjs";
4
4
  import { useQuery } from "@lukoweb/apitogo/react-query";
5
5
  import { useAuth } from "@lukoweb/apitogo/hooks";
6
6
 
@@ -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 } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
2
2
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
3
3
  import { useQuery } from "@lukoweb/apitogo/react-query";
4
4
 
@@ -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-BiI2jFjU.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.53",
3
+ "version": "0.2.56",
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.53",
50
- "@lukoweb/apitogo-module-user-panel": "0.2.53"
49
+ "@lukoweb/apitogo": "0.2.56",
50
+ "@lukoweb/apitogo-module-user-panel": "0.2.56"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": ">=19.2.0",