@lukoweb/apitogo-plugin-dev-portal-billing 0.2.30 → 0.2.34
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-CXOfTwlX.mjs +208 -0
- package/dist/ApiKeysPanel-DrArkHXf.mjs +285 -0
- package/dist/CheckoutPage-CW_EBGog.mjs +174 -0
- package/dist/ContactSalesPage-Vjzm-RxO.mjs +256 -0
- package/dist/CredentialsPage-CtIEboEy.mjs +189 -0
- package/dist/DevPortalBillingContext-CR83s4LO.mjs +13 -0
- package/dist/PricingCard-Wra2FUX9.mjs +145 -0
- package/dist/PricingPage-MTslSnCh.mjs +233 -0
- package/dist/SubscriptionsPage-9lXnVks7.mjs +226 -0
- package/dist/UsagePage--mg_NzrA.mjs +203 -0
- package/dist/billing-state-pnT0H1Lm.mjs +37 -0
- package/dist/demo-session-Cez740bi.mjs +65 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +121 -2378
- package/dist/paths-ujy9wjVK.mjs +12 -0
- package/dist/pricing-config-CfhRX9tk.mjs +11 -0
- package/dist/useDevPortalApiBaseUrl-CgygF2z5.mjs +241 -0
- package/dist/useMySubscriptions-CLh3eFY4.mjs +42 -0
- package/dist/usePlans-DUex2tzT.mjs +69 -0
- package/dist/useUsage-Cx2GB3wg.mjs +164 -0
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,943 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { t as AccountPlansPage } from "./AccountPlansPage-CXOfTwlX.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 { a as formatPlanDisplayPrice, c as isActiveSubscription, f as listPlanFeatureLines, h as pickFeaturedPlanSku, p as parseManifestPlans, s as formatPlanPriceUnit, v as resolveDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
|
|
4
|
+
import "./PricingCard-Wra2FUX9.mjs";
|
|
5
|
+
import { t as DevPortalBillingProvider } from "./DevPortalBillingContext-CR83s4LO.mjs";
|
|
6
|
+
import { t as useMySubscriptions } from "./useMySubscriptions-CLh3eFY4.mjs";
|
|
7
|
+
import { n as startPlansPrefetch, t as usePlans } from "./usePlans-DUex2tzT.mjs";
|
|
8
|
+
import { a as useRevokeApiKey, i as useCreateApiKey, n as API_KEY_CAP_REACHED, r as useApiKeys, t as ApiKeysPanel } from "./ApiKeysPanel-DrArkHXf.mjs";
|
|
9
|
+
import { n as useUsageRecent, t as useUsage } from "./useUsage-Cx2GB3wg.mjs";
|
|
10
|
+
import { t as isPreviewPricingEnabled } from "./pricing-config-CfhRX9tk.mjs";
|
|
11
|
+
import { Head, Link } from "@lukoweb/apitogo/components";
|
|
12
|
+
import { ArrowRightIcon, KeyRoundIcon, StarsIcon } from "@lukoweb/apitogo/icons";
|
|
13
|
+
import { Outlet } from "@lukoweb/apitogo/router";
|
|
14
|
+
import { Suspense, lazy } from "react";
|
|
7
15
|
import manifestCatalog from "virtual:apitogo-local-manifest";
|
|
8
|
-
import
|
|
9
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
-
import { useMutation, useQuery, useQueryClient } from "@lukoweb/apitogo/react-query";
|
|
16
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
17
|
import { useAuth } from "@lukoweb/apitogo/hooks";
|
|
12
18
|
import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
|
|
13
|
-
import { Alert, AlertTitle } from "@lukoweb/apitogo/ui/Alert.js";
|
|
14
|
-
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@lukoweb/apitogo/ui/Dialog.js";
|
|
15
|
-
import { Input } from "@lukoweb/apitogo/ui/Input.js";
|
|
16
|
-
import { Secret } from "@lukoweb/apitogo/ui/Secret.js";
|
|
17
19
|
import { AccountLayout } from "@lukoweb/apitogo-module-user-panel";
|
|
18
20
|
import { createPlugin } from "@lukoweb/apitogo/plugins";
|
|
19
|
-
import { Alert as Alert$1, AlertAction, AlertDescription, AlertTitle as AlertTitle$1 } from "@lukoweb/apitogo/ui/Alert";
|
|
20
|
-
import { Button as Button$2 } from "@lukoweb/apitogo/ui/Button";
|
|
21
|
-
import { Input as Input$1 } from "@lukoweb/apitogo/ui/Input";
|
|
22
|
-
import { Label } from "@lukoweb/apitogo/ui/Label";
|
|
23
|
-
import { Textarea } from "@lukoweb/apitogo/ui/Textarea";
|
|
24
|
-
import { Value } from "@lukoweb/apitogo/ui/Value.js";
|
|
25
21
|
|
|
26
|
-
//#region src/paths.ts
|
|
27
|
-
const PRICING_PATH = "/pricing";
|
|
28
|
-
const ACCOUNT_PLANS_PATH = "/account/plans";
|
|
29
|
-
const CHECKOUT_PATH = "/checkout";
|
|
30
|
-
const CONTACT_SALES_PATH = "/contact-sales";
|
|
31
|
-
const SUBSCRIPTIONS_PATH = "/account/subscriptions";
|
|
32
|
-
const CREDENTIALS_PATH = "/account/credentials";
|
|
33
|
-
const USAGE_PATH = "/account/usage";
|
|
34
|
-
const LOGIN_NAV_PATH = "/signin";
|
|
35
|
-
|
|
36
|
-
//#endregion
|
|
37
|
-
//#region src/utils/dev-portal-api.ts
|
|
38
|
-
const DEV_PORTAL_PLACEHOLDER_API_URL = "https://dp-example.azurecontainerapps.io";
|
|
39
|
-
function resolveDevPortalApiBaseUrl(configuredUrl) {
|
|
40
|
-
const fromConfig = configuredUrl?.trim();
|
|
41
|
-
if (fromConfig) return fromConfig.replace(/\/+$/, "");
|
|
42
|
-
const envUrl = (typeof VITE_APITOGO_AUTHENTICATION_API_BASE_URL === "string" ? VITE_APITOGO_AUTHENTICATION_API_BASE_URL.trim() : "") || (typeof VITE_APITOGO_DEV_PORTAL_API_URL === "string" ? VITE_APITOGO_DEV_PORTAL_API_URL.trim() : "");
|
|
43
|
-
return envUrl ? envUrl.replace(/\/+$/, "") : void 0;
|
|
44
|
-
}
|
|
45
|
-
function isPlaceholderDevPortalApiUrl(apiBaseUrl) {
|
|
46
|
-
if (!apiBaseUrl) return true;
|
|
47
|
-
return apiBaseUrl === DEV_PORTAL_PLACEHOLDER_API_URL || apiBaseUrl.includes("dp-example.azurecontainerapps.io");
|
|
48
|
-
}
|
|
49
|
-
function resolveLimitsJson(plan) {
|
|
50
|
-
if (plan.limits && typeof plan.limits === "object" && !Array.isArray(plan.limits)) return JSON.stringify(plan.limits);
|
|
51
|
-
return plan.limitsJson ?? null;
|
|
52
|
-
}
|
|
53
|
-
function mapPreviewPlans(plans) {
|
|
54
|
-
return plans.map((plan) => ({
|
|
55
|
-
sku: plan.sku,
|
|
56
|
-
displayName: plan.displayName,
|
|
57
|
-
priceAmount: plan.priceAmount,
|
|
58
|
-
pricingModel: plan.pricingModel,
|
|
59
|
-
priceLabel: plan.priceLabel,
|
|
60
|
-
priceCurrency: plan.priceCurrency,
|
|
61
|
-
billingPeriod: plan.billingPeriod,
|
|
62
|
-
limitsJson: resolveLimitsJson(plan),
|
|
63
|
-
includedActionKeys: plan.includedActionKeys,
|
|
64
|
-
features: plan.features
|
|
65
|
-
}));
|
|
66
|
-
}
|
|
67
|
-
function readModulePlanItems() {
|
|
68
|
-
const items = (zudokuConfig?.modules)?.userPanel?.plans?.items;
|
|
69
|
-
return Array.isArray(items) && items.length > 0 ? items : void 0;
|
|
70
|
-
}
|
|
71
|
-
function parseManifestPlans(configPlans) {
|
|
72
|
-
if (configPlans?.length) return { plans: mapPreviewPlans(configPlans.map((plan) => ({
|
|
73
|
-
sku: plan.sku,
|
|
74
|
-
displayName: plan.displayName,
|
|
75
|
-
priceAmount: plan.priceAmount ?? 0,
|
|
76
|
-
pricingModel: plan.pricingModel,
|
|
77
|
-
priceLabel: plan.priceLabel,
|
|
78
|
-
priceCurrency: plan.priceCurrency ?? "usd",
|
|
79
|
-
billingPeriod: plan.billingPeriod ?? "monthly",
|
|
80
|
-
limits: plan.limits,
|
|
81
|
-
limitsJson: plan.limitsJson ?? null,
|
|
82
|
-
includedActionKeys: plan.includedActionKeys ?? [],
|
|
83
|
-
features: plan.features
|
|
84
|
-
}))) };
|
|
85
|
-
if (manifestCatalog?.plans?.length) return { plans: mapPreviewPlans(manifestCatalog.plans) };
|
|
86
|
-
const metaPlans = zudokuConfig?.__meta?.previewPlans;
|
|
87
|
-
if (metaPlans?.length) return { plans: mapPreviewPlans(metaPlans) };
|
|
88
|
-
const moduleItems = readModulePlanItems();
|
|
89
|
-
if (moduleItems?.length) return { plans: mapPreviewPlans(moduleItems.map((plan) => ({
|
|
90
|
-
sku: plan.sku,
|
|
91
|
-
displayName: plan.displayName,
|
|
92
|
-
priceAmount: plan.priceAmount ?? 0,
|
|
93
|
-
pricingModel: plan.pricingModel,
|
|
94
|
-
priceLabel: plan.priceLabel,
|
|
95
|
-
priceCurrency: plan.priceCurrency ?? "usd",
|
|
96
|
-
billingPeriod: plan.billingPeriod ?? "monthly",
|
|
97
|
-
limits: plan.limits,
|
|
98
|
-
limitsJson: plan.limitsJson ?? null,
|
|
99
|
-
includedActionKeys: plan.includedActionKeys ?? [],
|
|
100
|
-
features: plan.features
|
|
101
|
-
}))) };
|
|
102
|
-
return { plans: [] };
|
|
103
|
-
}
|
|
104
|
-
function formatActionKey(actionKey) {
|
|
105
|
-
const separatorIndex = actionKey.indexOf(":");
|
|
106
|
-
if (separatorIndex <= 0) return actionKey;
|
|
107
|
-
return `${actionKey.slice(0, separatorIndex).toUpperCase()} ${actionKey.slice(separatorIndex + 1)}`;
|
|
108
|
-
}
|
|
109
|
-
/** Custom-priced plans go to a contact form; a negative price is the legacy marker for the same. */
|
|
110
|
-
function isPlanCustomPriced(plan) {
|
|
111
|
-
return plan.pricingModel?.toLowerCase() === "custom" || plan.priceAmount < 0;
|
|
112
|
-
}
|
|
113
|
-
function formatCustomPriceLabel(plan) {
|
|
114
|
-
return plan.priceLabel?.trim() || "Custom";
|
|
115
|
-
}
|
|
116
|
-
function formatPlanPrice(plan) {
|
|
117
|
-
if (isPlanCustomPriced(plan)) return formatCustomPriceLabel(plan);
|
|
118
|
-
if (plan.priceAmount <= 0) return "Free";
|
|
119
|
-
const amount = new Intl.NumberFormat(void 0, {
|
|
120
|
-
style: "currency",
|
|
121
|
-
currency: plan.priceCurrency.toUpperCase(),
|
|
122
|
-
minimumFractionDigits: plan.priceAmount % 1 === 0 ? 0 : 2
|
|
123
|
-
}).format(plan.priceAmount);
|
|
124
|
-
const period = plan.billingPeriod.toLowerCase();
|
|
125
|
-
if (period === "monthly" || period === "month") return `${amount}/mo`;
|
|
126
|
-
if (period === "annual" || period === "year") return `${amount}/yr`;
|
|
127
|
-
return amount;
|
|
128
|
-
}
|
|
129
|
-
function isPlanFree(plan) {
|
|
130
|
-
return !isPlanCustomPriced(plan) && plan.priceAmount <= 0;
|
|
131
|
-
}
|
|
132
|
-
function formatBillingPeriod(period) {
|
|
133
|
-
const normalized = period.toLowerCase();
|
|
134
|
-
if (normalized === "month" || normalized === "monthly") return "per month";
|
|
135
|
-
if (normalized === "year" || normalized === "annual") return "per year";
|
|
136
|
-
if (normalized === "custom") return "talk to us";
|
|
137
|
-
return period;
|
|
138
|
-
}
|
|
139
|
-
/** Headline price for a plan card, without the period suffix (e.g. `$19`, `$0`, `Custom`). */
|
|
140
|
-
function formatPlanDisplayPrice(plan) {
|
|
141
|
-
if (isPlanCustomPriced(plan)) return formatCustomPriceLabel(plan);
|
|
142
|
-
if (isPlanFree(plan)) return "$0";
|
|
143
|
-
const priceLabel = formatPlanPrice(plan);
|
|
144
|
-
return priceLabel.replace(/\/mo$/, "").replace(/\/yr$/, "") || priceLabel;
|
|
145
|
-
}
|
|
146
|
-
/** Caption shown under the plan price (e.g. `per month`, `talk to us`). */
|
|
147
|
-
function formatPlanPriceUnit(plan) {
|
|
148
|
-
return isPlanCustomPriced(plan) ? "talk to us" : formatBillingPeriod(plan.billingPeriod);
|
|
149
|
-
}
|
|
150
|
-
/** SKU of the plan to highlight: `developer` if present, else the first low-priced paid plan. */
|
|
151
|
-
function pickFeaturedPlanSku(plans) {
|
|
152
|
-
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;
|
|
153
|
-
}
|
|
154
|
-
function formatRateLimit(rate) {
|
|
155
|
-
if (!rate || typeof rate !== "object") return null;
|
|
156
|
-
const { calls, renewalPeriodSeconds } = rate;
|
|
157
|
-
if (calls == null || renewalPeriodSeconds == null) return null;
|
|
158
|
-
if (renewalPeriodSeconds === 1) return `${calls} req/sec rate limit`;
|
|
159
|
-
const window = renewalPeriodSeconds === 60 ? "minute" : `${renewalPeriodSeconds} seconds`;
|
|
160
|
-
return `${calls} request${calls === 1 ? "" : "s"} per ${window}`;
|
|
161
|
-
}
|
|
162
|
-
function formatQuota(quota) {
|
|
163
|
-
if (!quota || typeof quota !== "object") return null;
|
|
164
|
-
const { calls, renewalPeriodDays } = quota;
|
|
165
|
-
if (calls == null) return null;
|
|
166
|
-
if (calls < 0) return "Unlimited requests";
|
|
167
|
-
const formatted = calls.toLocaleString();
|
|
168
|
-
if (renewalPeriodDays === 30 || renewalPeriodDays === 31) return `${formatted} requests / month`;
|
|
169
|
-
if (renewalPeriodDays === 1) return `${formatted} requests / day`;
|
|
170
|
-
if (renewalPeriodDays != null) return `${formatted} requests / ${renewalPeriodDays} days`;
|
|
171
|
-
return `${formatted} requests`;
|
|
172
|
-
}
|
|
173
|
-
/** Marketing bullets keyed by common sample SKUs when limitsJson is sparse. */
|
|
174
|
-
const SAMPLE_PLAN_FEATURE_EXTRAS = {
|
|
175
|
-
free: ["All endpoints"],
|
|
176
|
-
developer: ["Webhooks included", "Email support"],
|
|
177
|
-
team: ["5 team seats", "Priority support"],
|
|
178
|
-
enterprise: [
|
|
179
|
-
"Unlimited requests",
|
|
180
|
-
"Custom rate limits",
|
|
181
|
-
"SSO & audit logs",
|
|
182
|
-
"Dedicated SLA"
|
|
183
|
-
]
|
|
184
|
-
};
|
|
185
|
-
function listPlanFeatureLines(plan) {
|
|
186
|
-
if (plan.features && plan.features.length > 0) return plan.features;
|
|
187
|
-
const lines = [];
|
|
188
|
-
if (plan.limitsJson?.trim()) try {
|
|
189
|
-
const limits = JSON.parse(plan.limitsJson);
|
|
190
|
-
const quotaLine = formatQuota(limits.quota);
|
|
191
|
-
if (quotaLine) lines.push(quotaLine);
|
|
192
|
-
const rateLine = formatRateLimit(limits.rateLimit);
|
|
193
|
-
if (rateLine) lines.push(rateLine);
|
|
194
|
-
} catch {}
|
|
195
|
-
const extras = SAMPLE_PLAN_FEATURE_EXTRAS[plan.sku.toLowerCase()];
|
|
196
|
-
if (extras) {
|
|
197
|
-
for (const extra of extras) if (!lines.includes(extra)) lines.push(extra);
|
|
198
|
-
}
|
|
199
|
-
if (plan.includedActionKeys && plan.includedActionKeys.length > 0) for (const actionKey of plan.includedActionKeys) lines.push(formatActionKey(actionKey));
|
|
200
|
-
if (lines.length === 0 && isPlanFree(plan)) lines.push("API access not included");
|
|
201
|
-
return lines;
|
|
202
|
-
}
|
|
203
|
-
function isActiveSubscription(status) {
|
|
204
|
-
return status.toLowerCase() === "active";
|
|
205
|
-
}
|
|
206
|
-
/** Returns monthly call quota from plan limitsJson, or null when unknown/unlimited. */
|
|
207
|
-
function parseMonthlyQuotaCalls(limitsJson) {
|
|
208
|
-
if (!limitsJson?.trim()) return null;
|
|
209
|
-
try {
|
|
210
|
-
const quota = JSON.parse(limitsJson).quota;
|
|
211
|
-
if (!quota || typeof quota !== "object") return null;
|
|
212
|
-
const { calls, renewalPeriodDays } = quota;
|
|
213
|
-
if (calls == null || calls < 0) return null;
|
|
214
|
-
if (renewalPeriodDays == null || renewalPeriodDays === 30 || renewalPeriodDays === 31) return calls;
|
|
215
|
-
return null;
|
|
216
|
-
} catch {
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
//#endregion
|
|
222
|
-
//#region src/components/PricingCard.tsx
|
|
223
|
-
const publicCtaLabel = (plan) => {
|
|
224
|
-
if (isPlanCustomPriced(plan)) return "Contact sales";
|
|
225
|
-
if (isPlanFree(plan)) return "Get started for free";
|
|
226
|
-
return "Subscribe now";
|
|
227
|
-
};
|
|
228
|
-
const accountCtaLabel = (plan) => {
|
|
229
|
-
if (isPlanCustomPriced(plan)) return "Contact sales";
|
|
230
|
-
return isPlanFree(plan) ? "Get started" : "Subscribe";
|
|
231
|
-
};
|
|
232
|
-
/** Custom-priced plans skip checkout and open the contact-sales form. */
|
|
233
|
-
const planCtaPath = (plan) => `${isPlanCustomPriced(plan) ? CONTACT_SALES_PATH : CHECKOUT_PATH}?planSku=${encodeURIComponent(plan.sku)}`;
|
|
234
|
-
const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, onSwitch, isSwitching = false, isCanceling = false, cancelDisabled = false, featured = false }) => {
|
|
235
|
-
const isActive = isSubscribed || isActiveSubscription(subscriptionStatus ?? "");
|
|
236
|
-
const features = listPlanFeatureLines(plan);
|
|
237
|
-
const displayPrice = formatPlanDisplayPrice(plan);
|
|
238
|
-
return /* @__PURE__ */ jsxs("article", {
|
|
239
|
-
className: cn("flex h-full w-full flex-col rounded-xl border border-border bg-card p-7", featured && "border-primary/50 shadow-[0_10px_28px_rgba(13,148,136,0.28)] ring-1 ring-primary/25", isActive && mode === "account" && "border-primary/40 ring-1 ring-primary/20"),
|
|
240
|
-
"data-testid": `pricing-card-${plan.sku}`,
|
|
241
|
-
children: [
|
|
242
|
-
/* @__PURE__ */ jsxs("div", {
|
|
243
|
-
className: "flex items-center justify-between gap-2",
|
|
244
|
-
children: [
|
|
245
|
-
/* @__PURE__ */ jsx("h2", {
|
|
246
|
-
className: "text-sm font-bold uppercase tracking-[0.04em]",
|
|
247
|
-
children: plan.displayName
|
|
248
|
-
}),
|
|
249
|
-
featured && !isActive ? /* @__PURE__ */ jsx("span", {
|
|
250
|
-
className: "shrink-0 rounded-md bg-primary/10 px-2.5 py-0.5 text-xs font-semibold text-primary",
|
|
251
|
-
children: "Popular"
|
|
252
|
-
}) : null,
|
|
253
|
-
isActive && mode === "account" ? /* @__PURE__ */ jsx("span", {
|
|
254
|
-
className: "shrink-0 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary",
|
|
255
|
-
"data-testid": `subscribed-badge-${plan.sku}`,
|
|
256
|
-
children: "Subscribed"
|
|
257
|
-
}) : null
|
|
258
|
-
]
|
|
259
|
-
}),
|
|
260
|
-
/* @__PURE__ */ jsx("p", {
|
|
261
|
-
className: "mt-3.5 text-[2.1rem] font-extrabold tracking-[-0.02em]",
|
|
262
|
-
children: displayPrice
|
|
263
|
-
}),
|
|
264
|
-
/* @__PURE__ */ jsx("p", {
|
|
265
|
-
className: "mb-[18px] mt-0.5 text-[0.78125rem] text-muted-foreground",
|
|
266
|
-
children: formatPlanPriceUnit(plan)
|
|
267
|
-
}),
|
|
268
|
-
/* @__PURE__ */ jsx("div", {
|
|
269
|
-
className: "space-y-2",
|
|
270
|
-
children: mode === "account" ? isActive ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
271
|
-
/* @__PURE__ */ jsx(Button, {
|
|
272
|
-
variant: "secondary",
|
|
273
|
-
className: "w-full",
|
|
274
|
-
disabled: true,
|
|
275
|
-
children: "Subscribed"
|
|
276
|
-
}),
|
|
277
|
-
onUnsubscribe ? /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
|
|
278
|
-
asChild: true,
|
|
279
|
-
children: /* @__PURE__ */ jsx(Button, {
|
|
280
|
-
variant: "outline",
|
|
281
|
-
className: "w-full",
|
|
282
|
-
disabled: cancelDisabled || isCanceling,
|
|
283
|
-
"data-testid": `unsubscribe-${plan.sku}`,
|
|
284
|
-
children: isCanceling ? "Canceling…" : "Unsubscribe"
|
|
285
|
-
})
|
|
286
|
-
}), /* @__PURE__ */ jsxs(AlertDialogContent, { children: [/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [/* @__PURE__ */ jsxs(AlertDialogTitle, { children: [
|
|
287
|
-
"Unsubscribe from ",
|
|
288
|
-
plan.displayName,
|
|
289
|
-
"?"
|
|
290
|
-
] }), /* @__PURE__ */ jsx(AlertDialogDescription, { children: "Your API access for this plan will be revoked. You can subscribe again later." })] }), /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [/* @__PURE__ */ jsx(AlertDialogCancel, { children: "Keep subscription" }), /* @__PURE__ */ jsx(AlertDialogAction, {
|
|
291
|
-
onClick: () => onUnsubscribe(plan.sku),
|
|
292
|
-
disabled: isCanceling,
|
|
293
|
-
children: "Unsubscribe"
|
|
294
|
-
})] })] })] }) : null,
|
|
295
|
-
cancelDisabled ? /* @__PURE__ */ jsx("p", {
|
|
296
|
-
className: "text-center text-xs text-muted-foreground",
|
|
297
|
-
children: "Unsubscribe is available after the portal is published."
|
|
298
|
-
}) : null
|
|
299
|
-
] }) : onSwitch && !isPlanCustomPriced(plan) ? /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
|
|
300
|
-
asChild: true,
|
|
301
|
-
children: /* @__PURE__ */ jsx(Button, {
|
|
302
|
-
variant: featured ? "default" : "outline",
|
|
303
|
-
className: "w-full",
|
|
304
|
-
disabled: isSwitching,
|
|
305
|
-
"data-testid": `switch-${plan.sku}`,
|
|
306
|
-
children: isSwitching ? "Switching…" : "Switch to this plan"
|
|
307
|
-
})
|
|
308
|
-
}), /* @__PURE__ */ jsxs(AlertDialogContent, { children: [/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [/* @__PURE__ */ jsxs(AlertDialogTitle, { children: [
|
|
309
|
-
"Switch to ",
|
|
310
|
-
plan.displayName,
|
|
311
|
-
"?"
|
|
312
|
-
] }), /* @__PURE__ */ jsxs(AlertDialogDescription, { children: [
|
|
313
|
-
"Your current plan is replaced by ",
|
|
314
|
-
plan.displayName,
|
|
315
|
-
". The API keys of your current plan stop working. Create a key for",
|
|
316
|
-
" ",
|
|
317
|
-
plan.displayName,
|
|
318
|
-
" on the API keys page."
|
|
319
|
-
] })] }), /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [/* @__PURE__ */ jsx(AlertDialogCancel, { children: "Keep current plan" }), /* @__PURE__ */ jsx(AlertDialogAction, {
|
|
320
|
-
onClick: () => onSwitch(plan.sku),
|
|
321
|
-
disabled: isSwitching,
|
|
322
|
-
children: "Switch plan"
|
|
323
|
-
})] })] })] }) : /* @__PURE__ */ jsx(Button, {
|
|
324
|
-
variant: featured ? "default" : "outline",
|
|
325
|
-
className: "w-full",
|
|
326
|
-
asChild: true,
|
|
327
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
328
|
-
to: planCtaPath(plan),
|
|
329
|
-
children: accountCtaLabel(plan)
|
|
330
|
-
})
|
|
331
|
-
}) : /* @__PURE__ */ jsx(Button, {
|
|
332
|
-
variant: featured ? "default" : "outline",
|
|
333
|
-
className: "w-full",
|
|
334
|
-
asChild: true,
|
|
335
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
336
|
-
to: planCtaPath(plan),
|
|
337
|
-
children: publicCtaLabel(plan)
|
|
338
|
-
})
|
|
339
|
-
})
|
|
340
|
-
}),
|
|
341
|
-
features.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
342
|
-
className: "mt-5 flex flex-1 flex-col gap-[11px] border-t border-border pt-[18px]",
|
|
343
|
-
"data-testid": plan.includedActionKeys && plan.includedActionKeys.length > 0 ? `included-endpoints-${plan.sku}` : void 0,
|
|
344
|
-
children: features.map((feat) => /* @__PURE__ */ jsxs("div", {
|
|
345
|
-
className: "flex items-start gap-2.5 text-[0.84375rem] text-muted-foreground",
|
|
346
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
347
|
-
className: "mt-0.5 shrink-0 text-emerald-600",
|
|
348
|
-
"aria-hidden": true,
|
|
349
|
-
children: "✓"
|
|
350
|
-
}), /* @__PURE__ */ jsx("span", { children: feat })]
|
|
351
|
-
}, feat))
|
|
352
|
-
}) : null
|
|
353
|
-
]
|
|
354
|
-
});
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
//#endregion
|
|
358
|
-
//#region src/DevPortalBillingContext.tsx
|
|
359
|
-
const DevPortalBillingContext = createContext({});
|
|
360
|
-
const DevPortalBillingProvider = ({ options, children }) => /* @__PURE__ */ jsx(DevPortalBillingContext.Provider, {
|
|
361
|
-
value: options,
|
|
362
|
-
children
|
|
363
|
-
});
|
|
364
|
-
const useDevPortalBillingConfig = () => useContext(DevPortalBillingContext);
|
|
365
|
-
|
|
366
|
-
//#endregion
|
|
367
|
-
//#region src/hooks/useDevPortalApiBaseUrl.ts
|
|
368
|
-
function useDevPortalApiBaseUrl() {
|
|
369
|
-
const providerData = useAuth().providerData;
|
|
370
|
-
if (providerData?.type === "dev-portal" && providerData.apiBaseUrl) return providerData.apiBaseUrl;
|
|
371
|
-
return resolveDevPortalApiBaseUrl();
|
|
372
|
-
}
|
|
373
|
-
function useIsDemoSession() {
|
|
374
|
-
return useAuth().isDemoSession === true;
|
|
375
|
-
}
|
|
376
|
-
function useDevPortalPreviewMode() {
|
|
377
|
-
const auth = useAuth();
|
|
378
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
379
|
-
if (auth.isDemoSession) return false;
|
|
380
|
-
if (auth.authMode === "preview" || auth.isBackendAvailable === false) return true;
|
|
381
|
-
return isPlaceholderDevPortalApiUrl(apiBaseUrl);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
//#endregion
|
|
385
|
-
//#region src/hooks/useCancelSubscription.ts
|
|
386
|
-
function useCancelSubscription() {
|
|
387
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
388
|
-
const queryClient = useQueryClient();
|
|
389
|
-
return useMutation({
|
|
390
|
-
mutationFn: async (planSku) => {
|
|
391
|
-
if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
|
|
392
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/cancel`, {
|
|
393
|
-
method: "POST",
|
|
394
|
-
headers: {
|
|
395
|
-
Accept: "application/json",
|
|
396
|
-
"Content-Type": "application/json"
|
|
397
|
-
},
|
|
398
|
-
credentials: "include",
|
|
399
|
-
body: JSON.stringify({ planSku })
|
|
400
|
-
});
|
|
401
|
-
if (!response.ok) {
|
|
402
|
-
const detail = await response.text();
|
|
403
|
-
throw new Error(detail || "Failed to cancel subscription.");
|
|
404
|
-
}
|
|
405
|
-
return await response.json();
|
|
406
|
-
},
|
|
407
|
-
onSuccess: async () => {
|
|
408
|
-
await queryClient.invalidateQueries({ queryKey: ["dev-portal-me", apiBaseUrl] });
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
//#endregion
|
|
414
|
-
//#region src/hooks/useChangePlan.ts
|
|
415
|
-
function useChangePlan() {
|
|
416
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
417
|
-
const queryClient = useQueryClient();
|
|
418
|
-
return useMutation({
|
|
419
|
-
mutationFn: async ({ fromPlanSku, toPlanSku }) => {
|
|
420
|
-
if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
|
|
421
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/change`, {
|
|
422
|
-
method: "POST",
|
|
423
|
-
headers: {
|
|
424
|
-
Accept: "application/json",
|
|
425
|
-
"Content-Type": "application/json"
|
|
426
|
-
},
|
|
427
|
-
credentials: "include",
|
|
428
|
-
body: JSON.stringify({
|
|
429
|
-
fromPlanSku,
|
|
430
|
-
toPlanSku
|
|
431
|
-
})
|
|
432
|
-
});
|
|
433
|
-
if (!response.ok) {
|
|
434
|
-
const detail = await response.text();
|
|
435
|
-
throw new Error(detail || "Failed to change plan.");
|
|
436
|
-
}
|
|
437
|
-
return await response.json();
|
|
438
|
-
},
|
|
439
|
-
onSuccess: async (result) => {
|
|
440
|
-
if (result.checkoutUrl) {
|
|
441
|
-
window.location.assign(result.checkoutUrl);
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
await queryClient.invalidateQueries({ queryKey: ["dev-portal-me", apiBaseUrl] });
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
//#endregion
|
|
450
|
-
//#region src/utils/demo-session.ts
|
|
451
|
-
const DEMO_SUBSCRIPTION_KEY = "demo-subscription-key";
|
|
452
|
-
const DEMO_GATEWAY_KEY_HEADER = "Ocp-Apim-Subscription-Key";
|
|
453
|
-
const DEMO_MAX_KEYS = 5;
|
|
454
|
-
function resolveDemoPlans(plans) {
|
|
455
|
-
if (plans && plans.length > 0) return plans.map((plan) => ({
|
|
456
|
-
sku: plan.sku,
|
|
457
|
-
displayName: plan.displayName
|
|
458
|
-
}));
|
|
459
|
-
return [{
|
|
460
|
-
sku: "free",
|
|
461
|
-
displayName: "Free"
|
|
462
|
-
}];
|
|
463
|
-
}
|
|
464
|
-
function createDemoMeResponse(plans) {
|
|
465
|
-
const periodEnd = /* @__PURE__ */ new Date();
|
|
466
|
-
periodEnd.setMonth(periodEnd.getMonth() + 1);
|
|
467
|
-
return {
|
|
468
|
-
userId: "00000000-0000-0000-0000-000000000001",
|
|
469
|
-
displayName: "Demo User",
|
|
470
|
-
email: "demo@example.com",
|
|
471
|
-
emailVerified: true,
|
|
472
|
-
hasNativePassword: true,
|
|
473
|
-
subscriptions: resolveDemoPlans(plans).map((plan) => ({
|
|
474
|
-
planSku: plan.sku,
|
|
475
|
-
status: "Active",
|
|
476
|
-
entitlementState: "active",
|
|
477
|
-
currentPeriodEnd: periodEnd.toISOString()
|
|
478
|
-
}))
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
function createDemoCredentials(plans) {
|
|
482
|
-
return {
|
|
483
|
-
idToken: "demo-id-token",
|
|
484
|
-
accessToken: null,
|
|
485
|
-
gatewayBaseUrl: null,
|
|
486
|
-
planSkus: resolveDemoPlans(plans).map((plan) => plan.sku),
|
|
487
|
-
expiresAt: null
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
function createDemoApiKeys(plans) {
|
|
491
|
-
return {
|
|
492
|
-
plans: resolveDemoPlans(plans).map((plan, index) => ({
|
|
493
|
-
planSku: plan.sku,
|
|
494
|
-
displayName: plan.displayName,
|
|
495
|
-
keys: index === 0 ? [{
|
|
496
|
-
id: "00000000-0000-0000-0000-0000000000a1",
|
|
497
|
-
name: "default",
|
|
498
|
-
keyPrefix: DEMO_SUBSCRIPTION_KEY.slice(0, 8),
|
|
499
|
-
status: "active",
|
|
500
|
-
createdAtUtc: (/* @__PURE__ */ new Date()).toISOString(),
|
|
501
|
-
revokedAtUtc: null,
|
|
502
|
-
key: null
|
|
503
|
-
}] : [],
|
|
504
|
-
activeCount: index === 0 ? 1 : 0,
|
|
505
|
-
maxKeys: DEMO_MAX_KEYS,
|
|
506
|
-
canCreate: true
|
|
507
|
-
})),
|
|
508
|
-
header: DEMO_GATEWAY_KEY_HEADER,
|
|
509
|
-
gatewayBaseUrl: null
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
//#endregion
|
|
514
|
-
//#region src/hooks/useMySubscriptions.ts
|
|
515
|
-
function useMySubscriptions() {
|
|
516
|
-
const auth = useAuth();
|
|
517
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
518
|
-
const isPreview = useDevPortalPreviewMode();
|
|
519
|
-
const isDemoSession = useIsDemoSession();
|
|
520
|
-
const { previewPlans } = useDevPortalBillingConfig();
|
|
521
|
-
const liveQuery = useQuery({
|
|
522
|
-
queryKey: ["dev-portal-me", apiBaseUrl],
|
|
523
|
-
enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
|
|
524
|
-
queryFn: async () => {
|
|
525
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/auth/me`, {
|
|
526
|
-
method: "GET",
|
|
527
|
-
headers: { Accept: "application/json" },
|
|
528
|
-
credentials: "include"
|
|
529
|
-
});
|
|
530
|
-
if (!response.ok) throw new Error("Failed to load subscription status.");
|
|
531
|
-
return await response.json();
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
if (isDemoSession && auth.isAuthenticated) return {
|
|
535
|
-
...liveQuery,
|
|
536
|
-
data: createDemoMeResponse(previewPlans),
|
|
537
|
-
error: null,
|
|
538
|
-
isError: false,
|
|
539
|
-
isFetching: false,
|
|
540
|
-
isLoading: false,
|
|
541
|
-
isPending: false,
|
|
542
|
-
isSuccess: true,
|
|
543
|
-
status: "success"
|
|
544
|
-
};
|
|
545
|
-
return liveQuery;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
//#endregion
|
|
549
|
-
//#region src/hooks/usePlans.ts
|
|
550
|
-
function usePlans() {
|
|
551
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
552
|
-
const isPreview = useDevPortalPreviewMode();
|
|
553
|
-
const isDemoSession = useIsDemoSession();
|
|
554
|
-
const { previewPlans } = useDevPortalBillingConfig();
|
|
555
|
-
const liveQuery = useQuery({
|
|
556
|
-
queryKey: ["dev-portal-plans", apiBaseUrl],
|
|
557
|
-
enabled: !isPreview && !isDemoSession && Boolean(apiBaseUrl),
|
|
558
|
-
queryFn: async () => {
|
|
559
|
-
if (!apiBaseUrl) return { plans: [] };
|
|
560
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/plans`, { headers: { Accept: "application/json" } });
|
|
561
|
-
if (!response.ok) throw new Error("Failed to load plans.");
|
|
562
|
-
return await response.json();
|
|
563
|
-
}
|
|
564
|
-
});
|
|
565
|
-
if (isPreview || isDemoSession) {
|
|
566
|
-
const data = parseManifestPlans(previewPlans);
|
|
567
|
-
return {
|
|
568
|
-
...liveQuery,
|
|
569
|
-
data,
|
|
570
|
-
dataUpdatedAt: Date.now(),
|
|
571
|
-
error: null,
|
|
572
|
-
fetchStatus: "idle",
|
|
573
|
-
isError: false,
|
|
574
|
-
isFetching: false,
|
|
575
|
-
isLoading: false,
|
|
576
|
-
isPending: false,
|
|
577
|
-
isSuccess: true,
|
|
578
|
-
status: "success"
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
return liveQuery;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
//#endregion
|
|
585
|
-
//#region src/pages/AccountPlansPage.tsx
|
|
586
|
-
const AccountPlansPage = () => {
|
|
587
|
-
const { previewPlans } = useDevPortalBillingConfig();
|
|
588
|
-
const isPreview = useDevPortalPreviewMode();
|
|
589
|
-
const isDemoSession = useIsDemoSession();
|
|
590
|
-
const livePlans = usePlans();
|
|
591
|
-
const catalog = isPreview ? parseManifestPlans(previewPlans) : livePlans.data;
|
|
592
|
-
const isLoading = isPreview ? false : livePlans.isLoading;
|
|
593
|
-
const isError = isPreview ? false : livePlans.isError;
|
|
594
|
-
const { data: me } = useMySubscriptions();
|
|
595
|
-
const cancelMutation = useCancelSubscription();
|
|
596
|
-
const changeMutation = useChangePlan();
|
|
597
|
-
const [cancelingSku, setCancelingSku] = useState(null);
|
|
598
|
-
const [switchingSku, setSwitchingSku] = useState(null);
|
|
599
|
-
const subscriptionBySku = new Map((me?.subscriptions ?? []).map((sub) => [sub.planSku, sub]));
|
|
600
|
-
const activePlanSkus = (me?.subscriptions ?? []).filter((sub) => isActiveSubscription(sub.status)).map((sub) => sub.planSku);
|
|
601
|
-
const currentPlanSku = activePlanSkus.length === 1 ? activePlanSkus[0] : null;
|
|
602
|
-
const handleSwitch = async (toPlanSku) => {
|
|
603
|
-
if (!currentPlanSku) return;
|
|
604
|
-
setSwitchingSku(toPlanSku);
|
|
605
|
-
try {
|
|
606
|
-
await changeMutation.mutateAsync({
|
|
607
|
-
fromPlanSku: currentPlanSku,
|
|
608
|
-
toPlanSku
|
|
609
|
-
});
|
|
610
|
-
} finally {
|
|
611
|
-
setSwitchingSku(null);
|
|
612
|
-
}
|
|
613
|
-
};
|
|
614
|
-
const handleUnsubscribe = async (planSku) => {
|
|
615
|
-
setCancelingSku(planSku);
|
|
616
|
-
try {
|
|
617
|
-
await cancelMutation.mutateAsync(planSku);
|
|
618
|
-
} finally {
|
|
619
|
-
setCancelingSku(null);
|
|
620
|
-
}
|
|
621
|
-
};
|
|
622
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
623
|
-
className: "mx-auto max-w-screen-xl px-4 pb-16 pt-(--padding-content-top) lg:px-8",
|
|
624
|
-
children: [
|
|
625
|
-
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Plans & billing" }) }),
|
|
626
|
-
/* @__PURE__ */ jsxs("div", {
|
|
627
|
-
className: "mb-8 flex items-start gap-4",
|
|
628
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
629
|
-
className: "flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary",
|
|
630
|
-
children: /* @__PURE__ */ jsx(CreditCardIcon, { size: 24 })
|
|
631
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
632
|
-
className: "space-y-1",
|
|
633
|
-
children: [/* @__PURE__ */ jsx("h1", {
|
|
634
|
-
className: "text-3xl font-semibold tracking-tight",
|
|
635
|
-
children: "Plans & billing"
|
|
636
|
-
}), /* @__PURE__ */ jsx("p", {
|
|
637
|
-
className: "max-w-2xl text-muted-foreground",
|
|
638
|
-
children: "Compare plans, manage your subscriptions, and change tiers."
|
|
639
|
-
})]
|
|
640
|
-
})]
|
|
641
|
-
}),
|
|
642
|
-
isDemoSession ? /* @__PURE__ */ jsx("div", {
|
|
643
|
-
className: "mb-8 rounded-xl border border-dashed bg-muted/20 p-6 text-center text-sm text-muted-foreground",
|
|
644
|
-
children: "Local demo mode shows plans from your manifest with a sample active subscription. Billing actions activate after publish."
|
|
645
|
-
}) : null,
|
|
646
|
-
cancelMutation.isError ? /* @__PURE__ */ jsx("div", {
|
|
647
|
-
className: "mb-6 rounded-xl border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive",
|
|
648
|
-
children: cancelMutation.error instanceof Error ? cancelMutation.error.message : "Failed to cancel subscription."
|
|
649
|
-
}) : null,
|
|
650
|
-
changeMutation.isError ? /* @__PURE__ */ jsx("div", {
|
|
651
|
-
className: "mb-6 rounded-xl border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive",
|
|
652
|
-
children: changeMutation.error instanceof Error ? changeMutation.error.message : "Failed to change plan."
|
|
653
|
-
}) : null,
|
|
654
|
-
isLoading ? /* @__PURE__ */ jsx("p", {
|
|
655
|
-
className: "text-muted-foreground",
|
|
656
|
-
children: "Loading plans…"
|
|
657
|
-
}) : null,
|
|
658
|
-
isError ? /* @__PURE__ */ jsx("p", {
|
|
659
|
-
className: "text-destructive",
|
|
660
|
-
children: "Unable to load plans."
|
|
661
|
-
}) : null,
|
|
662
|
-
catalog && catalog.plans.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
663
|
-
className: "grid grid-cols-1 justify-center gap-6 sm:grid-cols-[repeat(auto-fit,minmax(280px,max-content))]",
|
|
664
|
-
children: catalog.plans.map((plan) => {
|
|
665
|
-
const subscription = subscriptionBySku.get(plan.sku);
|
|
666
|
-
const isSubscribed = isActiveSubscription(subscription?.status ?? "");
|
|
667
|
-
return /* @__PURE__ */ jsx(PricingCard, {
|
|
668
|
-
plan,
|
|
669
|
-
mode: "account",
|
|
670
|
-
subscriptionStatus: subscription?.status,
|
|
671
|
-
isSubscribed,
|
|
672
|
-
onUnsubscribe: isPreview || isDemoSession ? void 0 : handleUnsubscribe,
|
|
673
|
-
onSwitch: currentPlanSku && !isPreview && !isDemoSession ? handleSwitch : void 0,
|
|
674
|
-
isSwitching: switchingSku === plan.sku,
|
|
675
|
-
isCanceling: cancelingSku === plan.sku,
|
|
676
|
-
cancelDisabled: isPreview || isDemoSession
|
|
677
|
-
}, plan.sku);
|
|
678
|
-
})
|
|
679
|
-
}) : !isLoading && !isError ? /* @__PURE__ */ jsx("div", {
|
|
680
|
-
className: "rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
|
|
681
|
-
children: "No plans are available yet. Sync plans from your API ToGo manifest or platform gateway."
|
|
682
|
-
}) : null
|
|
683
|
-
]
|
|
684
|
-
});
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
//#endregion
|
|
688
|
-
//#region src/hooks/useApiKeys.ts
|
|
689
|
-
/** Thrown when the developer has hit their plan's per-developer key cap. */
|
|
690
|
-
const API_KEY_CAP_REACHED = "API_KEY_CAP_REACHED";
|
|
691
|
-
function apiKeysQueryKey(apiBaseUrl) {
|
|
692
|
-
return ["dev-portal-api-keys", apiBaseUrl];
|
|
693
|
-
}
|
|
694
|
-
function useApiKeys() {
|
|
695
|
-
const auth = useAuth();
|
|
696
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
697
|
-
const isPreview = useDevPortalPreviewMode();
|
|
698
|
-
const isDemoSession = useIsDemoSession();
|
|
699
|
-
const { previewPlans } = useDevPortalBillingConfig();
|
|
700
|
-
const liveQuery = useQuery({
|
|
701
|
-
queryKey: apiKeysQueryKey(apiBaseUrl),
|
|
702
|
-
enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
|
|
703
|
-
queryFn: async () => {
|
|
704
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
|
|
705
|
-
method: "GET",
|
|
706
|
-
headers: { Accept: "application/json" },
|
|
707
|
-
credentials: "include"
|
|
708
|
-
});
|
|
709
|
-
if (response.status === 404) throw new Error("NO_ENTITLEMENT");
|
|
710
|
-
if (!response.ok) throw new Error("Failed to load API keys.");
|
|
711
|
-
return await response.json();
|
|
712
|
-
},
|
|
713
|
-
retry: (failureCount, error) => error instanceof Error && error.message === "NO_ENTITLEMENT" ? false : failureCount < 2
|
|
714
|
-
});
|
|
715
|
-
if (isDemoSession && auth.isAuthenticated) return {
|
|
716
|
-
...liveQuery,
|
|
717
|
-
data: createDemoApiKeys(previewPlans),
|
|
718
|
-
error: null,
|
|
719
|
-
isError: false,
|
|
720
|
-
isFetching: false,
|
|
721
|
-
isLoading: false,
|
|
722
|
-
isPending: false,
|
|
723
|
-
isSuccess: true,
|
|
724
|
-
status: "success"
|
|
725
|
-
};
|
|
726
|
-
return liveQuery;
|
|
727
|
-
}
|
|
728
|
-
function useCreateApiKey() {
|
|
729
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
730
|
-
const queryClient = useQueryClient();
|
|
731
|
-
return useMutation({
|
|
732
|
-
mutationFn: async ({ name, planSku }) => {
|
|
733
|
-
if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
|
|
734
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
|
|
735
|
-
method: "POST",
|
|
736
|
-
headers: {
|
|
737
|
-
Accept: "application/json",
|
|
738
|
-
"Content-Type": "application/json"
|
|
739
|
-
},
|
|
740
|
-
credentials: "include",
|
|
741
|
-
body: JSON.stringify({
|
|
742
|
-
name,
|
|
743
|
-
planSku
|
|
744
|
-
})
|
|
745
|
-
});
|
|
746
|
-
if (response.status === 409) throw new Error(API_KEY_CAP_REACHED);
|
|
747
|
-
if (!response.ok) throw new Error(await readProblemDetail$1(response) ?? "Failed to create API key.");
|
|
748
|
-
return await response.json();
|
|
749
|
-
},
|
|
750
|
-
onSuccess: async () => {
|
|
751
|
-
await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
|
|
752
|
-
}
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
function useRevokeApiKey() {
|
|
756
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
757
|
-
const queryClient = useQueryClient();
|
|
758
|
-
return useMutation({
|
|
759
|
-
mutationFn: async (keyId) => {
|
|
760
|
-
if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
|
|
761
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/api-keys/${keyId}`, {
|
|
762
|
-
method: "DELETE",
|
|
763
|
-
headers: { Accept: "application/json" },
|
|
764
|
-
credentials: "include"
|
|
765
|
-
});
|
|
766
|
-
if (!response.ok && response.status !== 204) throw new Error(await readProblemDetail$1(response) ?? "Failed to revoke API key.");
|
|
767
|
-
},
|
|
768
|
-
onSuccess: async () => {
|
|
769
|
-
await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
|
|
770
|
-
}
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
async function readProblemDetail$1(response) {
|
|
774
|
-
try {
|
|
775
|
-
return (await response.json()).detail?.trim() || null;
|
|
776
|
-
} catch {
|
|
777
|
-
return null;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
//#endregion
|
|
782
|
-
//#region src/hooks/useUsage.ts
|
|
783
|
-
function createDemoUsage(range) {
|
|
784
|
-
const now = /* @__PURE__ */ new Date();
|
|
785
|
-
const days = range === "24h" ? 1 : range === "7d" ? 7 : range === "90d" ? 90 : range === "mtd" ? Math.max(1, now.getUTCDate()) : 30;
|
|
786
|
-
const series = [];
|
|
787
|
-
let total = 0;
|
|
788
|
-
const today = /* @__PURE__ */ new Date();
|
|
789
|
-
for (let i = days - 1; i >= 0; i--) {
|
|
790
|
-
const day = new Date(today);
|
|
791
|
-
day.setUTCDate(day.getUTCDate() - i);
|
|
792
|
-
const requestCount = 40 + i * 17 % 80;
|
|
793
|
-
total += requestCount;
|
|
794
|
-
series.push({
|
|
795
|
-
dayUtc: day.toISOString().slice(0, 10),
|
|
796
|
-
requestCount,
|
|
797
|
-
errorCount: Math.floor(requestCount * .02),
|
|
798
|
-
avgLatencyMs: 40 + i % 20
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
return {
|
|
802
|
-
range,
|
|
803
|
-
fromDayUtc: series[0]?.dayUtc ?? today.toISOString().slice(0, 10),
|
|
804
|
-
toDayUtc: series.at(-1)?.dayUtc ?? today.toISOString().slice(0, 10),
|
|
805
|
-
requestCount: total,
|
|
806
|
-
avgLatencyMs: 48,
|
|
807
|
-
errorRate: .018,
|
|
808
|
-
series,
|
|
809
|
-
byEndpoint: [
|
|
810
|
-
{
|
|
811
|
-
path: "GET /pets",
|
|
812
|
-
count: Math.floor(total * .45)
|
|
813
|
-
},
|
|
814
|
-
{
|
|
815
|
-
path: "POST /pets",
|
|
816
|
-
count: Math.floor(total * .25)
|
|
817
|
-
},
|
|
818
|
-
{
|
|
819
|
-
path: "GET /pets/{id}",
|
|
820
|
-
count: Math.floor(total * .2)
|
|
821
|
-
},
|
|
822
|
-
{
|
|
823
|
-
path: "DELETE /pets/{id}",
|
|
824
|
-
count: Math.floor(total * .1)
|
|
825
|
-
}
|
|
826
|
-
],
|
|
827
|
-
byStatus: [
|
|
828
|
-
{
|
|
829
|
-
label: "2xx",
|
|
830
|
-
count: Math.floor(total * .96)
|
|
831
|
-
},
|
|
832
|
-
{
|
|
833
|
-
label: "4xx",
|
|
834
|
-
count: Math.floor(total * .03)
|
|
835
|
-
},
|
|
836
|
-
{
|
|
837
|
-
label: "5xx",
|
|
838
|
-
count: Math.floor(total * .01)
|
|
839
|
-
}
|
|
840
|
-
]
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
function createDemoRecent() {
|
|
844
|
-
const now = Date.now();
|
|
845
|
-
return { items: [
|
|
846
|
-
{
|
|
847
|
-
occurredAtUtc: (/* @__PURE__ */ new Date(now - 6e4)).toISOString(),
|
|
848
|
-
method: "GET",
|
|
849
|
-
path: "/pets",
|
|
850
|
-
statusCode: 200,
|
|
851
|
-
latencyMs: 42
|
|
852
|
-
},
|
|
853
|
-
{
|
|
854
|
-
occurredAtUtc: (/* @__PURE__ */ new Date(now - 12e4)).toISOString(),
|
|
855
|
-
method: "POST",
|
|
856
|
-
path: "/pets",
|
|
857
|
-
statusCode: 201,
|
|
858
|
-
latencyMs: 88
|
|
859
|
-
},
|
|
860
|
-
{
|
|
861
|
-
occurredAtUtc: (/* @__PURE__ */ new Date(now - 18e4)).toISOString(),
|
|
862
|
-
method: "GET",
|
|
863
|
-
path: "/pets/1",
|
|
864
|
-
statusCode: 404,
|
|
865
|
-
latencyMs: 31
|
|
866
|
-
}
|
|
867
|
-
] };
|
|
868
|
-
}
|
|
869
|
-
function useUsage(range = "30d") {
|
|
870
|
-
const auth = useAuth();
|
|
871
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
872
|
-
const isPreview = useDevPortalPreviewMode();
|
|
873
|
-
const isDemoSession = useIsDemoSession();
|
|
874
|
-
const liveQuery = useQuery({
|
|
875
|
-
queryKey: [
|
|
876
|
-
"dev-portal-usage",
|
|
877
|
-
apiBaseUrl,
|
|
878
|
-
range
|
|
879
|
-
],
|
|
880
|
-
enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
|
|
881
|
-
queryFn: async () => {
|
|
882
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/usage?range=${encodeURIComponent(range)}`, {
|
|
883
|
-
method: "GET",
|
|
884
|
-
headers: { Accept: "application/json" },
|
|
885
|
-
credentials: "include"
|
|
886
|
-
});
|
|
887
|
-
if (!response.ok) throw new Error("Failed to load usage.");
|
|
888
|
-
return await response.json();
|
|
889
|
-
}
|
|
890
|
-
});
|
|
891
|
-
if (isDemoSession && auth.isAuthenticated) return {
|
|
892
|
-
...liveQuery,
|
|
893
|
-
data: createDemoUsage(range),
|
|
894
|
-
error: null,
|
|
895
|
-
isError: false,
|
|
896
|
-
isFetching: false,
|
|
897
|
-
isLoading: false,
|
|
898
|
-
isPending: false,
|
|
899
|
-
isSuccess: true,
|
|
900
|
-
status: "success"
|
|
901
|
-
};
|
|
902
|
-
return liveQuery;
|
|
903
|
-
}
|
|
904
|
-
function useUsageRecent(limit = 20) {
|
|
905
|
-
const auth = useAuth();
|
|
906
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
907
|
-
const isPreview = useDevPortalPreviewMode();
|
|
908
|
-
const isDemoSession = useIsDemoSession();
|
|
909
|
-
const liveQuery = useQuery({
|
|
910
|
-
queryKey: [
|
|
911
|
-
"dev-portal-usage-recent",
|
|
912
|
-
apiBaseUrl,
|
|
913
|
-
limit
|
|
914
|
-
],
|
|
915
|
-
enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
|
|
916
|
-
queryFn: async () => {
|
|
917
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/usage/recent?limit=${limit}`, {
|
|
918
|
-
method: "GET",
|
|
919
|
-
headers: { Accept: "application/json" },
|
|
920
|
-
credentials: "include"
|
|
921
|
-
});
|
|
922
|
-
if (!response.ok) throw new Error("Failed to load recent requests.");
|
|
923
|
-
return await response.json();
|
|
924
|
-
}
|
|
925
|
-
});
|
|
926
|
-
if (isDemoSession && auth.isAuthenticated) return {
|
|
927
|
-
...liveQuery,
|
|
928
|
-
data: createDemoRecent(),
|
|
929
|
-
error: null,
|
|
930
|
-
isError: false,
|
|
931
|
-
isFetching: false,
|
|
932
|
-
isLoading: false,
|
|
933
|
-
isPending: false,
|
|
934
|
-
isSuccess: true,
|
|
935
|
-
status: "success"
|
|
936
|
-
};
|
|
937
|
-
return liveQuery;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
//#endregion
|
|
941
22
|
//#region src/pages/OverviewDashboardPage.tsx
|
|
942
23
|
function StatCard({ label, value, sub, subClassName }) {
|
|
943
24
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -958,7 +39,7 @@ function StatCard({ label, value, sub, subClassName }) {
|
|
|
958
39
|
]
|
|
959
40
|
});
|
|
960
41
|
}
|
|
961
|
-
function formatNumber
|
|
42
|
+
function formatNumber(value) {
|
|
962
43
|
return new Intl.NumberFormat().format(value);
|
|
963
44
|
}
|
|
964
45
|
const OverviewDashboardPage = () => {
|
|
@@ -992,8 +73,8 @@ const OverviewDashboardPage = () => {
|
|
|
992
73
|
children: [
|
|
993
74
|
/* @__PURE__ */ jsx(StatCard, {
|
|
994
75
|
label: "Requests (30d)",
|
|
995
|
-
value: usage ? formatNumber
|
|
996
|
-
sub: usage ? `${formatNumber
|
|
76
|
+
value: usage ? formatNumber(usage.requestCount) : statsLoading ? "…" : "—",
|
|
77
|
+
sub: usage ? `${formatNumber(usage.series.reduce((sum, p) => sum + p.errorCount, 0))} errors` : "No usage data yet"
|
|
997
78
|
}),
|
|
998
79
|
/* @__PURE__ */ jsx(StatCard, {
|
|
999
80
|
label: "Active keys",
|
|
@@ -1157,181 +238,6 @@ const OverviewDashboardPage = () => {
|
|
|
1157
238
|
});
|
|
1158
239
|
};
|
|
1159
240
|
|
|
1160
|
-
//#endregion
|
|
1161
|
-
//#region src/components/ApiKeysPanel.tsx
|
|
1162
|
-
/**
|
|
1163
|
-
* Lists gateway API keys for one plan with create and revoke. The plaintext key is returned by the
|
|
1164
|
-
* platform exactly once, so it is held in local state until dismissed and never re-fetched.
|
|
1165
|
-
*/
|
|
1166
|
-
const ApiKeysPanel = ({ plan, header, readOnly = false }) => {
|
|
1167
|
-
const createKey = useCreateApiKey();
|
|
1168
|
-
const revokeKey = useRevokeApiKey();
|
|
1169
|
-
const [name, setName] = useState("");
|
|
1170
|
-
const [newKey, setNewKey] = useState(null);
|
|
1171
|
-
const [openRevokeId, setOpenRevokeId] = useState(null);
|
|
1172
|
-
const [pendingRevokeId, setPendingRevokeId] = useState(null);
|
|
1173
|
-
const activeKeys = plan.keys.filter((key) => key.status === "active");
|
|
1174
|
-
const planActiveCount = activeKeys.length;
|
|
1175
|
-
const capLabel = plan.maxKeys == null ? `${plan.activeCount} active` : `${plan.activeCount} of ${plan.maxKeys} used`;
|
|
1176
|
-
const canCreate = !readOnly && plan.canCreate;
|
|
1177
|
-
const handleCreate = () => {
|
|
1178
|
-
const trimmed = name.trim();
|
|
1179
|
-
if (!trimmed) return;
|
|
1180
|
-
createKey.mutate({
|
|
1181
|
-
name: trimmed,
|
|
1182
|
-
planSku: plan.planSku
|
|
1183
|
-
}, { onSuccess: (key) => {
|
|
1184
|
-
setNewKey(key);
|
|
1185
|
-
setName("");
|
|
1186
|
-
} });
|
|
1187
|
-
};
|
|
1188
|
-
const handleRevoke = (keyId) => {
|
|
1189
|
-
setPendingRevokeId(keyId);
|
|
1190
|
-
revokeKey.mutate(keyId, { onSettled: () => {
|
|
1191
|
-
setPendingRevokeId(null);
|
|
1192
|
-
setOpenRevokeId(null);
|
|
1193
|
-
} });
|
|
1194
|
-
};
|
|
1195
|
-
return /* @__PURE__ */ jsxs("article", {
|
|
1196
|
-
className: "rounded-xl border bg-card/60 p-6 space-y-5",
|
|
1197
|
-
children: [
|
|
1198
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1199
|
-
className: "flex flex-wrap items-start justify-between gap-3",
|
|
1200
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1201
|
-
className: "space-y-1",
|
|
1202
|
-
children: [/* @__PURE__ */ jsxs("h2", {
|
|
1203
|
-
className: "text-lg font-medium",
|
|
1204
|
-
children: [plan.displayName, " API keys"]
|
|
1205
|
-
}), /* @__PURE__ */ jsxs("p", {
|
|
1206
|
-
className: "text-sm text-muted-foreground",
|
|
1207
|
-
children: [
|
|
1208
|
-
"Send a key in the",
|
|
1209
|
-
" ",
|
|
1210
|
-
/* @__PURE__ */ jsx("code", {
|
|
1211
|
-
className: "rounded bg-muted px-1 py-0.5 text-xs",
|
|
1212
|
-
children: header
|
|
1213
|
-
}),
|
|
1214
|
-
" ",
|
|
1215
|
-
"header when calling this plan's endpoints. Keys share this plan's rate limit and key cap."
|
|
1216
|
-
]
|
|
1217
|
-
})]
|
|
1218
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
1219
|
-
className: "shrink-0 rounded-full border px-3 py-1 text-xs text-muted-foreground",
|
|
1220
|
-
children: capLabel
|
|
1221
|
-
})]
|
|
1222
|
-
}),
|
|
1223
|
-
newKey?.key ? /* @__PURE__ */ jsxs(Alert, { children: [
|
|
1224
|
-
/* @__PURE__ */ jsx(KeyRoundIcon, { size: 16 }),
|
|
1225
|
-
/* @__PURE__ */ jsx(AlertTitle, { children: "Copy your new key now — it is shown only once." }),
|
|
1226
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1227
|
-
className: "mt-3 space-y-3",
|
|
1228
|
-
children: [/* @__PURE__ */ jsx(Secret, {
|
|
1229
|
-
className: "max-w-full w-full",
|
|
1230
|
-
secret: newKey.key,
|
|
1231
|
-
status: "active"
|
|
1232
|
-
}), /* @__PURE__ */ jsx(Button$1, {
|
|
1233
|
-
variant: "outline",
|
|
1234
|
-
onClick: () => setNewKey(null),
|
|
1235
|
-
children: "Done"
|
|
1236
|
-
})]
|
|
1237
|
-
})
|
|
1238
|
-
] }) : null,
|
|
1239
|
-
createKey.isError ? /* @__PURE__ */ jsx(Alert, {
|
|
1240
|
-
variant: "destructive",
|
|
1241
|
-
children: /* @__PURE__ */ jsx(AlertTitle, { children: createKey.error.message === API_KEY_CAP_REACHED ? plan.maxKeys == null ? "You cannot create more API keys right now." : `You already have the maximum of ${plan.maxKeys} active API keys. Revoke one to create another.` : createKey.error.message })
|
|
1242
|
-
}) : null,
|
|
1243
|
-
revokeKey.isError ? /* @__PURE__ */ jsx(Alert, {
|
|
1244
|
-
variant: "destructive",
|
|
1245
|
-
children: /* @__PURE__ */ jsx(AlertTitle, { children: revokeKey.error.message })
|
|
1246
|
-
}) : null,
|
|
1247
|
-
planActiveCount === 0 ? /* @__PURE__ */ jsx("p", {
|
|
1248
|
-
className: "text-sm text-muted-foreground",
|
|
1249
|
-
children: "You have no active API keys for this plan yet."
|
|
1250
|
-
}) : null,
|
|
1251
|
-
activeKeys.length > 0 ? /* @__PURE__ */ jsx("ul", {
|
|
1252
|
-
className: "divide-y rounded-lg border",
|
|
1253
|
-
children: activeKeys.map((key) => {
|
|
1254
|
-
const isRevoking = pendingRevokeId === key.id;
|
|
1255
|
-
return /* @__PURE__ */ jsxs("li", {
|
|
1256
|
-
className: "flex flex-wrap items-center justify-between gap-3 p-4",
|
|
1257
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1258
|
-
className: "min-w-0 space-y-1",
|
|
1259
|
-
children: [/* @__PURE__ */ jsx("p", {
|
|
1260
|
-
className: "truncate font-medium",
|
|
1261
|
-
children: key.name
|
|
1262
|
-
}), /* @__PURE__ */ jsxs("p", {
|
|
1263
|
-
className: "text-xs text-muted-foreground",
|
|
1264
|
-
children: [
|
|
1265
|
-
/* @__PURE__ */ jsxs("code", { children: [key.keyPrefix, "…"] }),
|
|
1266
|
-
" · created",
|
|
1267
|
-
" ",
|
|
1268
|
-
new Date(key.createdAtUtc).toLocaleDateString()
|
|
1269
|
-
]
|
|
1270
|
-
})]
|
|
1271
|
-
}), readOnly ? null : /* @__PURE__ */ jsxs(Dialog, {
|
|
1272
|
-
open: openRevokeId === key.id,
|
|
1273
|
-
onOpenChange: (open) => {
|
|
1274
|
-
if (isRevoking) return;
|
|
1275
|
-
setOpenRevokeId(open ? key.id : null);
|
|
1276
|
-
},
|
|
1277
|
-
children: [/* @__PURE__ */ jsx(DialogTrigger, {
|
|
1278
|
-
asChild: true,
|
|
1279
|
-
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
1280
|
-
variant: "ghost",
|
|
1281
|
-
className: "flex items-center gap-2",
|
|
1282
|
-
disabled: isRevoking,
|
|
1283
|
-
children: [isRevoking ? /* @__PURE__ */ jsx(Spinner, { size: 14 }) : /* @__PURE__ */ jsx(TrashIcon, { size: 16 }), /* @__PURE__ */ jsx("span", {
|
|
1284
|
-
className: "hidden md:block",
|
|
1285
|
-
children: isRevoking ? "Revoking…" : "Revoke"
|
|
1286
|
-
})]
|
|
1287
|
-
})
|
|
1288
|
-
}), /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, { children: [
|
|
1289
|
-
"Revoke “",
|
|
1290
|
-
key.name,
|
|
1291
|
-
"”?"
|
|
1292
|
-
] }), /* @__PURE__ */ jsx(DialogDescription, { children: "Any application using this key will immediately start getting 401 responses from the gateway. This cannot be undone." })] }), /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button$1, {
|
|
1293
|
-
variant: "outline",
|
|
1294
|
-
disabled: isRevoking,
|
|
1295
|
-
onClick: () => setOpenRevokeId(null),
|
|
1296
|
-
children: "Cancel"
|
|
1297
|
-
}), /* @__PURE__ */ jsx(Button$1, {
|
|
1298
|
-
variant: "destructive",
|
|
1299
|
-
disabled: isRevoking,
|
|
1300
|
-
onClick: () => handleRevoke(key.id),
|
|
1301
|
-
children: isRevoking ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { size: 14 }), "Revoking…"] }) : "Revoke key"
|
|
1302
|
-
})] })] })]
|
|
1303
|
-
})]
|
|
1304
|
-
}, key.id);
|
|
1305
|
-
})
|
|
1306
|
-
}) : null,
|
|
1307
|
-
readOnly ? null : /* @__PURE__ */ jsxs("div", {
|
|
1308
|
-
className: "flex flex-wrap items-center gap-2",
|
|
1309
|
-
children: [/* @__PURE__ */ jsx(Input, {
|
|
1310
|
-
className: "max-w-xs",
|
|
1311
|
-
maxLength: 64,
|
|
1312
|
-
placeholder: "Key name (e.g. production)",
|
|
1313
|
-
"aria-label": `New API key name for ${plan.displayName}`,
|
|
1314
|
-
value: name,
|
|
1315
|
-
disabled: !canCreate || createKey.isPending,
|
|
1316
|
-
onChange: (event) => setName(event.target.value),
|
|
1317
|
-
onKeyDown: (event) => {
|
|
1318
|
-
if (event.key === "Enter") handleCreate();
|
|
1319
|
-
}
|
|
1320
|
-
}), /* @__PURE__ */ jsx(Button$1, {
|
|
1321
|
-
onClick: handleCreate,
|
|
1322
|
-
disabled: !canCreate || createKey.isPending || !name.trim(),
|
|
1323
|
-
className: "flex items-center gap-2",
|
|
1324
|
-
children: createKey.isPending ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { size: 14 }), "Creating…"] }) : "Create API key"
|
|
1325
|
-
})]
|
|
1326
|
-
}),
|
|
1327
|
-
plan && !plan.canCreate && !readOnly ? /* @__PURE__ */ jsx("p", {
|
|
1328
|
-
className: "text-sm text-muted-foreground",
|
|
1329
|
-
children: plan.maxKeys == null ? "API key creation is currently disabled for this API." : `You have reached this plan's limit of ${plan.maxKeys} API keys. Revoke a key to create a new one.`
|
|
1330
|
-
}) : null
|
|
1331
|
-
]
|
|
1332
|
-
});
|
|
1333
|
-
};
|
|
1334
|
-
|
|
1335
241
|
//#endregion
|
|
1336
242
|
//#region src/DevPortalBillingWrapper.tsx
|
|
1337
243
|
const DevPortalBillingWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(DevPortalBillingProvider, {
|
|
@@ -1339,1182 +245,6 @@ const DevPortalBillingWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(DevPor
|
|
|
1339
245
|
children: /* @__PURE__ */ jsx(Outlet, {})
|
|
1340
246
|
});
|
|
1341
247
|
|
|
1342
|
-
//#endregion
|
|
1343
|
-
//#region src/components/RedirectPage.tsx
|
|
1344
|
-
const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
|
|
1345
|
-
useEffect(() => {
|
|
1346
|
-
if (url) window.location.assign(url);
|
|
1347
|
-
}, [url]);
|
|
1348
|
-
return /* @__PURE__ */ jsx("div", {
|
|
1349
|
-
className: "flex min-h-screen items-center justify-center bg-muted",
|
|
1350
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
1351
|
-
className: "flex max-w-md flex-col items-center space-y-6 text-center",
|
|
1352
|
-
children: [
|
|
1353
|
-
/* @__PURE__ */ jsx("div", {
|
|
1354
|
-
className: "relative",
|
|
1355
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
1356
|
-
className: "flex h-24 w-24 items-center justify-center rounded-full bg-foreground/10",
|
|
1357
|
-
children: /* @__PURE__ */ jsx(Icon, { className: "h-12 w-12 text-foreground" })
|
|
1358
|
-
})
|
|
1359
|
-
}),
|
|
1360
|
-
children,
|
|
1361
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1362
|
-
className: "space-y-2",
|
|
1363
|
-
children: [/* @__PURE__ */ jsx("h2", {
|
|
1364
|
-
className: "text-2xl font-bold text-card-foreground",
|
|
1365
|
-
children: title
|
|
1366
|
-
}), /* @__PURE__ */ jsx("p", {
|
|
1367
|
-
className: "text-muted-foreground",
|
|
1368
|
-
children: description
|
|
1369
|
-
})]
|
|
1370
|
-
}),
|
|
1371
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1372
|
-
className: "flex space-x-2",
|
|
1373
|
-
children: [
|
|
1374
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.3s]" }),
|
|
1375
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.15s]" }),
|
|
1376
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary" })
|
|
1377
|
-
]
|
|
1378
|
-
})
|
|
1379
|
-
]
|
|
1380
|
-
})
|
|
1381
|
-
});
|
|
1382
|
-
};
|
|
1383
|
-
|
|
1384
|
-
//#endregion
|
|
1385
|
-
//#region src/pages/CheckoutPage.tsx
|
|
1386
|
-
const CheckoutPage = () => {
|
|
1387
|
-
const [searchParams] = useSearchParams();
|
|
1388
|
-
const planSku = searchParams.get("planSku") ?? "";
|
|
1389
|
-
const navigate = useNavigate();
|
|
1390
|
-
const auth = useAuth();
|
|
1391
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
1392
|
-
const isPreview = useDevPortalPreviewMode();
|
|
1393
|
-
const isDemoSession = useIsDemoSession();
|
|
1394
|
-
const plansPath = auth.isAuthenticated ? ACCOUNT_PLANS_PATH : PRICING_PATH;
|
|
1395
|
-
const checkoutQuery = useQuery({
|
|
1396
|
-
queryKey: [
|
|
1397
|
-
"dev-portal-checkout",
|
|
1398
|
-
apiBaseUrl,
|
|
1399
|
-
planSku,
|
|
1400
|
-
auth.profile?.sub
|
|
1401
|
-
],
|
|
1402
|
-
enabled: Boolean(planSku) && !isPreview && !isDemoSession && auth.isAuthenticated && Boolean(apiBaseUrl),
|
|
1403
|
-
retry: false,
|
|
1404
|
-
queryFn: async () => {
|
|
1405
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/checkout`, {
|
|
1406
|
-
method: "POST",
|
|
1407
|
-
headers: {
|
|
1408
|
-
Accept: "application/json",
|
|
1409
|
-
"Content-Type": "application/json"
|
|
1410
|
-
},
|
|
1411
|
-
credentials: "include",
|
|
1412
|
-
body: JSON.stringify({ planSku })
|
|
1413
|
-
});
|
|
1414
|
-
if (!response.ok) {
|
|
1415
|
-
const detail = await response.text();
|
|
1416
|
-
throw new Error(detail || "Checkout failed.");
|
|
1417
|
-
}
|
|
1418
|
-
return await response.json();
|
|
1419
|
-
}
|
|
1420
|
-
});
|
|
1421
|
-
const freeProvisioned = checkoutQuery.data?.checkoutUrl == null && checkoutQuery.data?.entitlementProvisioned === true;
|
|
1422
|
-
useEffect(() => {
|
|
1423
|
-
if (freeProvisioned) navigate("/account/subscriptions?checkout=success", { replace: true });
|
|
1424
|
-
}, [freeProvisioned, navigate]);
|
|
1425
|
-
if (!planSku) return /* @__PURE__ */ jsxs("div", {
|
|
1426
|
-
className: "mx-auto max-w-lg px-4 py-16 text-center text-muted-foreground",
|
|
1427
|
-
children: [
|
|
1428
|
-
"Missing plan SKU.",
|
|
1429
|
-
" ",
|
|
1430
|
-
/* @__PURE__ */ jsx(Link$1, {
|
|
1431
|
-
to: plansPath,
|
|
1432
|
-
className: "text-primary underline",
|
|
1433
|
-
children: "Back to plans"
|
|
1434
|
-
})
|
|
1435
|
-
]
|
|
1436
|
-
});
|
|
1437
|
-
if (isDemoSession) return /* @__PURE__ */ jsxs("div", {
|
|
1438
|
-
className: "mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center",
|
|
1439
|
-
children: [
|
|
1440
|
-
/* @__PURE__ */ jsx("h1", {
|
|
1441
|
-
className: "text-2xl font-semibold tracking-tight",
|
|
1442
|
-
children: "Demo checkout"
|
|
1443
|
-
}),
|
|
1444
|
-
/* @__PURE__ */ jsx("p", {
|
|
1445
|
-
className: "text-muted-foreground",
|
|
1446
|
-
children: "Checkout is simulated in local demo mode. Publish your portal to API ToGo to complete real subscriptions and billing."
|
|
1447
|
-
}),
|
|
1448
|
-
/* @__PURE__ */ jsx(Button$2, {
|
|
1449
|
-
asChild: true,
|
|
1450
|
-
variant: "outline",
|
|
1451
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1452
|
-
to: "/account/subscriptions",
|
|
1453
|
-
children: "View demo subscriptions"
|
|
1454
|
-
})
|
|
1455
|
-
})
|
|
1456
|
-
]
|
|
1457
|
-
});
|
|
1458
|
-
if (isPreview) return /* @__PURE__ */ jsxs("div", {
|
|
1459
|
-
className: "mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center",
|
|
1460
|
-
children: [
|
|
1461
|
-
/* @__PURE__ */ jsx("h1", {
|
|
1462
|
-
className: "text-2xl font-semibold tracking-tight",
|
|
1463
|
-
children: "Checkout is temporarily unavailable"
|
|
1464
|
-
}),
|
|
1465
|
-
/* @__PURE__ */ jsx("p", {
|
|
1466
|
-
className: "text-muted-foreground",
|
|
1467
|
-
children: "We couldn’t reach the developer portal API. Try again shortly, or contact the site owner if this continues."
|
|
1468
|
-
}),
|
|
1469
|
-
/* @__PURE__ */ jsx(Button$2, {
|
|
1470
|
-
asChild: true,
|
|
1471
|
-
variant: "outline",
|
|
1472
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1473
|
-
to: plansPath,
|
|
1474
|
-
children: "Back to plans"
|
|
1475
|
-
})
|
|
1476
|
-
})
|
|
1477
|
-
]
|
|
1478
|
-
});
|
|
1479
|
-
if (freeProvisioned) return null;
|
|
1480
|
-
return /* @__PURE__ */ jsx(RedirectPage, {
|
|
1481
|
-
icon: ShieldIcon,
|
|
1482
|
-
title: "Establishing secure checkout…",
|
|
1483
|
-
description: "Redirecting you to complete your subscription.",
|
|
1484
|
-
url: checkoutQuery.data?.checkoutUrl ?? void 0,
|
|
1485
|
-
children: checkoutQuery.isError ? /* @__PURE__ */ jsxs(Alert$1, {
|
|
1486
|
-
variant: "destructive",
|
|
1487
|
-
children: [/* @__PURE__ */ jsx(AlertDescription, {
|
|
1488
|
-
className: "first-letter:uppercase",
|
|
1489
|
-
children: checkoutQuery.error.message
|
|
1490
|
-
}), /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(Button$2, {
|
|
1491
|
-
variant: "outline",
|
|
1492
|
-
size: "xs",
|
|
1493
|
-
asChild: true,
|
|
1494
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1495
|
-
to: plansPath,
|
|
1496
|
-
children: "Back to plans"
|
|
1497
|
-
})
|
|
1498
|
-
}) })]
|
|
1499
|
-
}) : null
|
|
1500
|
-
});
|
|
1501
|
-
};
|
|
1502
|
-
|
|
1503
|
-
//#endregion
|
|
1504
|
-
//#region src/hooks/usePlanInquiry.ts
|
|
1505
|
-
const PLAN_INQUIRY_RATE_LIMITED = "Too many messages were sent from your network. Please try again later.";
|
|
1506
|
-
function useSubmitPlanInquiry() {
|
|
1507
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
1508
|
-
return useMutation({ mutationFn: async ({ planSku, inquiry }) => {
|
|
1509
|
-
if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
|
|
1510
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/plans/${encodeURIComponent(planSku)}/inquiries`, {
|
|
1511
|
-
method: "POST",
|
|
1512
|
-
headers: {
|
|
1513
|
-
Accept: "application/json",
|
|
1514
|
-
"Content-Type": "application/json"
|
|
1515
|
-
},
|
|
1516
|
-
credentials: "include",
|
|
1517
|
-
body: JSON.stringify(inquiry)
|
|
1518
|
-
});
|
|
1519
|
-
if (response.status === 429) throw new Error(PLAN_INQUIRY_RATE_LIMITED);
|
|
1520
|
-
if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Your message could not be sent. Please try again later.");
|
|
1521
|
-
} });
|
|
1522
|
-
}
|
|
1523
|
-
async function readProblemDetail(response) {
|
|
1524
|
-
try {
|
|
1525
|
-
const body = await response.json();
|
|
1526
|
-
return (body.errors ? Object.values(body.errors).flat()[0] : void 0) ?? body.detail ?? body.title ?? null;
|
|
1527
|
-
} catch {
|
|
1528
|
-
return null;
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
//#endregion
|
|
1533
|
-
//#region src/pages/ContactSalesPage.tsx
|
|
1534
|
-
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1535
|
-
const validate = (form) => ({
|
|
1536
|
-
...form.name.trim() ? {} : { name: "Enter your name." },
|
|
1537
|
-
...EMAIL_PATTERN.test(form.email.trim()) ? {} : { email: "Enter a valid email address." },
|
|
1538
|
-
...form.message.trim() ? {} : { message: "Tell us what you need." }
|
|
1539
|
-
});
|
|
1540
|
-
const ContactSalesPage = () => {
|
|
1541
|
-
const [searchParams] = useSearchParams();
|
|
1542
|
-
const planSku = searchParams.get("planSku") ?? "";
|
|
1543
|
-
const auth = useAuth();
|
|
1544
|
-
const isPreview = useDevPortalPreviewMode();
|
|
1545
|
-
const isDemoSession = useIsDemoSession();
|
|
1546
|
-
const plans = usePlans();
|
|
1547
|
-
const submit = useSubmitPlanInquiry();
|
|
1548
|
-
const [form, setForm] = useState(() => ({
|
|
1549
|
-
name: auth.profile?.name ?? "",
|
|
1550
|
-
email: auth.profile?.email ?? "",
|
|
1551
|
-
company: "",
|
|
1552
|
-
message: "",
|
|
1553
|
-
website: ""
|
|
1554
|
-
}));
|
|
1555
|
-
const [errors, setErrors] = useState({});
|
|
1556
|
-
const plan = plans.data?.plans.find((p) => p.sku === planSku);
|
|
1557
|
-
const isSimulated = isPreview || isDemoSession;
|
|
1558
|
-
if (!planSku || plans.isSuccess && (!plan || !isPlanCustomPriced(plan))) return /* @__PURE__ */ jsxs("div", {
|
|
1559
|
-
className: "mx-auto max-w-lg px-4 py-16 text-center text-muted-foreground",
|
|
1560
|
-
children: [
|
|
1561
|
-
"This plan is not available for sales inquiries.",
|
|
1562
|
-
" ",
|
|
1563
|
-
/* @__PURE__ */ jsx(Link$1, {
|
|
1564
|
-
to: PRICING_PATH,
|
|
1565
|
-
className: "text-primary underline",
|
|
1566
|
-
children: "Back to pricing"
|
|
1567
|
-
})
|
|
1568
|
-
]
|
|
1569
|
-
});
|
|
1570
|
-
if (submit.isSuccess) return /* @__PURE__ */ jsxs("div", {
|
|
1571
|
-
className: "mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center",
|
|
1572
|
-
children: [
|
|
1573
|
-
/* @__PURE__ */ jsx("h1", {
|
|
1574
|
-
className: "text-2xl font-semibold tracking-tight",
|
|
1575
|
-
children: "Thanks, we got your message"
|
|
1576
|
-
}),
|
|
1577
|
-
/* @__PURE__ */ jsxs("p", {
|
|
1578
|
-
className: "text-muted-foreground",
|
|
1579
|
-
children: [
|
|
1580
|
-
"The team will reply to ",
|
|
1581
|
-
form.email.trim(),
|
|
1582
|
-
" soon."
|
|
1583
|
-
]
|
|
1584
|
-
}),
|
|
1585
|
-
/* @__PURE__ */ jsx(Button$2, {
|
|
1586
|
-
asChild: true,
|
|
1587
|
-
variant: "outline",
|
|
1588
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1589
|
-
to: PRICING_PATH,
|
|
1590
|
-
children: "Back to pricing"
|
|
1591
|
-
})
|
|
1592
|
-
})
|
|
1593
|
-
]
|
|
1594
|
-
});
|
|
1595
|
-
const update = (field) => (event) => setForm((prev) => ({
|
|
1596
|
-
...prev,
|
|
1597
|
-
[field]: event.target.value
|
|
1598
|
-
}));
|
|
1599
|
-
const handleSubmit = (event) => {
|
|
1600
|
-
event.preventDefault();
|
|
1601
|
-
const nextErrors = validate(form);
|
|
1602
|
-
setErrors(nextErrors);
|
|
1603
|
-
if (Object.keys(nextErrors).length > 0 || isSimulated) return;
|
|
1604
|
-
submit.mutate({
|
|
1605
|
-
planSku,
|
|
1606
|
-
inquiry: {
|
|
1607
|
-
name: form.name.trim(),
|
|
1608
|
-
email: form.email.trim(),
|
|
1609
|
-
company: form.company.trim() || void 0,
|
|
1610
|
-
message: form.message.trim(),
|
|
1611
|
-
website: form.website || void 0
|
|
1612
|
-
}
|
|
1613
|
-
});
|
|
1614
|
-
};
|
|
1615
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1616
|
-
className: "mx-auto w-full max-w-lg px-4 py-12",
|
|
1617
|
-
children: [
|
|
1618
|
-
/* @__PURE__ */ jsx("h1", {
|
|
1619
|
-
className: "text-2xl font-semibold tracking-tight",
|
|
1620
|
-
children: "Contact sales"
|
|
1621
|
-
}),
|
|
1622
|
-
/* @__PURE__ */ jsx("p", {
|
|
1623
|
-
className: "mt-2 text-muted-foreground",
|
|
1624
|
-
children: plan ? `Tell us about your needs for the ${plan.displayName} plan and we'll get back to you with pricing.` : "Tell us about your needs and we'll get back to you with pricing."
|
|
1625
|
-
}),
|
|
1626
|
-
isSimulated ? /* @__PURE__ */ jsxs(Alert$1, {
|
|
1627
|
-
className: "mt-6",
|
|
1628
|
-
children: [/* @__PURE__ */ jsx(AlertTitle$1, { children: "Preview only" }), /* @__PURE__ */ jsx(AlertDescription, { children: "Messages are sent once your portal is published to API ToGo." })]
|
|
1629
|
-
}) : null,
|
|
1630
|
-
/* @__PURE__ */ jsxs("form", {
|
|
1631
|
-
className: "mt-6 space-y-4",
|
|
1632
|
-
noValidate: true,
|
|
1633
|
-
onSubmit: handleSubmit,
|
|
1634
|
-
children: [
|
|
1635
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1636
|
-
className: "space-y-1.5",
|
|
1637
|
-
children: [
|
|
1638
|
-
/* @__PURE__ */ jsx(Label, {
|
|
1639
|
-
htmlFor: "contact-name",
|
|
1640
|
-
children: "Name"
|
|
1641
|
-
}),
|
|
1642
|
-
/* @__PURE__ */ jsx(Input$1, {
|
|
1643
|
-
id: "contact-name",
|
|
1644
|
-
autoComplete: "name",
|
|
1645
|
-
maxLength: 200,
|
|
1646
|
-
value: form.name,
|
|
1647
|
-
onChange: update("name"),
|
|
1648
|
-
"aria-invalid": Boolean(errors.name)
|
|
1649
|
-
}),
|
|
1650
|
-
errors.name ? /* @__PURE__ */ jsx("p", {
|
|
1651
|
-
className: "text-sm text-destructive",
|
|
1652
|
-
children: errors.name
|
|
1653
|
-
}) : null
|
|
1654
|
-
]
|
|
1655
|
-
}),
|
|
1656
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1657
|
-
className: "space-y-1.5",
|
|
1658
|
-
children: [
|
|
1659
|
-
/* @__PURE__ */ jsx(Label, {
|
|
1660
|
-
htmlFor: "contact-email",
|
|
1661
|
-
children: "Work email"
|
|
1662
|
-
}),
|
|
1663
|
-
/* @__PURE__ */ jsx(Input$1, {
|
|
1664
|
-
id: "contact-email",
|
|
1665
|
-
type: "email",
|
|
1666
|
-
autoComplete: "email",
|
|
1667
|
-
maxLength: 320,
|
|
1668
|
-
value: form.email,
|
|
1669
|
-
onChange: update("email"),
|
|
1670
|
-
"aria-invalid": Boolean(errors.email)
|
|
1671
|
-
}),
|
|
1672
|
-
errors.email ? /* @__PURE__ */ jsx("p", {
|
|
1673
|
-
className: "text-sm text-destructive",
|
|
1674
|
-
children: errors.email
|
|
1675
|
-
}) : null
|
|
1676
|
-
]
|
|
1677
|
-
}),
|
|
1678
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1679
|
-
className: "space-y-1.5",
|
|
1680
|
-
children: [/* @__PURE__ */ jsx(Label, {
|
|
1681
|
-
htmlFor: "contact-company",
|
|
1682
|
-
children: "Company (optional)"
|
|
1683
|
-
}), /* @__PURE__ */ jsx(Input$1, {
|
|
1684
|
-
id: "contact-company",
|
|
1685
|
-
autoComplete: "organization",
|
|
1686
|
-
maxLength: 200,
|
|
1687
|
-
value: form.company,
|
|
1688
|
-
onChange: update("company")
|
|
1689
|
-
})]
|
|
1690
|
-
}),
|
|
1691
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1692
|
-
className: "space-y-1.5",
|
|
1693
|
-
children: [
|
|
1694
|
-
/* @__PURE__ */ jsx(Label, {
|
|
1695
|
-
htmlFor: "contact-message",
|
|
1696
|
-
children: "Message"
|
|
1697
|
-
}),
|
|
1698
|
-
/* @__PURE__ */ jsx(Textarea, {
|
|
1699
|
-
id: "contact-message",
|
|
1700
|
-
rows: 5,
|
|
1701
|
-
maxLength: 5e3,
|
|
1702
|
-
placeholder: "Expected traffic, use case, timeline…",
|
|
1703
|
-
value: form.message,
|
|
1704
|
-
onChange: update("message"),
|
|
1705
|
-
"aria-invalid": Boolean(errors.message)
|
|
1706
|
-
}),
|
|
1707
|
-
errors.message ? /* @__PURE__ */ jsx("p", {
|
|
1708
|
-
className: "text-sm text-destructive",
|
|
1709
|
-
children: errors.message
|
|
1710
|
-
}) : null
|
|
1711
|
-
]
|
|
1712
|
-
}),
|
|
1713
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1714
|
-
className: "absolute -left-[9999px]",
|
|
1715
|
-
"aria-hidden": "true",
|
|
1716
|
-
children: [/* @__PURE__ */ jsx("label", {
|
|
1717
|
-
htmlFor: "contact-website",
|
|
1718
|
-
children: "Website"
|
|
1719
|
-
}), /* @__PURE__ */ jsx("input", {
|
|
1720
|
-
id: "contact-website",
|
|
1721
|
-
tabIndex: -1,
|
|
1722
|
-
autoComplete: "off",
|
|
1723
|
-
value: form.website,
|
|
1724
|
-
onChange: update("website")
|
|
1725
|
-
})]
|
|
1726
|
-
}),
|
|
1727
|
-
submit.isError ? /* @__PURE__ */ jsx(Alert$1, {
|
|
1728
|
-
variant: "destructive",
|
|
1729
|
-
children: /* @__PURE__ */ jsx(AlertDescription, { children: submit.error.message })
|
|
1730
|
-
}) : null,
|
|
1731
|
-
/* @__PURE__ */ jsx(Button$2, {
|
|
1732
|
-
type: "submit",
|
|
1733
|
-
className: "w-full",
|
|
1734
|
-
disabled: submit.isPending || isSimulated,
|
|
1735
|
-
children: submit.isPending ? "Sending…" : "Send message"
|
|
1736
|
-
})
|
|
1737
|
-
]
|
|
1738
|
-
})
|
|
1739
|
-
]
|
|
1740
|
-
});
|
|
1741
|
-
};
|
|
1742
|
-
|
|
1743
|
-
//#endregion
|
|
1744
|
-
//#region src/hooks/useCredentials.ts
|
|
1745
|
-
function useCredentials() {
|
|
1746
|
-
const auth = useAuth();
|
|
1747
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
1748
|
-
const isPreview = useDevPortalPreviewMode();
|
|
1749
|
-
const isDemoSession = useIsDemoSession();
|
|
1750
|
-
const { previewPlans } = useDevPortalBillingConfig();
|
|
1751
|
-
const liveQuery = useQuery({
|
|
1752
|
-
queryKey: ["dev-portal-credentials", apiBaseUrl],
|
|
1753
|
-
enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
|
|
1754
|
-
queryFn: async () => {
|
|
1755
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/credentials`, {
|
|
1756
|
-
method: "GET",
|
|
1757
|
-
headers: { Accept: "application/json" },
|
|
1758
|
-
credentials: "include"
|
|
1759
|
-
});
|
|
1760
|
-
if (response.status === 404) throw new Error("NO_ENTITLEMENT");
|
|
1761
|
-
if (!response.ok) throw new Error("Failed to load API credentials.");
|
|
1762
|
-
return await response.json();
|
|
1763
|
-
},
|
|
1764
|
-
retry: (failureCount, error) => error instanceof Error && error.message === "NO_ENTITLEMENT" ? false : failureCount < 2
|
|
1765
|
-
});
|
|
1766
|
-
if (isDemoSession && auth.isAuthenticated) return {
|
|
1767
|
-
...liveQuery,
|
|
1768
|
-
data: createDemoCredentials(previewPlans),
|
|
1769
|
-
error: null,
|
|
1770
|
-
isError: false,
|
|
1771
|
-
isFetching: false,
|
|
1772
|
-
isLoading: false,
|
|
1773
|
-
isPending: false,
|
|
1774
|
-
isSuccess: true,
|
|
1775
|
-
status: "success"
|
|
1776
|
-
};
|
|
1777
|
-
return liveQuery;
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
//#endregion
|
|
1781
|
-
//#region src/pages/CredentialsPage.tsx
|
|
1782
|
-
const CredentialsPage = () => {
|
|
1783
|
-
const auth = useAuth();
|
|
1784
|
-
const isPreview = useDevPortalPreviewMode();
|
|
1785
|
-
const isDemoSession = useIsDemoSession();
|
|
1786
|
-
const { data: credentials, isLoading, error } = useCredentials();
|
|
1787
|
-
const { data: apiKeys, isLoading: keysLoading, error: keysError } = useApiKeys();
|
|
1788
|
-
const { gatewayBaseUrl: configuredGatewayBaseUrl } = useDevPortalBillingConfig();
|
|
1789
|
-
const gatewayBaseUrl = credentials?.gatewayBaseUrl?.trim() || apiKeys?.gatewayBaseUrl?.trim() || configuredGatewayBaseUrl?.trim() || zudokuConfig.__meta?.gatewayBaseUrl?.trim() || null;
|
|
1790
|
-
const header = apiKeys?.header ?? "Ocp-Apim-Subscription-Key";
|
|
1791
|
-
const plans = apiKeys?.plans ?? [];
|
|
1792
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1793
|
-
className: "mx-auto max-w-screen-lg px-4 pb-16 pt-(--padding-content-top) lg:px-8",
|
|
1794
|
-
children: [
|
|
1795
|
-
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "API credentials" }) }),
|
|
1796
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1797
|
-
className: "mb-8 flex items-start gap-4",
|
|
1798
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1799
|
-
className: "flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary",
|
|
1800
|
-
children: /* @__PURE__ */ jsx(KeyRoundIcon, { size: 24 })
|
|
1801
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1802
|
-
className: "space-y-1",
|
|
1803
|
-
children: [/* @__PURE__ */ jsx("h1", {
|
|
1804
|
-
className: "text-3xl font-semibold tracking-tight",
|
|
1805
|
-
children: "API credentials"
|
|
1806
|
-
}), /* @__PURE__ */ jsx("p", {
|
|
1807
|
-
className: "text-muted-foreground",
|
|
1808
|
-
children: "Create API keys for each subscribed plan and use them to authenticate requests to the API gateway."
|
|
1809
|
-
})]
|
|
1810
|
-
})]
|
|
1811
|
-
}),
|
|
1812
|
-
isDemoSession ? /* @__PURE__ */ jsx("div", {
|
|
1813
|
-
className: "mb-6 rounded-xl border border-dashed bg-muted/20 p-4 text-sm text-muted-foreground",
|
|
1814
|
-
children: "Local demo credentials. Publish the portal to receive live subscription keys from your dev-portal backend."
|
|
1815
|
-
}) : null,
|
|
1816
|
-
isPreview && !isDemoSession ? /* @__PURE__ */ jsx("div", {
|
|
1817
|
-
className: "rounded-xl border border-dashed p-8 text-center",
|
|
1818
|
-
children: /* @__PURE__ */ jsx("p", {
|
|
1819
|
-
className: "text-muted-foreground",
|
|
1820
|
-
children: "API credentials are available after the developer portal is published."
|
|
1821
|
-
})
|
|
1822
|
-
}) : null,
|
|
1823
|
-
!isPreview && !isDemoSession && !auth.isAuthenticated ? /* @__PURE__ */ jsxs("div", {
|
|
1824
|
-
className: "rounded-xl border border-dashed p-8 text-center",
|
|
1825
|
-
children: [/* @__PURE__ */ jsx("p", {
|
|
1826
|
-
className: "text-muted-foreground",
|
|
1827
|
-
children: "Sign in to view your subscription keys."
|
|
1828
|
-
}), /* @__PURE__ */ jsx(Button$2, {
|
|
1829
|
-
className: "mt-4",
|
|
1830
|
-
asChild: true,
|
|
1831
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1832
|
-
to: ACCOUNT_PLANS_PATH,
|
|
1833
|
-
children: "View plans"
|
|
1834
|
-
})
|
|
1835
|
-
})]
|
|
1836
|
-
}) : null,
|
|
1837
|
-
!isPreview && !isDemoSession && auth.isAuthenticated && (isLoading || keysLoading) ? /* @__PURE__ */ jsx("p", {
|
|
1838
|
-
className: "text-muted-foreground",
|
|
1839
|
-
children: "Loading credentials…"
|
|
1840
|
-
}) : null,
|
|
1841
|
-
!isPreview && !isDemoSession && auth.isAuthenticated && (error || keysError) ? /* @__PURE__ */ jsxs("div", {
|
|
1842
|
-
className: "rounded-xl border border-dashed p-8 text-center",
|
|
1843
|
-
children: [/* @__PURE__ */ jsx("p", {
|
|
1844
|
-
className: "text-muted-foreground",
|
|
1845
|
-
children: error instanceof Error && error.message === "NO_ENTITLEMENT" || keysError instanceof Error && keysError.message === "NO_ENTITLEMENT" ? "No active subscription found. Choose a plan to get your API key." : "Unable to load API credentials. Try again in a moment."
|
|
1846
|
-
}), /* @__PURE__ */ jsx(Button$2, {
|
|
1847
|
-
className: "mt-4",
|
|
1848
|
-
asChild: true,
|
|
1849
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1850
|
-
to: ACCOUNT_PLANS_PATH,
|
|
1851
|
-
children: "View plans"
|
|
1852
|
-
})
|
|
1853
|
-
})]
|
|
1854
|
-
}) : null,
|
|
1855
|
-
(isDemoSession || !isPreview) && auth.isAuthenticated && !error && !keysError && !isLoading && !keysLoading ? /* @__PURE__ */ jsxs("div", {
|
|
1856
|
-
className: "space-y-6",
|
|
1857
|
-
children: [plans.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
1858
|
-
className: "rounded-xl border border-dashed p-8 text-center",
|
|
1859
|
-
children: [/* @__PURE__ */ jsx("p", {
|
|
1860
|
-
className: "text-muted-foreground",
|
|
1861
|
-
children: "Choose a plan to get your first API key."
|
|
1862
|
-
}), /* @__PURE__ */ jsx(Button$2, {
|
|
1863
|
-
className: "mt-4",
|
|
1864
|
-
asChild: true,
|
|
1865
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1866
|
-
to: ACCOUNT_PLANS_PATH,
|
|
1867
|
-
children: "View plans"
|
|
1868
|
-
})
|
|
1869
|
-
})]
|
|
1870
|
-
}) : plans.map((plan) => /* @__PURE__ */ jsx(ApiKeysPanel, {
|
|
1871
|
-
plan,
|
|
1872
|
-
header,
|
|
1873
|
-
readOnly: isDemoSession
|
|
1874
|
-
}, plan.planSku)), /* @__PURE__ */ jsxs("article", {
|
|
1875
|
-
className: "rounded-xl border bg-card/60 p-6 space-y-4",
|
|
1876
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1877
|
-
className: "space-y-1",
|
|
1878
|
-
children: [/* @__PURE__ */ jsx("h2", {
|
|
1879
|
-
className: "text-lg font-medium",
|
|
1880
|
-
children: "Gateway URL"
|
|
1881
|
-
}), /* @__PURE__ */ jsx("p", {
|
|
1882
|
-
className: "text-sm text-muted-foreground",
|
|
1883
|
-
children: "Base URL for API requests through the gateway."
|
|
1884
|
-
})]
|
|
1885
|
-
}), gatewayBaseUrl ? /* @__PURE__ */ jsx(Value, {
|
|
1886
|
-
className: "max-w-full w-full",
|
|
1887
|
-
value: gatewayBaseUrl
|
|
1888
|
-
}) : /* @__PURE__ */ jsx("p", {
|
|
1889
|
-
className: "text-sm text-muted-foreground",
|
|
1890
|
-
children: "Available after your API is deployed to the gateway."
|
|
1891
|
-
})]
|
|
1892
|
-
})]
|
|
1893
|
-
}) : null,
|
|
1894
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1895
|
-
className: "mt-8 flex flex-wrap gap-3",
|
|
1896
|
-
children: [/* @__PURE__ */ jsx(Button$2, {
|
|
1897
|
-
variant: "outline",
|
|
1898
|
-
asChild: true,
|
|
1899
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1900
|
-
to: "/account/subscriptions",
|
|
1901
|
-
children: "My subscriptions"
|
|
1902
|
-
})
|
|
1903
|
-
}), /* @__PURE__ */ jsx(Button$2, {
|
|
1904
|
-
variant: "outline",
|
|
1905
|
-
asChild: true,
|
|
1906
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1907
|
-
to: "/api",
|
|
1908
|
-
children: "API reference"
|
|
1909
|
-
})
|
|
1910
|
-
})]
|
|
1911
|
-
})
|
|
1912
|
-
]
|
|
1913
|
-
});
|
|
1914
|
-
};
|
|
1915
|
-
|
|
1916
|
-
//#endregion
|
|
1917
|
-
//#region src/utils/pricing-config.ts
|
|
1918
|
-
function isPreviewPricingEnabled() {
|
|
1919
|
-
if (manifestCatalog?.pricingEnabled === true) return true;
|
|
1920
|
-
return zudokuConfig?.__meta?.pricingEnabled === true;
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
//#endregion
|
|
1924
|
-
//#region src/pages/PricingPage.tsx
|
|
1925
|
-
const DEFAULT_COMPARE_ROWS = [
|
|
1926
|
-
{
|
|
1927
|
-
label: "Monthly requests",
|
|
1928
|
-
values: [
|
|
1929
|
-
"1,000",
|
|
1930
|
-
"50,000",
|
|
1931
|
-
"500,000",
|
|
1932
|
-
"Unlimited"
|
|
1933
|
-
]
|
|
1934
|
-
},
|
|
1935
|
-
{
|
|
1936
|
-
label: "Rate limit",
|
|
1937
|
-
values: [
|
|
1938
|
-
"10/sec",
|
|
1939
|
-
"50/sec",
|
|
1940
|
-
"200/sec",
|
|
1941
|
-
"Custom"
|
|
1942
|
-
]
|
|
1943
|
-
},
|
|
1944
|
-
{
|
|
1945
|
-
label: "Team seats",
|
|
1946
|
-
values: [
|
|
1947
|
-
"1",
|
|
1948
|
-
"1",
|
|
1949
|
-
"5",
|
|
1950
|
-
"Unlimited"
|
|
1951
|
-
]
|
|
1952
|
-
},
|
|
1953
|
-
{
|
|
1954
|
-
label: "Webhooks",
|
|
1955
|
-
values: [
|
|
1956
|
-
"—",
|
|
1957
|
-
"Included",
|
|
1958
|
-
"Included",
|
|
1959
|
-
"Included"
|
|
1960
|
-
]
|
|
1961
|
-
},
|
|
1962
|
-
{
|
|
1963
|
-
label: "SSO",
|
|
1964
|
-
values: [
|
|
1965
|
-
"—",
|
|
1966
|
-
"—",
|
|
1967
|
-
"—",
|
|
1968
|
-
"Included"
|
|
1969
|
-
]
|
|
1970
|
-
},
|
|
1971
|
-
{
|
|
1972
|
-
label: "Support",
|
|
1973
|
-
values: [
|
|
1974
|
-
"—",
|
|
1975
|
-
"Email",
|
|
1976
|
-
"Priority",
|
|
1977
|
-
"Dedicated SLA"
|
|
1978
|
-
]
|
|
1979
|
-
}
|
|
1980
|
-
];
|
|
1981
|
-
const DEFAULT_FAQS = [{
|
|
1982
|
-
question: "What happens if I exceed my quota?",
|
|
1983
|
-
answer: "Requests over your monthly quota return a 429 response so you can test rate-limit handling instead of being cut off unexpectedly."
|
|
1984
|
-
}, {
|
|
1985
|
-
question: "Can I change plans later?",
|
|
1986
|
-
answer: "Yes, upgrade or downgrade any time from your billing page. Changes apply on your next billing cycle."
|
|
1987
|
-
}];
|
|
1988
|
-
const PricingPage = () => {
|
|
1989
|
-
const { pricing, previewPlans } = useDevPortalBillingConfig();
|
|
1990
|
-
const isPreview = useDevPortalPreviewMode();
|
|
1991
|
-
const pricingEnabled = isPreviewPricingEnabled() || isPreview;
|
|
1992
|
-
const livePlans = usePlans();
|
|
1993
|
-
const catalog = isPreview ? parseManifestPlans(previewPlans) : livePlans.data;
|
|
1994
|
-
const isLoading = isPreview ? false : livePlans.isLoading;
|
|
1995
|
-
const isError = isPreview ? false : livePlans.isError;
|
|
1996
|
-
if (!pricingEnabled) return /* @__PURE__ */ jsx(Navigate, {
|
|
1997
|
-
to: "/",
|
|
1998
|
-
replace: true
|
|
1999
|
-
});
|
|
2000
|
-
const title = pricing?.title ?? "Pay for what you call";
|
|
2001
|
-
const subtitle = pricing?.subtitle ?? "Every plan includes the full endpoint catalog. Higher tiers raise your request quota, rate limit, and support level.";
|
|
2002
|
-
const plans = catalog?.plans ?? [];
|
|
2003
|
-
const featuredSku = pickFeaturedPlanSku(plans);
|
|
2004
|
-
const compareRows = pricing?.compareRows && pricing.compareRows.length > 0 ? pricing.compareRows : DEFAULT_COMPARE_ROWS;
|
|
2005
|
-
const faqs = pricing?.faqs && pricing.faqs.length > 0 ? pricing.faqs : DEFAULT_FAQS;
|
|
2006
|
-
const compareHeaders = plans.length > 0 ? plans.map((p) => p.displayName) : [
|
|
2007
|
-
"Free",
|
|
2008
|
-
"Developer",
|
|
2009
|
-
"Team",
|
|
2010
|
-
"Enterprise"
|
|
2011
|
-
];
|
|
2012
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
2013
|
-
className: "w-full",
|
|
2014
|
-
children: [
|
|
2015
|
-
/* @__PURE__ */ jsxs(Head, { children: [/* @__PURE__ */ jsx("title", { children: title }), /* @__PURE__ */ jsx("meta", {
|
|
2016
|
-
name: "description",
|
|
2017
|
-
content: subtitle
|
|
2018
|
-
})] }),
|
|
2019
|
-
/* @__PURE__ */ jsxs("section", {
|
|
2020
|
-
className: "px-6 pb-4 pt-[clamp(3rem,6vw,4.5rem)] text-center",
|
|
2021
|
-
children: [
|
|
2022
|
-
/* @__PURE__ */ jsx("p", {
|
|
2023
|
-
className: "text-xs font-bold uppercase tracking-[0.08em] text-primary",
|
|
2024
|
-
children: "Pricing"
|
|
2025
|
-
}),
|
|
2026
|
-
/* @__PURE__ */ jsx(Heading, {
|
|
2027
|
-
level: 1,
|
|
2028
|
-
className: "mt-3 text-[clamp(2rem,3.5vw,3rem)] font-extrabold tracking-[-0.02em]",
|
|
2029
|
-
"data-testid": "title",
|
|
2030
|
-
children: title
|
|
2031
|
-
}),
|
|
2032
|
-
/* @__PURE__ */ jsx("p", {
|
|
2033
|
-
className: "mx-auto mt-3.5 max-w-[34rem] text-lg leading-relaxed text-muted-foreground",
|
|
2034
|
-
"data-testid": "subtitle",
|
|
2035
|
-
children: subtitle
|
|
2036
|
-
})
|
|
2037
|
-
]
|
|
2038
|
-
}),
|
|
2039
|
-
/* @__PURE__ */ jsxs("section", {
|
|
2040
|
-
className: "px-6 pb-16 pt-8",
|
|
2041
|
-
children: [
|
|
2042
|
-
isLoading ? /* @__PURE__ */ jsx("p", {
|
|
2043
|
-
className: "text-center text-muted-foreground",
|
|
2044
|
-
children: "Loading plans…"
|
|
2045
|
-
}) : null,
|
|
2046
|
-
isError ? /* @__PURE__ */ jsx("p", {
|
|
2047
|
-
className: "text-center text-destructive",
|
|
2048
|
-
children: "Unable to load plans."
|
|
2049
|
-
}) : null,
|
|
2050
|
-
plans.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
2051
|
-
className: "mx-auto grid max-w-[1200px] grid-cols-1 items-stretch gap-5 sm:grid-cols-2 lg:grid-cols-4",
|
|
2052
|
-
children: plans.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
|
|
2053
|
-
plan,
|
|
2054
|
-
mode: "public",
|
|
2055
|
-
featured: plan.sku === featuredSku
|
|
2056
|
-
}, plan.sku))
|
|
2057
|
-
}) : !isLoading && !isError ? /* @__PURE__ */ jsxs("div", {
|
|
2058
|
-
className: "mx-auto max-w-lg rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
|
|
2059
|
-
children: [
|
|
2060
|
-
"No plans are configured yet. Add plans in",
|
|
2061
|
-
" ",
|
|
2062
|
-
/* @__PURE__ */ jsx("code", {
|
|
2063
|
-
className: "rounded bg-muted px-1.5 py-0.5",
|
|
2064
|
-
children: "apitogo.config.json"
|
|
2065
|
-
}),
|
|
2066
|
-
" ",
|
|
2067
|
-
"and set",
|
|
2068
|
-
" ",
|
|
2069
|
-
/* @__PURE__ */ jsx("code", {
|
|
2070
|
-
className: "rounded bg-muted px-1.5 py-0.5",
|
|
2071
|
-
children: "modules.userPanel.plans.enabled"
|
|
2072
|
-
}),
|
|
2073
|
-
" ",
|
|
2074
|
-
"to true."
|
|
2075
|
-
]
|
|
2076
|
-
}) : null
|
|
2077
|
-
]
|
|
2078
|
-
}),
|
|
2079
|
-
/* @__PURE__ */ jsx("section", {
|
|
2080
|
-
className: "border-t border-border bg-muted/40 px-6 py-16",
|
|
2081
|
-
"data-testid": "pricing-compare",
|
|
2082
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
2083
|
-
className: "mx-auto max-w-[1200px]",
|
|
2084
|
-
children: [/* @__PURE__ */ jsx("h2", {
|
|
2085
|
-
className: "mb-6 text-[clamp(1.5rem,2.5vw,2.25rem)] font-extrabold tracking-[-0.02em]",
|
|
2086
|
-
children: "Compare plans"
|
|
2087
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
2088
|
-
className: "overflow-x-auto rounded-xl border border-border bg-card",
|
|
2089
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
2090
|
-
className: "min-w-[720px]",
|
|
2091
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
2092
|
-
className: "grid gap-0 border-b border-border px-5 py-3.5 text-[0.78125rem] font-bold uppercase tracking-[0.04em] text-muted-foreground",
|
|
2093
|
-
style: { gridTemplateColumns: `minmax(10rem,1.4fr) repeat(${compareHeaders.length}, minmax(6rem,1fr))` },
|
|
2094
|
-
children: [/* @__PURE__ */ jsx("div", { children: "Feature" }), compareHeaders.map((header) => /* @__PURE__ */ jsx("div", { children: header }, header))]
|
|
2095
|
-
}), compareRows.map((row) => /* @__PURE__ */ jsxs("div", {
|
|
2096
|
-
className: "grid gap-0 border-b border-border px-5 py-3.5 text-[0.84375rem] text-muted-foreground last:border-b-0",
|
|
2097
|
-
style: { gridTemplateColumns: `minmax(10rem,1.4fr) repeat(${compareHeaders.length}, minmax(6rem,1fr))` },
|
|
2098
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
2099
|
-
className: "font-medium text-foreground",
|
|
2100
|
-
children: row.label
|
|
2101
|
-
}), compareHeaders.map((header, index) => /* @__PURE__ */ jsx("div", { children: row.values[index] ?? "—" }, `${row.label}-${header}`))]
|
|
2102
|
-
}, row.label))]
|
|
2103
|
-
})
|
|
2104
|
-
})]
|
|
2105
|
-
})
|
|
2106
|
-
}),
|
|
2107
|
-
/* @__PURE__ */ jsx("section", {
|
|
2108
|
-
className: "px-6 py-16",
|
|
2109
|
-
"data-testid": "pricing-faq",
|
|
2110
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
2111
|
-
className: "mx-auto max-w-4xl",
|
|
2112
|
-
children: [/* @__PURE__ */ jsx("h2", {
|
|
2113
|
-
className: "mb-8 text-center text-[clamp(1.5rem,2.5vw,2.25rem)] font-extrabold tracking-[-0.02em]",
|
|
2114
|
-
children: "Frequently asked questions"
|
|
2115
|
-
}), /* @__PURE__ */ jsx("div", { children: faqs.map((faq) => /* @__PURE__ */ jsxs("div", {
|
|
2116
|
-
className: "border-b border-border py-5 last:border-b-0",
|
|
2117
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
2118
|
-
className: "text-[0.9375rem] font-bold",
|
|
2119
|
-
children: faq.question
|
|
2120
|
-
}), /* @__PURE__ */ jsx("p", {
|
|
2121
|
-
className: "mt-2 text-sm leading-relaxed text-muted-foreground",
|
|
2122
|
-
children: faq.answer
|
|
2123
|
-
})]
|
|
2124
|
-
}, faq.question)) })]
|
|
2125
|
-
})
|
|
2126
|
-
})
|
|
2127
|
-
]
|
|
2128
|
-
});
|
|
2129
|
-
};
|
|
2130
|
-
|
|
2131
|
-
//#endregion
|
|
2132
|
-
//#region src/hooks/useBillingPortal.ts
|
|
2133
|
-
function useBillingPortal() {
|
|
2134
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
2135
|
-
return useMutation({ mutationFn: async (returnUrl) => {
|
|
2136
|
-
if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
|
|
2137
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/billing-portal`, {
|
|
2138
|
-
method: "POST",
|
|
2139
|
-
headers: {
|
|
2140
|
-
Accept: "application/json",
|
|
2141
|
-
"Content-Type": "application/json"
|
|
2142
|
-
},
|
|
2143
|
-
credentials: "include",
|
|
2144
|
-
body: JSON.stringify({ returnUrl: returnUrl ?? `${window.location.origin}/account/subscriptions` })
|
|
2145
|
-
});
|
|
2146
|
-
if (!response.ok) throw new Error("Unable to open billing portal.");
|
|
2147
|
-
const body = await response.json();
|
|
2148
|
-
if (!body.url) throw new Error("Billing portal URL was not returned.");
|
|
2149
|
-
window.location.assign(body.url);
|
|
2150
|
-
} });
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
//#endregion
|
|
2154
|
-
//#region src/pages/SubscriptionsPage.tsx
|
|
2155
|
-
const SubscriptionsPage = () => {
|
|
2156
|
-
const auth = useAuth();
|
|
2157
|
-
const [searchParams] = useSearchParams();
|
|
2158
|
-
const checkoutSuccess = searchParams.get("checkout") === "success";
|
|
2159
|
-
const { data: me, isLoading } = useMySubscriptions();
|
|
2160
|
-
const { data: catalog } = usePlans();
|
|
2161
|
-
const billingPortal = useBillingPortal();
|
|
2162
|
-
const { data: usage } = useUsage("mtd");
|
|
2163
|
-
const planBySku = new Map((catalog?.plans ?? []).map((plan) => [plan.sku, plan]));
|
|
2164
|
-
const subscriptions = me?.subscriptions ?? [];
|
|
2165
|
-
const activeSubscription = subscriptions.find((sub) => isActiveSubscription(sub.status));
|
|
2166
|
-
const activePlan = activeSubscription ? planBySku.get(activeSubscription.planSku) : void 0;
|
|
2167
|
-
const monthlyQuota = activePlan ? parseMonthlyQuotaCalls(activePlan.limitsJson) : null;
|
|
2168
|
-
const usedRequests = usage?.requestCount ?? 0;
|
|
2169
|
-
const quotaPct = monthlyQuota && monthlyQuota > 0 ? Math.min(100, Math.round(usedRequests / monthlyQuota * 100)) : null;
|
|
2170
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
2171
|
-
className: "px-8 py-7",
|
|
2172
|
-
children: [
|
|
2173
|
-
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Billing" }) }),
|
|
2174
|
-
/* @__PURE__ */ jsx("h1", {
|
|
2175
|
-
className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
|
|
2176
|
-
children: "Billing"
|
|
2177
|
-
}),
|
|
2178
|
-
/* @__PURE__ */ jsx("p", {
|
|
2179
|
-
className: "mb-8 text-muted-foreground",
|
|
2180
|
-
children: "Manage your plan, payment method, and invoices."
|
|
2181
|
-
}),
|
|
2182
|
-
checkoutSuccess ? /* @__PURE__ */ jsxs("div", {
|
|
2183
|
-
className: "mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-sm",
|
|
2184
|
-
children: [
|
|
2185
|
-
"Your subscription is active.",
|
|
2186
|
-
" ",
|
|
2187
|
-
/* @__PURE__ */ jsx(Link$1, {
|
|
2188
|
-
to: CREDENTIALS_PATH,
|
|
2189
|
-
className: "font-medium underline",
|
|
2190
|
-
children: "View your API credentials"
|
|
2191
|
-
}),
|
|
2192
|
-
"."
|
|
2193
|
-
]
|
|
2194
|
-
}) : null,
|
|
2195
|
-
isLoading ? /* @__PURE__ */ jsx("p", {
|
|
2196
|
-
className: "text-muted-foreground",
|
|
2197
|
-
children: "Loading billing details…"
|
|
2198
|
-
}) : null,
|
|
2199
|
-
!isLoading && activePlan && activeSubscription ? /* @__PURE__ */ jsxs("div", {
|
|
2200
|
-
className: "mb-8 rounded-xl border border-border bg-card p-6",
|
|
2201
|
-
children: [
|
|
2202
|
-
/* @__PURE__ */ jsx("p", {
|
|
2203
|
-
className: "text-xs font-medium uppercase tracking-[0.05em] text-muted-foreground",
|
|
2204
|
-
children: "Current plan"
|
|
2205
|
-
}),
|
|
2206
|
-
/* @__PURE__ */ jsx("h2", {
|
|
2207
|
-
className: "mt-2 text-2xl font-bold",
|
|
2208
|
-
children: activePlan.displayName
|
|
2209
|
-
}),
|
|
2210
|
-
/* @__PURE__ */ jsxs("p", {
|
|
2211
|
-
className: "mt-1 text-sm text-muted-foreground",
|
|
2212
|
-
children: [
|
|
2213
|
-
formatPlanPrice(activePlan),
|
|
2214
|
-
" ·",
|
|
2215
|
-
" ",
|
|
2216
|
-
formatBillingPeriod(activePlan.billingPeriod)
|
|
2217
|
-
]
|
|
2218
|
-
}),
|
|
2219
|
-
activeSubscription.currentPeriodEnd ? /* @__PURE__ */ jsxs("p", {
|
|
2220
|
-
className: "mt-3 text-sm text-muted-foreground",
|
|
2221
|
-
children: [
|
|
2222
|
-
"Renews",
|
|
2223
|
-
" ",
|
|
2224
|
-
new Date(activeSubscription.currentPeriodEnd).toLocaleDateString()
|
|
2225
|
-
]
|
|
2226
|
-
}) : null,
|
|
2227
|
-
quotaPct != null && monthlyQuota != null ? /* @__PURE__ */ jsxs("div", {
|
|
2228
|
-
className: "mt-5",
|
|
2229
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
2230
|
-
className: "mb-1.5 flex justify-between text-xs text-muted-foreground",
|
|
2231
|
-
children: [/* @__PURE__ */ jsx("span", { children: "Monthly requests" }), /* @__PURE__ */ jsxs("span", { children: [
|
|
2232
|
-
usedRequests.toLocaleString(),
|
|
2233
|
-
" /",
|
|
2234
|
-
" ",
|
|
2235
|
-
monthlyQuota.toLocaleString()
|
|
2236
|
-
] })]
|
|
2237
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
2238
|
-
className: "h-2 rounded-full bg-muted",
|
|
2239
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
2240
|
-
className: "h-full rounded-full bg-primary transition-[width]",
|
|
2241
|
-
style: { width: `${quotaPct}%` }
|
|
2242
|
-
})
|
|
2243
|
-
})]
|
|
2244
|
-
}) : null
|
|
2245
|
-
]
|
|
2246
|
-
}) : null,
|
|
2247
|
-
!isLoading && subscriptions.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
2248
|
-
className: "mb-8 rounded-xl border border-dashed p-8 text-center",
|
|
2249
|
-
children: [/* @__PURE__ */ jsx("p", {
|
|
2250
|
-
className: "text-muted-foreground",
|
|
2251
|
-
children: "You do not have an active subscription yet."
|
|
2252
|
-
}), /* @__PURE__ */ jsx(Button$1, {
|
|
2253
|
-
className: "mt-4",
|
|
2254
|
-
asChild: true,
|
|
2255
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
2256
|
-
to: ACCOUNT_PLANS_PATH,
|
|
2257
|
-
children: "View plans"
|
|
2258
|
-
})
|
|
2259
|
-
})]
|
|
2260
|
-
}) : null,
|
|
2261
|
-
!isLoading && subscriptions.length > 1 ? /* @__PURE__ */ jsx("div", {
|
|
2262
|
-
className: "mb-8 space-y-4",
|
|
2263
|
-
children: subscriptions.map((subscription) => {
|
|
2264
|
-
const plan = planBySku.get(subscription.planSku);
|
|
2265
|
-
return /* @__PURE__ */ jsx("article", {
|
|
2266
|
-
className: "rounded-xl border bg-card p-5",
|
|
2267
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
2268
|
-
className: "flex flex-wrap items-start justify-between gap-4",
|
|
2269
|
-
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
2270
|
-
className: "font-semibold",
|
|
2271
|
-
children: plan?.displayName ?? subscription.planSku
|
|
2272
|
-
}), plan ? /* @__PURE__ */ jsxs("p", {
|
|
2273
|
-
className: "mt-1 text-sm text-muted-foreground",
|
|
2274
|
-
children: [
|
|
2275
|
-
formatPlanPrice(plan),
|
|
2276
|
-
" ·",
|
|
2277
|
-
" ",
|
|
2278
|
-
formatBillingPeriod(plan.billingPeriod)
|
|
2279
|
-
]
|
|
2280
|
-
}) : null] }), /* @__PURE__ */ jsx("span", {
|
|
2281
|
-
className: "rounded-full bg-muted px-3 py-1 text-xs font-medium text-muted-foreground",
|
|
2282
|
-
children: subscription.status
|
|
2283
|
-
})]
|
|
2284
|
-
})
|
|
2285
|
-
}, `${subscription.planSku}-${subscription.status}`);
|
|
2286
|
-
})
|
|
2287
|
-
}) : null,
|
|
2288
|
-
/* @__PURE__ */ jsxs("div", {
|
|
2289
|
-
className: "flex flex-wrap gap-3",
|
|
2290
|
-
children: [
|
|
2291
|
-
/* @__PURE__ */ jsx(Button$1, {
|
|
2292
|
-
variant: "outline",
|
|
2293
|
-
asChild: true,
|
|
2294
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
2295
|
-
to: ACCOUNT_PLANS_PATH,
|
|
2296
|
-
children: "Change plan"
|
|
2297
|
-
})
|
|
2298
|
-
}),
|
|
2299
|
-
auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
|
|
2300
|
-
variant: "default",
|
|
2301
|
-
disabled: billingPortal.isPending,
|
|
2302
|
-
onClick: () => {
|
|
2303
|
-
billingPortal.mutate();
|
|
2304
|
-
},
|
|
2305
|
-
children: billingPortal.isPending ? /* @__PURE__ */ jsx(Spinner, {}) : "Manage payment & invoices"
|
|
2306
|
-
}) : null,
|
|
2307
|
-
auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
|
|
2308
|
-
variant: "secondary",
|
|
2309
|
-
asChild: true,
|
|
2310
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
2311
|
-
to: CREDENTIALS_PATH,
|
|
2312
|
-
children: "API credentials"
|
|
2313
|
-
})
|
|
2314
|
-
}) : null
|
|
2315
|
-
]
|
|
2316
|
-
}),
|
|
2317
|
-
billingPortal.isError ? /* @__PURE__ */ jsx("p", {
|
|
2318
|
-
className: "mt-4 text-sm text-destructive",
|
|
2319
|
-
children: billingPortal.error instanceof Error ? billingPortal.error.message : "Unable to open billing portal."
|
|
2320
|
-
}) : null
|
|
2321
|
-
]
|
|
2322
|
-
});
|
|
2323
|
-
};
|
|
2324
|
-
|
|
2325
|
-
//#endregion
|
|
2326
|
-
//#region src/pages/UsagePage.tsx
|
|
2327
|
-
const RANGES = [
|
|
2328
|
-
{
|
|
2329
|
-
value: "24h",
|
|
2330
|
-
label: "24h"
|
|
2331
|
-
},
|
|
2332
|
-
{
|
|
2333
|
-
value: "7d",
|
|
2334
|
-
label: "7d"
|
|
2335
|
-
},
|
|
2336
|
-
{
|
|
2337
|
-
value: "30d",
|
|
2338
|
-
label: "30d"
|
|
2339
|
-
},
|
|
2340
|
-
{
|
|
2341
|
-
value: "90d",
|
|
2342
|
-
label: "90d"
|
|
2343
|
-
}
|
|
2344
|
-
];
|
|
2345
|
-
function formatNumber(value) {
|
|
2346
|
-
return new Intl.NumberFormat().format(value);
|
|
2347
|
-
}
|
|
2348
|
-
function formatPct(rate) {
|
|
2349
|
-
return `${(rate * 100).toFixed(rate >= .1 ? 1 : 2)}%`;
|
|
2350
|
-
}
|
|
2351
|
-
const UsagePage = () => {
|
|
2352
|
-
const auth = useAuth();
|
|
2353
|
-
const [range, setRange] = useState("30d");
|
|
2354
|
-
const { data, isLoading, isError } = useUsage(range);
|
|
2355
|
-
const maxBar = Math.max(1, ...data?.series.map((point) => point.requestCount) ?? [1]);
|
|
2356
|
-
const endpointTotal = data?.byEndpoint.reduce((sum, item) => sum + item.count, 0) ?? 0;
|
|
2357
|
-
if (!auth.isAuthenticated) return /* @__PURE__ */ jsxs("div", {
|
|
2358
|
-
className: "px-8 py-7",
|
|
2359
|
-
children: [
|
|
2360
|
-
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Usage" }) }),
|
|
2361
|
-
/* @__PURE__ */ jsx("h1", {
|
|
2362
|
-
className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
|
|
2363
|
-
children: "Usage"
|
|
2364
|
-
}),
|
|
2365
|
-
/* @__PURE__ */ jsx("p", {
|
|
2366
|
-
className: "text-muted-foreground",
|
|
2367
|
-
children: "Sign in to view your API usage."
|
|
2368
|
-
}),
|
|
2369
|
-
auth.isAuthEnabled ? /* @__PURE__ */ jsx(Button$1, {
|
|
2370
|
-
asChild: true,
|
|
2371
|
-
className: "mt-4",
|
|
2372
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
2373
|
-
to: "/signin",
|
|
2374
|
-
children: "Sign in"
|
|
2375
|
-
})
|
|
2376
|
-
}) : null
|
|
2377
|
-
]
|
|
2378
|
-
});
|
|
2379
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
2380
|
-
className: "px-8 py-7",
|
|
2381
|
-
children: [
|
|
2382
|
-
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Usage" }) }),
|
|
2383
|
-
/* @__PURE__ */ jsxs("div", {
|
|
2384
|
-
className: "mb-6 flex flex-wrap items-center justify-between gap-3",
|
|
2385
|
-
children: [/* @__PURE__ */ jsx("h1", {
|
|
2386
|
-
className: "text-[1.6rem] font-bold tracking-[-0.01em]",
|
|
2387
|
-
children: "Usage"
|
|
2388
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
2389
|
-
className: "flex gap-1 rounded-lg border border-border bg-card p-1",
|
|
2390
|
-
children: RANGES.map((item) => /* @__PURE__ */ jsx("button", {
|
|
2391
|
-
type: "button",
|
|
2392
|
-
onClick: () => setRange(item.value),
|
|
2393
|
-
className: `rounded-md px-3 py-1.5 text-xs font-semibold transition-colors ${range === item.value ? "bg-primary/14 text-primary" : "text-muted-foreground hover:text-foreground"}`,
|
|
2394
|
-
children: item.label
|
|
2395
|
-
}, item.value))
|
|
2396
|
-
})]
|
|
2397
|
-
}),
|
|
2398
|
-
isLoading ? /* @__PURE__ */ jsx("p", {
|
|
2399
|
-
className: "text-sm text-muted-foreground",
|
|
2400
|
-
children: "Loading usage…"
|
|
2401
|
-
}) : null,
|
|
2402
|
-
isError ? /* @__PURE__ */ jsx("p", {
|
|
2403
|
-
className: "text-sm text-destructive",
|
|
2404
|
-
children: "Unable to load usage data."
|
|
2405
|
-
}) : null,
|
|
2406
|
-
/* @__PURE__ */ jsxs("div", {
|
|
2407
|
-
className: "mb-5 grid grid-cols-1 gap-4 sm:grid-cols-3",
|
|
2408
|
-
children: [
|
|
2409
|
-
/* @__PURE__ */ jsxs("div", {
|
|
2410
|
-
className: "rounded-xl border border-border bg-card p-4",
|
|
2411
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
2412
|
-
className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
|
|
2413
|
-
children: "Requests"
|
|
2414
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
2415
|
-
className: "mt-1.5 text-2xl font-bold",
|
|
2416
|
-
children: data ? formatNumber(data.requestCount) : "—"
|
|
2417
|
-
})]
|
|
2418
|
-
}),
|
|
2419
|
-
/* @__PURE__ */ jsxs("div", {
|
|
2420
|
-
className: "rounded-xl border border-border bg-card p-4",
|
|
2421
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
2422
|
-
className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
|
|
2423
|
-
children: "Avg latency"
|
|
2424
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
2425
|
-
className: "mt-1.5 text-2xl font-bold",
|
|
2426
|
-
children: data?.avgLatencyMs != null ? `${data.avgLatencyMs} ms` : "—"
|
|
2427
|
-
})]
|
|
2428
|
-
}),
|
|
2429
|
-
/* @__PURE__ */ jsxs("div", {
|
|
2430
|
-
className: "rounded-xl border border-border bg-card p-4",
|
|
2431
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
2432
|
-
className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
|
|
2433
|
-
children: "Error rate"
|
|
2434
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
2435
|
-
className: "mt-1.5 text-2xl font-bold",
|
|
2436
|
-
children: data ? formatPct(data.errorRate) : "—"
|
|
2437
|
-
})]
|
|
2438
|
-
})
|
|
2439
|
-
]
|
|
2440
|
-
}),
|
|
2441
|
-
/* @__PURE__ */ jsxs("div", {
|
|
2442
|
-
className: "mb-5 rounded-xl border border-border bg-card p-6",
|
|
2443
|
-
children: [/* @__PURE__ */ jsxs("h2", {
|
|
2444
|
-
className: "mb-4 text-lg font-semibold",
|
|
2445
|
-
children: [
|
|
2446
|
-
"Requests over time (",
|
|
2447
|
-
range,
|
|
2448
|
-
")"
|
|
2449
|
-
]
|
|
2450
|
-
}), data && data.series.some((p) => p.requestCount > 0) ? /* @__PURE__ */ jsx("div", {
|
|
2451
|
-
className: "flex h-40 items-end gap-1.5",
|
|
2452
|
-
children: data.series.map((point) => /* @__PURE__ */ jsx("div", {
|
|
2453
|
-
title: `${point.dayUtc}: ${point.requestCount}`,
|
|
2454
|
-
className: "min-w-0 flex-1 rounded-t-sm bg-primary",
|
|
2455
|
-
style: { height: `${Math.max(4, point.requestCount / maxBar * 100)}%` }
|
|
2456
|
-
}, point.dayUtc))
|
|
2457
|
-
}) : /* @__PURE__ */ jsx("p", {
|
|
2458
|
-
className: "text-sm text-muted-foreground",
|
|
2459
|
-
children: "No request data for this range yet."
|
|
2460
|
-
})]
|
|
2461
|
-
}),
|
|
2462
|
-
/* @__PURE__ */ jsxs("div", {
|
|
2463
|
-
className: "grid grid-cols-1 gap-5 lg:grid-cols-[1.2fr_1fr]",
|
|
2464
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
2465
|
-
className: "rounded-xl border border-border bg-card p-6",
|
|
2466
|
-
children: [/* @__PURE__ */ jsx("h2", {
|
|
2467
|
-
className: "mb-4 text-lg font-semibold",
|
|
2468
|
-
children: "By endpoint"
|
|
2469
|
-
}), data && data.byEndpoint.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
2470
|
-
className: "space-y-3.5",
|
|
2471
|
-
children: data.byEndpoint.map((item) => {
|
|
2472
|
-
const pct = endpointTotal > 0 ? Math.round(item.count / endpointTotal * 100) : 0;
|
|
2473
|
-
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
2474
|
-
className: "mb-1 flex justify-between text-[13px]",
|
|
2475
|
-
children: [/* @__PURE__ */ jsx("code", {
|
|
2476
|
-
className: "font-mono text-foreground",
|
|
2477
|
-
children: item.path
|
|
2478
|
-
}), /* @__PURE__ */ jsxs("span", {
|
|
2479
|
-
className: "text-muted-foreground",
|
|
2480
|
-
children: [pct, "%"]
|
|
2481
|
-
})]
|
|
2482
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
2483
|
-
className: "h-1.5 rounded-full bg-muted",
|
|
2484
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
2485
|
-
className: "h-full rounded-full bg-primary",
|
|
2486
|
-
style: { width: `${pct}%` }
|
|
2487
|
-
})
|
|
2488
|
-
})] }, item.path);
|
|
2489
|
-
})
|
|
2490
|
-
}) : /* @__PURE__ */ jsx("p", {
|
|
2491
|
-
className: "text-sm text-muted-foreground",
|
|
2492
|
-
children: "No endpoint data."
|
|
2493
|
-
})]
|
|
2494
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
2495
|
-
className: "rounded-xl border border-border bg-card p-6",
|
|
2496
|
-
children: [/* @__PURE__ */ jsx("h2", {
|
|
2497
|
-
className: "mb-4 text-lg font-semibold",
|
|
2498
|
-
children: "Status codes"
|
|
2499
|
-
}), data && data.byStatus.length > 0 ? /* @__PURE__ */ jsx("div", { children: data.byStatus.map((item) => /* @__PURE__ */ jsxs("div", {
|
|
2500
|
-
className: "flex items-center justify-between border-b border-border py-2.5 last:border-0",
|
|
2501
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
2502
|
-
className: "text-sm font-medium",
|
|
2503
|
-
children: item.label
|
|
2504
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
2505
|
-
className: "text-sm font-bold",
|
|
2506
|
-
children: formatNumber(item.count)
|
|
2507
|
-
})]
|
|
2508
|
-
}, item.label)) }) : /* @__PURE__ */ jsx("p", {
|
|
2509
|
-
className: "text-sm text-muted-foreground",
|
|
2510
|
-
children: "No status data."
|
|
2511
|
-
})]
|
|
2512
|
-
})]
|
|
2513
|
-
})
|
|
2514
|
-
]
|
|
2515
|
-
});
|
|
2516
|
-
};
|
|
2517
|
-
|
|
2518
248
|
//#endregion
|
|
2519
249
|
//#region src/utils/auth-config.ts
|
|
2520
250
|
async function resolveAuthenticationNavEnabled(config) {
|
|
@@ -2599,101 +329,114 @@ function buildDevPortalHeaderNavigation({ navigation, pricingEnabled, authNavEna
|
|
|
2599
329
|
|
|
2600
330
|
//#endregion
|
|
2601
331
|
//#region src/DevPortalBillingPlugin.tsx
|
|
332
|
+
const AccountPlansPage$1 = lazy(() => import("./AccountPlansPage-CXOfTwlX.mjs").then((n) => n.n));
|
|
333
|
+
const CheckoutPage = lazy(() => import("./CheckoutPage-CW_EBGog.mjs"));
|
|
334
|
+
const ContactSalesPage = lazy(() => import("./ContactSalesPage-Vjzm-RxO.mjs"));
|
|
335
|
+
const CredentialsPage = lazy(() => import("./CredentialsPage-CtIEboEy.mjs"));
|
|
336
|
+
const PricingPage = lazy(() => import("./PricingPage-MTslSnCh.mjs"));
|
|
337
|
+
const SubscriptionsPage = lazy(() => import("./SubscriptionsPage-9lXnVks7.mjs"));
|
|
338
|
+
const UsagePage = lazy(() => import("./UsagePage--mg_NzrA.mjs"));
|
|
2602
339
|
function resolvePricingEnabled(config) {
|
|
2603
340
|
if (config.__meta?.pricingEnabled !== void 0) return config.__meta.pricingEnabled;
|
|
2604
341
|
return isPreviewPricingEnabled();
|
|
2605
342
|
}
|
|
2606
|
-
const devPortalBillingPlugin = createPlugin((options = {}) =>
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
343
|
+
const devPortalBillingPlugin = createPlugin((options = {}) => {
|
|
344
|
+
if (typeof window !== "undefined" && window.location.pathname.replace(/\/+$/, "") === PRICING_PATH) startPlansPrefetch(resolveDevPortalApiBaseUrl(options.apiBaseUrl));
|
|
345
|
+
return {
|
|
346
|
+
transformConfig: async ({ config, merge }) => {
|
|
347
|
+
const pricingEnabled = resolvePricingEnabled(config);
|
|
348
|
+
const authNavEnabled = !!config.authentication && await resolveAuthenticationNavEnabled(config);
|
|
349
|
+
const headerNavigation = buildDevPortalHeaderNavigation({
|
|
350
|
+
navigation: config.header?.navigation,
|
|
351
|
+
pricingEnabled,
|
|
352
|
+
authNavEnabled
|
|
353
|
+
});
|
|
354
|
+
return merge({
|
|
355
|
+
navigation: pricingEnabled ? withPricingNavigation(config.navigation) : withoutPricingNavigation(config.navigation),
|
|
356
|
+
header: {
|
|
357
|
+
...config.header,
|
|
358
|
+
navigation: headerNavigation
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
},
|
|
362
|
+
getProfileMenuItems: () => [{
|
|
363
|
+
label: "My Subscriptions",
|
|
364
|
+
path: SUBSCRIPTIONS_PATH,
|
|
365
|
+
icon: StarsIcon,
|
|
366
|
+
category: "middle",
|
|
367
|
+
weight: 25
|
|
368
|
+
}, {
|
|
369
|
+
label: "API credentials",
|
|
370
|
+
path: CREDENTIALS_PATH,
|
|
371
|
+
icon: KeyRoundIcon,
|
|
372
|
+
category: "middle",
|
|
373
|
+
weight: 26
|
|
374
|
+
}],
|
|
375
|
+
getRoutes: () => [{
|
|
376
|
+
element: /* @__PURE__ */ jsx(Suspense, {
|
|
377
|
+
fallback: null,
|
|
378
|
+
children: /* @__PURE__ */ jsx(DevPortalBillingWrapper, { options })
|
|
379
|
+
}),
|
|
380
|
+
handle: { layout: "none" },
|
|
381
|
+
children: [
|
|
382
|
+
{
|
|
383
|
+
path: PRICING_PATH,
|
|
384
|
+
handle: { layout: "default" },
|
|
385
|
+
element: /* @__PURE__ */ jsx(PricingPage, {})
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
path: CHECKOUT_PATH,
|
|
389
|
+
handle: { layout: "default" },
|
|
390
|
+
element: /* @__PURE__ */ jsx(CheckoutPage, {})
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
path: CONTACT_SALES_PATH,
|
|
394
|
+
handle: { layout: "default" },
|
|
395
|
+
element: /* @__PURE__ */ jsx(ContactSalesPage, {})
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
path: ACCOUNT_PLANS_PATH,
|
|
399
|
+
handle: { layout: "default" },
|
|
400
|
+
element: /* @__PURE__ */ jsx(AccountLayout, {
|
|
401
|
+
includeBillingNav: true,
|
|
402
|
+
children: /* @__PURE__ */ jsx(AccountPlansPage$1, {})
|
|
403
|
+
})
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
path: SUBSCRIPTIONS_PATH,
|
|
407
|
+
handle: { layout: "default" },
|
|
408
|
+
element: /* @__PURE__ */ jsx(AccountLayout, {
|
|
409
|
+
includeBillingNav: true,
|
|
410
|
+
children: /* @__PURE__ */ jsx(SubscriptionsPage, {})
|
|
411
|
+
})
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
path: CREDENTIALS_PATH,
|
|
415
|
+
handle: { layout: "default" },
|
|
416
|
+
element: /* @__PURE__ */ jsx(AccountLayout, {
|
|
417
|
+
includeBillingNav: true,
|
|
418
|
+
children: /* @__PURE__ */ jsx(CredentialsPage, {})
|
|
419
|
+
})
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
path: USAGE_PATH,
|
|
423
|
+
handle: { layout: "default" },
|
|
424
|
+
element: /* @__PURE__ */ jsx(AccountLayout, {
|
|
425
|
+
includeBillingNav: true,
|
|
426
|
+
children: /* @__PURE__ */ jsx(UsagePage, {})
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
]
|
|
430
|
+
}],
|
|
431
|
+
getProtectedRoutes: () => [
|
|
432
|
+
CHECKOUT_PATH,
|
|
433
|
+
ACCOUNT_PLANS_PATH,
|
|
434
|
+
SUBSCRIPTIONS_PATH,
|
|
435
|
+
CREDENTIALS_PATH,
|
|
436
|
+
USAGE_PATH
|
|
2687
437
|
]
|
|
2688
|
-
}
|
|
2689
|
-
|
|
2690
|
-
CHECKOUT_PATH,
|
|
2691
|
-
ACCOUNT_PLANS_PATH,
|
|
2692
|
-
SUBSCRIPTIONS_PATH,
|
|
2693
|
-
CREDENTIALS_PATH,
|
|
2694
|
-
USAGE_PATH
|
|
2695
|
-
]
|
|
2696
|
-
}));
|
|
438
|
+
};
|
|
439
|
+
});
|
|
2697
440
|
|
|
2698
441
|
//#endregion
|
|
2699
442
|
//#region src/hooks/usePlansPreview.ts
|