@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.
- package/dist/{AccountPlansPage-DvRFe3di.mjs → AccountPlansPage-BuM7fe82.mjs} +14 -10
- package/dist/{CheckoutPage-CwSElFUk.mjs → CheckoutPage-BSiADcI6.mjs} +32 -15
- package/dist/{ContactSalesPage-q7Dxxtx6.mjs → ContactSalesPage-BqqDmorF.mjs} +4 -11
- package/dist/{CredentialsPage-B2jPo6Go.mjs → CredentialsPage-DjNvBBLb.mjs} +28 -19
- package/dist/{PricingCard-CKdq1oWn.mjs → PricingCard-DFuQEw7Y.mjs} +10 -4
- package/dist/{PricingPage-DnQQfy9L.mjs → PricingPage-SpL81vpd.mjs} +14 -13
- package/dist/SubscriptionsPage-CwsBtNld.mjs +274 -0
- package/dist/{UsagePage-BF4QgrTc.mjs → UsagePage-Dw5Uepg3.mjs} +61 -19
- package/dist/{account-links-D_vS0GJn.mjs → account-links-Drofs-f-.mjs} +1 -1
- package/dist/{billing-state-DbU5Lpoz.mjs → billing-state-DBQR2tad.mjs} +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +25 -23
- package/dist/{key-plan-note-DTWeFhn2.mjs → page-state-BLL2JJBc.mjs} +22 -2
- package/dist/paths-DaVyTIDJ.mjs +14 -0
- package/dist/pending-checkout-DpVaNnTu.mjs +45 -0
- package/dist/problem-details-n20-dy3w.mjs +18 -0
- package/dist/{useApiKeys-CVgXfUop.mjs → useApiKeys-BOY3vIOr.mjs} +3 -9
- package/dist/{useDevPortalApiBaseUrl-BstWzITX.mjs → useDevPortalApiBaseUrl-BiI2jFjU.mjs} +14 -14
- package/dist/{useMySubscriptions-BtxAXA1m.mjs → useMySubscriptions-BYVIIWbe.mjs} +2 -2
- package/dist/{usePlans-DPGFWcVo.mjs → usePlans-HyhIIuJZ.mjs} +1 -1
- package/dist/{useUsage--5sZub3M.mjs → useUsage-B5x9YuKa.mjs} +1 -1
- package/package.json +3 -3
- package/dist/SubscriptionsPage-msO31jv-.mjs +0 -223
- package/dist/paths-ujy9wjVK.mjs +0 -12
- /package/dist/{demo-session-rN2Gj4lM.mjs → demo-session-yK9qEqV-.mjs} +0 -0
- /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-
|
|
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-
|
|
4
|
-
import { t as usePlans } from "./usePlans-
|
|
5
|
-
import { t as useUsage } from "./useUsage
|
|
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: [
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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: [
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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-
|
|
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-
|
|
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; /**
|
|
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-
|
|
2
|
-
import { a as
|
|
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-
|
|
4
|
-
import "./PricingCard-
|
|
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-
|
|
7
|
-
import { t as useMySubscriptions } from "./useMySubscriptions-
|
|
8
|
-
import { n as startPlansPrefetch, t as usePlans } from "./usePlans-
|
|
9
|
-
import
|
|
10
|
-
import { n as
|
|
11
|
-
import {
|
|
12
|
-
import { t as
|
|
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-
|
|
370
|
-
const CheckoutPage = lazy(() => import("./CheckoutPage-
|
|
371
|
-
const ContactSalesPage = lazy(() => import("./ContactSalesPage-
|
|
372
|
-
const CredentialsPage = lazy(() => import("./CredentialsPage-
|
|
373
|
-
const PricingPage = lazy(() => import("./PricingPage-
|
|
374
|
-
const SubscriptionsPage = lazy(() => import("./SubscriptionsPage-
|
|
375
|
-
const UsagePage = lazy(() => import("./UsagePage-
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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.
|
|
50
|
-
"@lukoweb/apitogo-module-user-panel": "0.2.
|
|
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",
|