@lukoweb/apitogo-plugin-dev-portal-billing 0.2.31 → 0.2.35

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/index.mjs CHANGED
@@ -1,952 +1,24 @@
1
- import { Button, Head, Heading, Link, Spinner } from "@lukoweb/apitogo/components";
2
- import { ArrowRightIcon, CreditCardIcon, KeyRoundIcon, ShieldIcon, StarsIcon, TrashIcon } from "@lukoweb/apitogo/icons";
3
- import { Link as Link$1, Navigate, Outlet, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
4
- import { createContext, useContext, useEffect, useState } from "react";
5
- import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@lukoweb/apitogo/ui/AlertDialog.js";
6
- import { cn } from "@lukoweb/apitogo/ui/util.js";
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 zudokuConfig from "virtual:zudoku-config";
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 [searchParams] = useSearchParams();
588
- const checkoutCancelled = searchParams.get("checkout") === "cancelled";
589
- const { previewPlans } = useDevPortalBillingConfig();
590
- const isPreview = useDevPortalPreviewMode();
591
- const isDemoSession = useIsDemoSession();
592
- const livePlans = usePlans();
593
- const catalog = isPreview ? parseManifestPlans(previewPlans) : livePlans.data;
594
- const isLoading = isPreview ? false : livePlans.isLoading;
595
- const isError = isPreview ? false : livePlans.isError;
596
- const { data: me } = useMySubscriptions();
597
- const cancelMutation = useCancelSubscription();
598
- const changeMutation = useChangePlan();
599
- const [cancelingSku, setCancelingSku] = useState(null);
600
- const [switchingSku, setSwitchingSku] = useState(null);
601
- const subscriptionBySku = new Map((me?.subscriptions ?? []).map((sub) => [sub.planSku, sub]));
602
- const activePlanSkus = (me?.subscriptions ?? []).filter((sub) => isActiveSubscription(sub.status)).map((sub) => sub.planSku);
603
- const currentPlanSku = activePlanSkus.length === 1 ? activePlanSkus[0] : null;
604
- const handleSwitch = async (toPlanSku) => {
605
- if (!currentPlanSku) return;
606
- setSwitchingSku(toPlanSku);
607
- try {
608
- await changeMutation.mutateAsync({
609
- fromPlanSku: currentPlanSku,
610
- toPlanSku
611
- });
612
- } finally {
613
- setSwitchingSku(null);
614
- }
615
- };
616
- const handleUnsubscribe = async (planSku) => {
617
- setCancelingSku(planSku);
618
- try {
619
- await cancelMutation.mutateAsync(planSku);
620
- } finally {
621
- setCancelingSku(null);
622
- }
623
- };
624
- return /* @__PURE__ */ jsxs("div", {
625
- className: "mx-auto max-w-screen-xl px-4 pb-16 pt-(--padding-content-top) lg:px-8",
626
- children: [
627
- /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Plans & billing" }) }),
628
- /* @__PURE__ */ jsxs("div", {
629
- className: "mb-8 flex items-start gap-4",
630
- children: [/* @__PURE__ */ jsx("div", {
631
- className: "flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary",
632
- children: /* @__PURE__ */ jsx(CreditCardIcon, { size: 24 })
633
- }), /* @__PURE__ */ jsxs("div", {
634
- className: "space-y-1",
635
- children: [
636
- /* @__PURE__ */ jsx("h1", {
637
- className: "text-3xl font-semibold tracking-tight",
638
- children: "Plans & billing"
639
- }),
640
- /* @__PURE__ */ jsx("p", {
641
- className: "max-w-2xl text-muted-foreground",
642
- children: "Compare plans, manage your subscriptions, and change tiers."
643
- }),
644
- checkoutCancelled ? /* @__PURE__ */ jsx("p", {
645
- className: "max-w-2xl text-sm text-muted-foreground",
646
- children: "Checkout was cancelled and you were not charged."
647
- }) : null
648
- ]
649
- })]
650
- }),
651
- isDemoSession ? /* @__PURE__ */ jsx("div", {
652
- className: "mb-8 rounded-xl border border-dashed bg-muted/20 p-6 text-center text-sm text-muted-foreground",
653
- children: "Local demo mode shows plans from your manifest with a sample active subscription. Billing actions activate after publish."
654
- }) : null,
655
- cancelMutation.isError ? /* @__PURE__ */ jsx("div", {
656
- className: "mb-6 rounded-xl border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive",
657
- children: cancelMutation.error instanceof Error ? cancelMutation.error.message : "Failed to cancel subscription."
658
- }) : null,
659
- changeMutation.isError ? /* @__PURE__ */ jsx("div", {
660
- className: "mb-6 rounded-xl border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive",
661
- children: changeMutation.error instanceof Error ? changeMutation.error.message : "Failed to change plan."
662
- }) : null,
663
- isLoading ? /* @__PURE__ */ jsx("p", {
664
- className: "text-muted-foreground",
665
- children: "Loading plans…"
666
- }) : null,
667
- isError ? /* @__PURE__ */ jsx("p", {
668
- className: "text-destructive",
669
- children: "Unable to load plans."
670
- }) : null,
671
- catalog && catalog.plans.length > 0 ? /* @__PURE__ */ jsx("div", {
672
- className: "grid grid-cols-1 justify-center gap-6 sm:grid-cols-[repeat(auto-fit,minmax(280px,max-content))]",
673
- children: catalog.plans.map((plan) => {
674
- const subscription = subscriptionBySku.get(plan.sku);
675
- const isSubscribed = isActiveSubscription(subscription?.status ?? "");
676
- return /* @__PURE__ */ jsx(PricingCard, {
677
- plan,
678
- mode: "account",
679
- subscriptionStatus: subscription?.status,
680
- isSubscribed,
681
- onUnsubscribe: isPreview || isDemoSession ? void 0 : handleUnsubscribe,
682
- onSwitch: currentPlanSku && !isPreview && !isDemoSession ? handleSwitch : void 0,
683
- isSwitching: switchingSku === plan.sku,
684
- isCanceling: cancelingSku === plan.sku,
685
- cancelDisabled: isPreview || isDemoSession
686
- }, plan.sku);
687
- })
688
- }) : !isLoading && !isError ? /* @__PURE__ */ jsx("div", {
689
- className: "rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
690
- children: "No plans are available yet. Sync plans from your API ToGo manifest or platform gateway."
691
- }) : null
692
- ]
693
- });
694
- };
695
-
696
- //#endregion
697
- //#region src/hooks/useApiKeys.ts
698
- /** Thrown when the developer has hit their plan's per-developer key cap. */
699
- const API_KEY_CAP_REACHED = "API_KEY_CAP_REACHED";
700
- function apiKeysQueryKey(apiBaseUrl) {
701
- return ["dev-portal-api-keys", apiBaseUrl];
702
- }
703
- function useApiKeys() {
704
- const auth = useAuth();
705
- const apiBaseUrl = useDevPortalApiBaseUrl();
706
- const isPreview = useDevPortalPreviewMode();
707
- const isDemoSession = useIsDemoSession();
708
- const { previewPlans } = useDevPortalBillingConfig();
709
- const liveQuery = useQuery({
710
- queryKey: apiKeysQueryKey(apiBaseUrl),
711
- enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
712
- queryFn: async () => {
713
- const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
714
- method: "GET",
715
- headers: { Accept: "application/json" },
716
- credentials: "include"
717
- });
718
- if (response.status === 404) throw new Error("NO_ENTITLEMENT");
719
- if (!response.ok) throw new Error("Failed to load API keys.");
720
- return await response.json();
721
- },
722
- retry: (failureCount, error) => error instanceof Error && error.message === "NO_ENTITLEMENT" ? false : failureCount < 2
723
- });
724
- if (isDemoSession && auth.isAuthenticated) return {
725
- ...liveQuery,
726
- data: createDemoApiKeys(previewPlans),
727
- error: null,
728
- isError: false,
729
- isFetching: false,
730
- isLoading: false,
731
- isPending: false,
732
- isSuccess: true,
733
- status: "success"
734
- };
735
- return liveQuery;
736
- }
737
- function useCreateApiKey() {
738
- const apiBaseUrl = useDevPortalApiBaseUrl();
739
- const queryClient = useQueryClient();
740
- return useMutation({
741
- mutationFn: async ({ name, planSku }) => {
742
- if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
743
- const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
744
- method: "POST",
745
- headers: {
746
- Accept: "application/json",
747
- "Content-Type": "application/json"
748
- },
749
- credentials: "include",
750
- body: JSON.stringify({
751
- name,
752
- planSku
753
- })
754
- });
755
- if (response.status === 409) throw new Error(API_KEY_CAP_REACHED);
756
- if (!response.ok) throw new Error(await readProblemDetail$1(response) ?? "Failed to create API key.");
757
- return await response.json();
758
- },
759
- onSuccess: async () => {
760
- await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
761
- }
762
- });
763
- }
764
- function useRevokeApiKey() {
765
- const apiBaseUrl = useDevPortalApiBaseUrl();
766
- const queryClient = useQueryClient();
767
- return useMutation({
768
- mutationFn: async (keyId) => {
769
- if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
770
- const response = await fetch(`${apiBaseUrl}/api/v1/api-keys/${keyId}`, {
771
- method: "DELETE",
772
- headers: { Accept: "application/json" },
773
- credentials: "include"
774
- });
775
- if (!response.ok && response.status !== 204) throw new Error(await readProblemDetail$1(response) ?? "Failed to revoke API key.");
776
- },
777
- onSuccess: async () => {
778
- await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
779
- }
780
- });
781
- }
782
- async function readProblemDetail$1(response) {
783
- try {
784
- return (await response.json()).detail?.trim() || null;
785
- } catch {
786
- return null;
787
- }
788
- }
789
-
790
- //#endregion
791
- //#region src/hooks/useUsage.ts
792
- function createDemoUsage(range) {
793
- const now = /* @__PURE__ */ new Date();
794
- const days = range === "24h" ? 1 : range === "7d" ? 7 : range === "90d" ? 90 : range === "mtd" ? Math.max(1, now.getUTCDate()) : 30;
795
- const series = [];
796
- let total = 0;
797
- const today = /* @__PURE__ */ new Date();
798
- for (let i = days - 1; i >= 0; i--) {
799
- const day = new Date(today);
800
- day.setUTCDate(day.getUTCDate() - i);
801
- const requestCount = 40 + i * 17 % 80;
802
- total += requestCount;
803
- series.push({
804
- dayUtc: day.toISOString().slice(0, 10),
805
- requestCount,
806
- errorCount: Math.floor(requestCount * .02),
807
- avgLatencyMs: 40 + i % 20
808
- });
809
- }
810
- return {
811
- range,
812
- fromDayUtc: series[0]?.dayUtc ?? today.toISOString().slice(0, 10),
813
- toDayUtc: series.at(-1)?.dayUtc ?? today.toISOString().slice(0, 10),
814
- requestCount: total,
815
- avgLatencyMs: 48,
816
- errorRate: .018,
817
- series,
818
- byEndpoint: [
819
- {
820
- path: "GET /pets",
821
- count: Math.floor(total * .45)
822
- },
823
- {
824
- path: "POST /pets",
825
- count: Math.floor(total * .25)
826
- },
827
- {
828
- path: "GET /pets/{id}",
829
- count: Math.floor(total * .2)
830
- },
831
- {
832
- path: "DELETE /pets/{id}",
833
- count: Math.floor(total * .1)
834
- }
835
- ],
836
- byStatus: [
837
- {
838
- label: "2xx",
839
- count: Math.floor(total * .96)
840
- },
841
- {
842
- label: "4xx",
843
- count: Math.floor(total * .03)
844
- },
845
- {
846
- label: "5xx",
847
- count: Math.floor(total * .01)
848
- }
849
- ]
850
- };
851
- }
852
- function createDemoRecent() {
853
- const now = Date.now();
854
- return { items: [
855
- {
856
- occurredAtUtc: (/* @__PURE__ */ new Date(now - 6e4)).toISOString(),
857
- method: "GET",
858
- path: "/pets",
859
- statusCode: 200,
860
- latencyMs: 42
861
- },
862
- {
863
- occurredAtUtc: (/* @__PURE__ */ new Date(now - 12e4)).toISOString(),
864
- method: "POST",
865
- path: "/pets",
866
- statusCode: 201,
867
- latencyMs: 88
868
- },
869
- {
870
- occurredAtUtc: (/* @__PURE__ */ new Date(now - 18e4)).toISOString(),
871
- method: "GET",
872
- path: "/pets/1",
873
- statusCode: 404,
874
- latencyMs: 31
875
- }
876
- ] };
877
- }
878
- function useUsage(range = "30d") {
879
- const auth = useAuth();
880
- const apiBaseUrl = useDevPortalApiBaseUrl();
881
- const isPreview = useDevPortalPreviewMode();
882
- const isDemoSession = useIsDemoSession();
883
- const liveQuery = useQuery({
884
- queryKey: [
885
- "dev-portal-usage",
886
- apiBaseUrl,
887
- range
888
- ],
889
- enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
890
- queryFn: async () => {
891
- const response = await fetch(`${apiBaseUrl}/api/v1/usage?range=${encodeURIComponent(range)}`, {
892
- method: "GET",
893
- headers: { Accept: "application/json" },
894
- credentials: "include"
895
- });
896
- if (!response.ok) throw new Error("Failed to load usage.");
897
- return await response.json();
898
- }
899
- });
900
- if (isDemoSession && auth.isAuthenticated) return {
901
- ...liveQuery,
902
- data: createDemoUsage(range),
903
- error: null,
904
- isError: false,
905
- isFetching: false,
906
- isLoading: false,
907
- isPending: false,
908
- isSuccess: true,
909
- status: "success"
910
- };
911
- return liveQuery;
912
- }
913
- function useUsageRecent(limit = 20) {
914
- const auth = useAuth();
915
- const apiBaseUrl = useDevPortalApiBaseUrl();
916
- const isPreview = useDevPortalPreviewMode();
917
- const isDemoSession = useIsDemoSession();
918
- const liveQuery = useQuery({
919
- queryKey: [
920
- "dev-portal-usage-recent",
921
- apiBaseUrl,
922
- limit
923
- ],
924
- enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
925
- queryFn: async () => {
926
- const response = await fetch(`${apiBaseUrl}/api/v1/usage/recent?limit=${limit}`, {
927
- method: "GET",
928
- headers: { Accept: "application/json" },
929
- credentials: "include"
930
- });
931
- if (!response.ok) throw new Error("Failed to load recent requests.");
932
- return await response.json();
933
- }
934
- });
935
- if (isDemoSession && auth.isAuthenticated) return {
936
- ...liveQuery,
937
- data: createDemoRecent(),
938
- error: null,
939
- isError: false,
940
- isFetching: false,
941
- isLoading: false,
942
- isPending: false,
943
- isSuccess: true,
944
- status: "success"
945
- };
946
- return liveQuery;
947
- }
948
-
949
- //#endregion
950
22
  //#region src/pages/OverviewDashboardPage.tsx
951
23
  function StatCard({ label, value, sub, subClassName }) {
952
24
  return /* @__PURE__ */ jsxs("div", {
@@ -967,7 +39,7 @@ function StatCard({ label, value, sub, subClassName }) {
967
39
  ]
968
40
  });
969
41
  }
970
- function formatNumber$1(value) {
42
+ function formatNumber(value) {
971
43
  return new Intl.NumberFormat().format(value);
972
44
  }
973
45
  const OverviewDashboardPage = () => {
@@ -1001,8 +73,8 @@ const OverviewDashboardPage = () => {
1001
73
  children: [
1002
74
  /* @__PURE__ */ jsx(StatCard, {
1003
75
  label: "Requests (30d)",
1004
- value: usage ? formatNumber$1(usage.requestCount) : statsLoading ? "…" : "—",
1005
- sub: usage ? `${formatNumber$1(usage.series.reduce((sum, p) => sum + p.errorCount, 0))} errors` : "No usage data yet"
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"
1006
78
  }),
1007
79
  /* @__PURE__ */ jsx(StatCard, {
1008
80
  label: "Active keys",
@@ -1166,181 +238,6 @@ const OverviewDashboardPage = () => {
1166
238
  });
1167
239
  };
1168
240
 
1169
- //#endregion
1170
- //#region src/components/ApiKeysPanel.tsx
1171
- /**
1172
- * Lists gateway API keys for one plan with create and revoke. The plaintext key is returned by the
1173
- * platform exactly once, so it is held in local state until dismissed and never re-fetched.
1174
- */
1175
- const ApiKeysPanel = ({ plan, header, readOnly = false }) => {
1176
- const createKey = useCreateApiKey();
1177
- const revokeKey = useRevokeApiKey();
1178
- const [name, setName] = useState("");
1179
- const [newKey, setNewKey] = useState(null);
1180
- const [openRevokeId, setOpenRevokeId] = useState(null);
1181
- const [pendingRevokeId, setPendingRevokeId] = useState(null);
1182
- const activeKeys = plan.keys.filter((key) => key.status === "active");
1183
- const planActiveCount = activeKeys.length;
1184
- const capLabel = plan.maxKeys == null ? `${plan.activeCount} active` : `${plan.activeCount} of ${plan.maxKeys} used`;
1185
- const canCreate = !readOnly && plan.canCreate;
1186
- const handleCreate = () => {
1187
- const trimmed = name.trim();
1188
- if (!trimmed) return;
1189
- createKey.mutate({
1190
- name: trimmed,
1191
- planSku: plan.planSku
1192
- }, { onSuccess: (key) => {
1193
- setNewKey(key);
1194
- setName("");
1195
- } });
1196
- };
1197
- const handleRevoke = (keyId) => {
1198
- setPendingRevokeId(keyId);
1199
- revokeKey.mutate(keyId, { onSettled: () => {
1200
- setPendingRevokeId(null);
1201
- setOpenRevokeId(null);
1202
- } });
1203
- };
1204
- return /* @__PURE__ */ jsxs("article", {
1205
- className: "rounded-xl border bg-card/60 p-6 space-y-5",
1206
- children: [
1207
- /* @__PURE__ */ jsxs("div", {
1208
- className: "flex flex-wrap items-start justify-between gap-3",
1209
- children: [/* @__PURE__ */ jsxs("div", {
1210
- className: "space-y-1",
1211
- children: [/* @__PURE__ */ jsxs("h2", {
1212
- className: "text-lg font-medium",
1213
- children: [plan.displayName, " API keys"]
1214
- }), /* @__PURE__ */ jsxs("p", {
1215
- className: "text-sm text-muted-foreground",
1216
- children: [
1217
- "Send a key in the",
1218
- " ",
1219
- /* @__PURE__ */ jsx("code", {
1220
- className: "rounded bg-muted px-1 py-0.5 text-xs",
1221
- children: header
1222
- }),
1223
- " ",
1224
- "header when calling this plan's endpoints. Keys share this plan's rate limit and key cap."
1225
- ]
1226
- })]
1227
- }), /* @__PURE__ */ jsx("span", {
1228
- className: "shrink-0 rounded-full border px-3 py-1 text-xs text-muted-foreground",
1229
- children: capLabel
1230
- })]
1231
- }),
1232
- newKey?.key ? /* @__PURE__ */ jsxs(Alert, { children: [
1233
- /* @__PURE__ */ jsx(KeyRoundIcon, { size: 16 }),
1234
- /* @__PURE__ */ jsx(AlertTitle, { children: "Copy your new key now — it is shown only once." }),
1235
- /* @__PURE__ */ jsxs("div", {
1236
- className: "mt-3 space-y-3",
1237
- children: [/* @__PURE__ */ jsx(Secret, {
1238
- className: "max-w-full w-full",
1239
- secret: newKey.key,
1240
- status: "active"
1241
- }), /* @__PURE__ */ jsx(Button$1, {
1242
- variant: "outline",
1243
- onClick: () => setNewKey(null),
1244
- children: "Done"
1245
- })]
1246
- })
1247
- ] }) : null,
1248
- createKey.isError ? /* @__PURE__ */ jsx(Alert, {
1249
- variant: "destructive",
1250
- 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 })
1251
- }) : null,
1252
- revokeKey.isError ? /* @__PURE__ */ jsx(Alert, {
1253
- variant: "destructive",
1254
- children: /* @__PURE__ */ jsx(AlertTitle, { children: revokeKey.error.message })
1255
- }) : null,
1256
- planActiveCount === 0 ? /* @__PURE__ */ jsx("p", {
1257
- className: "text-sm text-muted-foreground",
1258
- children: "You have no active API keys for this plan yet."
1259
- }) : null,
1260
- activeKeys.length > 0 ? /* @__PURE__ */ jsx("ul", {
1261
- className: "divide-y rounded-lg border",
1262
- children: activeKeys.map((key) => {
1263
- const isRevoking = pendingRevokeId === key.id;
1264
- return /* @__PURE__ */ jsxs("li", {
1265
- className: "flex flex-wrap items-center justify-between gap-3 p-4",
1266
- children: [/* @__PURE__ */ jsxs("div", {
1267
- className: "min-w-0 space-y-1",
1268
- children: [/* @__PURE__ */ jsx("p", {
1269
- className: "truncate font-medium",
1270
- children: key.name
1271
- }), /* @__PURE__ */ jsxs("p", {
1272
- className: "text-xs text-muted-foreground",
1273
- children: [
1274
- /* @__PURE__ */ jsxs("code", { children: [key.keyPrefix, "…"] }),
1275
- " · created",
1276
- " ",
1277
- new Date(key.createdAtUtc).toLocaleDateString()
1278
- ]
1279
- })]
1280
- }), readOnly ? null : /* @__PURE__ */ jsxs(Dialog, {
1281
- open: openRevokeId === key.id,
1282
- onOpenChange: (open) => {
1283
- if (isRevoking) return;
1284
- setOpenRevokeId(open ? key.id : null);
1285
- },
1286
- children: [/* @__PURE__ */ jsx(DialogTrigger, {
1287
- asChild: true,
1288
- children: /* @__PURE__ */ jsxs(Button$1, {
1289
- variant: "ghost",
1290
- className: "flex items-center gap-2",
1291
- disabled: isRevoking,
1292
- children: [isRevoking ? /* @__PURE__ */ jsx(Spinner, { size: 14 }) : /* @__PURE__ */ jsx(TrashIcon, { size: 16 }), /* @__PURE__ */ jsx("span", {
1293
- className: "hidden md:block",
1294
- children: isRevoking ? "Revoking…" : "Revoke"
1295
- })]
1296
- })
1297
- }), /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, { children: [
1298
- "Revoke “",
1299
- key.name,
1300
- "”?"
1301
- ] }), /* @__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, {
1302
- variant: "outline",
1303
- disabled: isRevoking,
1304
- onClick: () => setOpenRevokeId(null),
1305
- children: "Cancel"
1306
- }), /* @__PURE__ */ jsx(Button$1, {
1307
- variant: "destructive",
1308
- disabled: isRevoking,
1309
- onClick: () => handleRevoke(key.id),
1310
- children: isRevoking ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { size: 14 }), "Revoking…"] }) : "Revoke key"
1311
- })] })] })]
1312
- })]
1313
- }, key.id);
1314
- })
1315
- }) : null,
1316
- readOnly ? null : /* @__PURE__ */ jsxs("div", {
1317
- className: "flex flex-wrap items-center gap-2",
1318
- children: [/* @__PURE__ */ jsx(Input, {
1319
- className: "max-w-xs",
1320
- maxLength: 64,
1321
- placeholder: "Key name (e.g. production)",
1322
- "aria-label": `New API key name for ${plan.displayName}`,
1323
- value: name,
1324
- disabled: !canCreate || createKey.isPending,
1325
- onChange: (event) => setName(event.target.value),
1326
- onKeyDown: (event) => {
1327
- if (event.key === "Enter") handleCreate();
1328
- }
1329
- }), /* @__PURE__ */ jsx(Button$1, {
1330
- onClick: handleCreate,
1331
- disabled: !canCreate || createKey.isPending || !name.trim(),
1332
- className: "flex items-center gap-2",
1333
- children: createKey.isPending ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { size: 14 }), "Creating…"] }) : "Create API key"
1334
- })]
1335
- }),
1336
- plan && !plan.canCreate && !readOnly ? /* @__PURE__ */ jsx("p", {
1337
- className: "text-sm text-muted-foreground",
1338
- 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.`
1339
- }) : null
1340
- ]
1341
- });
1342
- };
1343
-
1344
241
  //#endregion
1345
242
  //#region src/DevPortalBillingWrapper.tsx
1346
243
  const DevPortalBillingWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(DevPortalBillingProvider, {
@@ -1348,1197 +245,6 @@ const DevPortalBillingWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(DevPor
1348
245
  children: /* @__PURE__ */ jsx(Outlet, {})
1349
246
  });
1350
247
 
1351
- //#endregion
1352
- //#region src/components/RedirectPage.tsx
1353
- const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
1354
- useEffect(() => {
1355
- if (url) window.location.assign(url);
1356
- }, [url]);
1357
- return /* @__PURE__ */ jsx("div", {
1358
- className: "flex min-h-screen items-center justify-center bg-muted",
1359
- children: /* @__PURE__ */ jsxs("div", {
1360
- className: "flex max-w-md flex-col items-center space-y-6 text-center",
1361
- children: [
1362
- /* @__PURE__ */ jsx("div", {
1363
- className: "relative",
1364
- children: /* @__PURE__ */ jsx("div", {
1365
- className: "flex h-24 w-24 items-center justify-center rounded-full bg-foreground/10",
1366
- children: /* @__PURE__ */ jsx(Icon, { className: "h-12 w-12 text-foreground" })
1367
- })
1368
- }),
1369
- children,
1370
- /* @__PURE__ */ jsxs("div", {
1371
- className: "space-y-2",
1372
- children: [/* @__PURE__ */ jsx("h2", {
1373
- className: "text-2xl font-bold text-card-foreground",
1374
- children: title
1375
- }), /* @__PURE__ */ jsx("p", {
1376
- className: "text-muted-foreground",
1377
- children: description
1378
- })]
1379
- }),
1380
- /* @__PURE__ */ jsxs("div", {
1381
- className: "flex space-x-2",
1382
- children: [
1383
- /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.3s]" }),
1384
- /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.15s]" }),
1385
- /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary" })
1386
- ]
1387
- })
1388
- ]
1389
- })
1390
- });
1391
- };
1392
-
1393
- //#endregion
1394
- //#region src/pages/CheckoutPage.tsx
1395
- const CheckoutPage = () => {
1396
- const [searchParams] = useSearchParams();
1397
- const planSku = searchParams.get("planSku") ?? "";
1398
- const navigate = useNavigate();
1399
- const auth = useAuth();
1400
- const apiBaseUrl = useDevPortalApiBaseUrl();
1401
- const isPreview = useDevPortalPreviewMode();
1402
- const isDemoSession = useIsDemoSession();
1403
- const plansPath = auth.isAuthenticated ? ACCOUNT_PLANS_PATH : PRICING_PATH;
1404
- const checkoutQuery = useQuery({
1405
- queryKey: [
1406
- "dev-portal-checkout",
1407
- apiBaseUrl,
1408
- planSku,
1409
- auth.profile?.sub
1410
- ],
1411
- enabled: Boolean(planSku) && !isPreview && !isDemoSession && auth.isAuthenticated && Boolean(apiBaseUrl),
1412
- retry: false,
1413
- queryFn: async () => {
1414
- const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/checkout`, {
1415
- method: "POST",
1416
- headers: {
1417
- Accept: "application/json",
1418
- "Content-Type": "application/json"
1419
- },
1420
- credentials: "include",
1421
- body: JSON.stringify({ planSku })
1422
- });
1423
- if (!response.ok) {
1424
- const detail = await response.text();
1425
- throw new Error(detail || "Checkout failed.");
1426
- }
1427
- return await response.json();
1428
- }
1429
- });
1430
- const freeProvisioned = checkoutQuery.data?.checkoutUrl == null && checkoutQuery.data?.entitlementProvisioned === true;
1431
- useEffect(() => {
1432
- if (freeProvisioned) navigate("/account/subscriptions?checkout=success", { replace: true });
1433
- }, [freeProvisioned, navigate]);
1434
- if (!planSku) return /* @__PURE__ */ jsxs("div", {
1435
- className: "mx-auto max-w-lg px-4 py-16 text-center text-muted-foreground",
1436
- children: [
1437
- "Missing plan SKU.",
1438
- " ",
1439
- /* @__PURE__ */ jsx(Link$1, {
1440
- to: plansPath,
1441
- className: "text-primary underline",
1442
- children: "Back to plans"
1443
- })
1444
- ]
1445
- });
1446
- if (isDemoSession) return /* @__PURE__ */ jsxs("div", {
1447
- className: "mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center",
1448
- children: [
1449
- /* @__PURE__ */ jsx("h1", {
1450
- className: "text-2xl font-semibold tracking-tight",
1451
- children: "Demo checkout"
1452
- }),
1453
- /* @__PURE__ */ jsx("p", {
1454
- className: "text-muted-foreground",
1455
- children: "Checkout is simulated in local demo mode. Publish your portal to API ToGo to complete real subscriptions and billing."
1456
- }),
1457
- /* @__PURE__ */ jsx(Button$2, {
1458
- asChild: true,
1459
- variant: "outline",
1460
- children: /* @__PURE__ */ jsx(Link$1, {
1461
- to: "/account/subscriptions",
1462
- children: "View demo subscriptions"
1463
- })
1464
- })
1465
- ]
1466
- });
1467
- if (isPreview) return /* @__PURE__ */ jsxs("div", {
1468
- className: "mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center",
1469
- children: [
1470
- /* @__PURE__ */ jsx("h1", {
1471
- className: "text-2xl font-semibold tracking-tight",
1472
- children: "Checkout is temporarily unavailable"
1473
- }),
1474
- /* @__PURE__ */ jsx("p", {
1475
- className: "text-muted-foreground",
1476
- children: "We couldn’t reach the developer portal API. Try again shortly, or contact the site owner if this continues."
1477
- }),
1478
- /* @__PURE__ */ jsx(Button$2, {
1479
- asChild: true,
1480
- variant: "outline",
1481
- children: /* @__PURE__ */ jsx(Link$1, {
1482
- to: plansPath,
1483
- children: "Back to plans"
1484
- })
1485
- })
1486
- ]
1487
- });
1488
- if (freeProvisioned) return null;
1489
- return /* @__PURE__ */ jsx(RedirectPage, {
1490
- icon: ShieldIcon,
1491
- title: "Establishing secure checkout…",
1492
- description: "Redirecting you to complete your subscription.",
1493
- url: checkoutQuery.data?.checkoutUrl ?? void 0,
1494
- children: checkoutQuery.isError ? /* @__PURE__ */ jsxs(Alert$1, {
1495
- variant: "destructive",
1496
- children: [/* @__PURE__ */ jsx(AlertDescription, {
1497
- className: "first-letter:uppercase",
1498
- children: checkoutQuery.error.message
1499
- }), /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(Button$2, {
1500
- variant: "outline",
1501
- size: "xs",
1502
- asChild: true,
1503
- children: /* @__PURE__ */ jsx(Link$1, {
1504
- to: plansPath,
1505
- children: "Back to plans"
1506
- })
1507
- }) })]
1508
- }) : null
1509
- });
1510
- };
1511
-
1512
- //#endregion
1513
- //#region src/hooks/usePlanInquiry.ts
1514
- const PLAN_INQUIRY_RATE_LIMITED = "Too many messages were sent from your network. Please try again later.";
1515
- function useSubmitPlanInquiry() {
1516
- const apiBaseUrl = useDevPortalApiBaseUrl();
1517
- return useMutation({ mutationFn: async ({ planSku, inquiry }) => {
1518
- if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
1519
- const response = await fetch(`${apiBaseUrl}/api/v1/plans/${encodeURIComponent(planSku)}/inquiries`, {
1520
- method: "POST",
1521
- headers: {
1522
- Accept: "application/json",
1523
- "Content-Type": "application/json"
1524
- },
1525
- credentials: "include",
1526
- body: JSON.stringify(inquiry)
1527
- });
1528
- if (response.status === 429) throw new Error(PLAN_INQUIRY_RATE_LIMITED);
1529
- if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Your message could not be sent. Please try again later.");
1530
- } });
1531
- }
1532
- async function readProblemDetail(response) {
1533
- try {
1534
- const body = await response.json();
1535
- return (body.errors ? Object.values(body.errors).flat()[0] : void 0) ?? body.detail ?? body.title ?? null;
1536
- } catch {
1537
- return null;
1538
- }
1539
- }
1540
-
1541
- //#endregion
1542
- //#region src/pages/ContactSalesPage.tsx
1543
- const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1544
- const validate = (form) => ({
1545
- ...form.name.trim() ? {} : { name: "Enter your name." },
1546
- ...EMAIL_PATTERN.test(form.email.trim()) ? {} : { email: "Enter a valid email address." },
1547
- ...form.message.trim() ? {} : { message: "Tell us what you need." }
1548
- });
1549
- const ContactSalesPage = () => {
1550
- const [searchParams] = useSearchParams();
1551
- const planSku = searchParams.get("planSku") ?? "";
1552
- const auth = useAuth();
1553
- const isPreview = useDevPortalPreviewMode();
1554
- const isDemoSession = useIsDemoSession();
1555
- const plans = usePlans();
1556
- const submit = useSubmitPlanInquiry();
1557
- const [form, setForm] = useState(() => ({
1558
- name: auth.profile?.name ?? "",
1559
- email: auth.profile?.email ?? "",
1560
- company: "",
1561
- message: "",
1562
- website: ""
1563
- }));
1564
- const [errors, setErrors] = useState({});
1565
- const plan = plans.data?.plans.find((p) => p.sku === planSku);
1566
- const isSimulated = isPreview || isDemoSession;
1567
- if (!planSku || plans.isSuccess && (!plan || !isPlanCustomPriced(plan))) return /* @__PURE__ */ jsxs("div", {
1568
- className: "mx-auto max-w-lg px-4 py-16 text-center text-muted-foreground",
1569
- children: [
1570
- "This plan is not available for sales inquiries.",
1571
- " ",
1572
- /* @__PURE__ */ jsx(Link$1, {
1573
- to: PRICING_PATH,
1574
- className: "text-primary underline",
1575
- children: "Back to pricing"
1576
- })
1577
- ]
1578
- });
1579
- if (submit.isSuccess) return /* @__PURE__ */ jsxs("div", {
1580
- className: "mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center",
1581
- children: [
1582
- /* @__PURE__ */ jsx("h1", {
1583
- className: "text-2xl font-semibold tracking-tight",
1584
- children: "Thanks, we got your message"
1585
- }),
1586
- /* @__PURE__ */ jsxs("p", {
1587
- className: "text-muted-foreground",
1588
- children: [
1589
- "The team will reply to ",
1590
- form.email.trim(),
1591
- " soon."
1592
- ]
1593
- }),
1594
- /* @__PURE__ */ jsx(Button$2, {
1595
- asChild: true,
1596
- variant: "outline",
1597
- children: /* @__PURE__ */ jsx(Link$1, {
1598
- to: PRICING_PATH,
1599
- children: "Back to pricing"
1600
- })
1601
- })
1602
- ]
1603
- });
1604
- const update = (field) => (event) => setForm((prev) => ({
1605
- ...prev,
1606
- [field]: event.target.value
1607
- }));
1608
- const handleSubmit = (event) => {
1609
- event.preventDefault();
1610
- const nextErrors = validate(form);
1611
- setErrors(nextErrors);
1612
- if (Object.keys(nextErrors).length > 0 || isSimulated) return;
1613
- submit.mutate({
1614
- planSku,
1615
- inquiry: {
1616
- name: form.name.trim(),
1617
- email: form.email.trim(),
1618
- company: form.company.trim() || void 0,
1619
- message: form.message.trim(),
1620
- website: form.website || void 0
1621
- }
1622
- });
1623
- };
1624
- return /* @__PURE__ */ jsxs("div", {
1625
- className: "mx-auto w-full max-w-lg px-4 py-12",
1626
- children: [
1627
- /* @__PURE__ */ jsx("h1", {
1628
- className: "text-2xl font-semibold tracking-tight",
1629
- children: "Contact sales"
1630
- }),
1631
- /* @__PURE__ */ jsx("p", {
1632
- className: "mt-2 text-muted-foreground",
1633
- 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."
1634
- }),
1635
- isSimulated ? /* @__PURE__ */ jsxs(Alert$1, {
1636
- className: "mt-6",
1637
- children: [/* @__PURE__ */ jsx(AlertTitle$1, { children: "Preview only" }), /* @__PURE__ */ jsx(AlertDescription, { children: "Messages are sent once your portal is published to API ToGo." })]
1638
- }) : null,
1639
- /* @__PURE__ */ jsxs("form", {
1640
- className: "mt-6 space-y-4",
1641
- noValidate: true,
1642
- onSubmit: handleSubmit,
1643
- children: [
1644
- /* @__PURE__ */ jsxs("div", {
1645
- className: "space-y-1.5",
1646
- children: [
1647
- /* @__PURE__ */ jsx(Label, {
1648
- htmlFor: "contact-name",
1649
- children: "Name"
1650
- }),
1651
- /* @__PURE__ */ jsx(Input$1, {
1652
- id: "contact-name",
1653
- autoComplete: "name",
1654
- maxLength: 200,
1655
- value: form.name,
1656
- onChange: update("name"),
1657
- "aria-invalid": Boolean(errors.name)
1658
- }),
1659
- errors.name ? /* @__PURE__ */ jsx("p", {
1660
- className: "text-sm text-destructive",
1661
- children: errors.name
1662
- }) : null
1663
- ]
1664
- }),
1665
- /* @__PURE__ */ jsxs("div", {
1666
- className: "space-y-1.5",
1667
- children: [
1668
- /* @__PURE__ */ jsx(Label, {
1669
- htmlFor: "contact-email",
1670
- children: "Work email"
1671
- }),
1672
- /* @__PURE__ */ jsx(Input$1, {
1673
- id: "contact-email",
1674
- type: "email",
1675
- autoComplete: "email",
1676
- maxLength: 320,
1677
- value: form.email,
1678
- onChange: update("email"),
1679
- "aria-invalid": Boolean(errors.email)
1680
- }),
1681
- errors.email ? /* @__PURE__ */ jsx("p", {
1682
- className: "text-sm text-destructive",
1683
- children: errors.email
1684
- }) : null
1685
- ]
1686
- }),
1687
- /* @__PURE__ */ jsxs("div", {
1688
- className: "space-y-1.5",
1689
- children: [/* @__PURE__ */ jsx(Label, {
1690
- htmlFor: "contact-company",
1691
- children: "Company (optional)"
1692
- }), /* @__PURE__ */ jsx(Input$1, {
1693
- id: "contact-company",
1694
- autoComplete: "organization",
1695
- maxLength: 200,
1696
- value: form.company,
1697
- onChange: update("company")
1698
- })]
1699
- }),
1700
- /* @__PURE__ */ jsxs("div", {
1701
- className: "space-y-1.5",
1702
- children: [
1703
- /* @__PURE__ */ jsx(Label, {
1704
- htmlFor: "contact-message",
1705
- children: "Message"
1706
- }),
1707
- /* @__PURE__ */ jsx(Textarea, {
1708
- id: "contact-message",
1709
- rows: 5,
1710
- maxLength: 5e3,
1711
- placeholder: "Expected traffic, use case, timeline…",
1712
- value: form.message,
1713
- onChange: update("message"),
1714
- "aria-invalid": Boolean(errors.message)
1715
- }),
1716
- errors.message ? /* @__PURE__ */ jsx("p", {
1717
- className: "text-sm text-destructive",
1718
- children: errors.message
1719
- }) : null
1720
- ]
1721
- }),
1722
- /* @__PURE__ */ jsxs("div", {
1723
- className: "absolute -left-[9999px]",
1724
- "aria-hidden": "true",
1725
- children: [/* @__PURE__ */ jsx("label", {
1726
- htmlFor: "contact-website",
1727
- children: "Website"
1728
- }), /* @__PURE__ */ jsx("input", {
1729
- id: "contact-website",
1730
- tabIndex: -1,
1731
- autoComplete: "off",
1732
- value: form.website,
1733
- onChange: update("website")
1734
- })]
1735
- }),
1736
- submit.isError ? /* @__PURE__ */ jsx(Alert$1, {
1737
- variant: "destructive",
1738
- children: /* @__PURE__ */ jsx(AlertDescription, { children: submit.error.message })
1739
- }) : null,
1740
- /* @__PURE__ */ jsx(Button$2, {
1741
- type: "submit",
1742
- className: "w-full",
1743
- disabled: submit.isPending || isSimulated,
1744
- children: submit.isPending ? "Sending…" : "Send message"
1745
- })
1746
- ]
1747
- })
1748
- ]
1749
- });
1750
- };
1751
-
1752
- //#endregion
1753
- //#region src/hooks/useCredentials.ts
1754
- function useCredentials() {
1755
- const auth = useAuth();
1756
- const apiBaseUrl = useDevPortalApiBaseUrl();
1757
- const isPreview = useDevPortalPreviewMode();
1758
- const isDemoSession = useIsDemoSession();
1759
- const { previewPlans } = useDevPortalBillingConfig();
1760
- const liveQuery = useQuery({
1761
- queryKey: ["dev-portal-credentials", apiBaseUrl],
1762
- enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
1763
- queryFn: async () => {
1764
- const response = await fetch(`${apiBaseUrl}/api/v1/credentials`, {
1765
- method: "GET",
1766
- headers: { Accept: "application/json" },
1767
- credentials: "include"
1768
- });
1769
- if (response.status === 404) throw new Error("NO_ENTITLEMENT");
1770
- if (!response.ok) throw new Error("Failed to load API credentials.");
1771
- return await response.json();
1772
- },
1773
- retry: (failureCount, error) => error instanceof Error && error.message === "NO_ENTITLEMENT" ? false : failureCount < 2
1774
- });
1775
- if (isDemoSession && auth.isAuthenticated) return {
1776
- ...liveQuery,
1777
- data: createDemoCredentials(previewPlans),
1778
- error: null,
1779
- isError: false,
1780
- isFetching: false,
1781
- isLoading: false,
1782
- isPending: false,
1783
- isSuccess: true,
1784
- status: "success"
1785
- };
1786
- return liveQuery;
1787
- }
1788
-
1789
- //#endregion
1790
- //#region src/pages/CredentialsPage.tsx
1791
- const CredentialsPage = () => {
1792
- const auth = useAuth();
1793
- const isPreview = useDevPortalPreviewMode();
1794
- const isDemoSession = useIsDemoSession();
1795
- const { data: credentials, isLoading, error } = useCredentials();
1796
- const { data: apiKeys, isLoading: keysLoading, error: keysError } = useApiKeys();
1797
- const { gatewayBaseUrl: configuredGatewayBaseUrl } = useDevPortalBillingConfig();
1798
- const gatewayBaseUrl = credentials?.gatewayBaseUrl?.trim() || apiKeys?.gatewayBaseUrl?.trim() || configuredGatewayBaseUrl?.trim() || zudokuConfig.__meta?.gatewayBaseUrl?.trim() || null;
1799
- const header = apiKeys?.header ?? "Ocp-Apim-Subscription-Key";
1800
- const plans = apiKeys?.plans ?? [];
1801
- return /* @__PURE__ */ jsxs("div", {
1802
- className: "mx-auto max-w-screen-lg px-4 pb-16 pt-(--padding-content-top) lg:px-8",
1803
- children: [
1804
- /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "API credentials" }) }),
1805
- /* @__PURE__ */ jsxs("div", {
1806
- className: "mb-8 flex items-start gap-4",
1807
- children: [/* @__PURE__ */ jsx("div", {
1808
- className: "flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary",
1809
- children: /* @__PURE__ */ jsx(KeyRoundIcon, { size: 24 })
1810
- }), /* @__PURE__ */ jsxs("div", {
1811
- className: "space-y-1",
1812
- children: [/* @__PURE__ */ jsx("h1", {
1813
- className: "text-3xl font-semibold tracking-tight",
1814
- children: "API credentials"
1815
- }), /* @__PURE__ */ jsx("p", {
1816
- className: "text-muted-foreground",
1817
- children: "Create API keys for each subscribed plan and use them to authenticate requests to the API gateway."
1818
- })]
1819
- })]
1820
- }),
1821
- isDemoSession ? /* @__PURE__ */ jsx("div", {
1822
- className: "mb-6 rounded-xl border border-dashed bg-muted/20 p-4 text-sm text-muted-foreground",
1823
- children: "Local demo credentials. Publish the portal to receive live subscription keys from your dev-portal backend."
1824
- }) : null,
1825
- isPreview && !isDemoSession ? /* @__PURE__ */ jsx("div", {
1826
- className: "rounded-xl border border-dashed p-8 text-center",
1827
- children: /* @__PURE__ */ jsx("p", {
1828
- className: "text-muted-foreground",
1829
- children: "API credentials are available after the developer portal is published."
1830
- })
1831
- }) : null,
1832
- !isPreview && !isDemoSession && !auth.isAuthenticated ? /* @__PURE__ */ jsxs("div", {
1833
- className: "rounded-xl border border-dashed p-8 text-center",
1834
- children: [/* @__PURE__ */ jsx("p", {
1835
- className: "text-muted-foreground",
1836
- children: "Sign in to view your subscription keys."
1837
- }), /* @__PURE__ */ jsx(Button$2, {
1838
- className: "mt-4",
1839
- asChild: true,
1840
- children: /* @__PURE__ */ jsx(Link$1, {
1841
- to: ACCOUNT_PLANS_PATH,
1842
- children: "View plans"
1843
- })
1844
- })]
1845
- }) : null,
1846
- !isPreview && !isDemoSession && auth.isAuthenticated && (isLoading || keysLoading) ? /* @__PURE__ */ jsx("p", {
1847
- className: "text-muted-foreground",
1848
- children: "Loading credentials…"
1849
- }) : null,
1850
- !isPreview && !isDemoSession && auth.isAuthenticated && (error || keysError) ? /* @__PURE__ */ jsxs("div", {
1851
- className: "rounded-xl border border-dashed p-8 text-center",
1852
- children: [/* @__PURE__ */ jsx("p", {
1853
- className: "text-muted-foreground",
1854
- 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."
1855
- }), /* @__PURE__ */ jsx(Button$2, {
1856
- className: "mt-4",
1857
- asChild: true,
1858
- children: /* @__PURE__ */ jsx(Link$1, {
1859
- to: ACCOUNT_PLANS_PATH,
1860
- children: "View plans"
1861
- })
1862
- })]
1863
- }) : null,
1864
- (isDemoSession || !isPreview) && auth.isAuthenticated && !error && !keysError && !isLoading && !keysLoading ? /* @__PURE__ */ jsxs("div", {
1865
- className: "space-y-6",
1866
- children: [plans.length === 0 ? /* @__PURE__ */ jsxs("div", {
1867
- className: "rounded-xl border border-dashed p-8 text-center",
1868
- children: [/* @__PURE__ */ jsx("p", {
1869
- className: "text-muted-foreground",
1870
- children: "Choose a plan to get your first API key."
1871
- }), /* @__PURE__ */ jsx(Button$2, {
1872
- className: "mt-4",
1873
- asChild: true,
1874
- children: /* @__PURE__ */ jsx(Link$1, {
1875
- to: ACCOUNT_PLANS_PATH,
1876
- children: "View plans"
1877
- })
1878
- })]
1879
- }) : plans.map((plan) => /* @__PURE__ */ jsx(ApiKeysPanel, {
1880
- plan,
1881
- header,
1882
- readOnly: isDemoSession
1883
- }, plan.planSku)), /* @__PURE__ */ jsxs("article", {
1884
- className: "rounded-xl border bg-card/60 p-6 space-y-4",
1885
- children: [/* @__PURE__ */ jsxs("div", {
1886
- className: "space-y-1",
1887
- children: [/* @__PURE__ */ jsx("h2", {
1888
- className: "text-lg font-medium",
1889
- children: "Gateway URL"
1890
- }), /* @__PURE__ */ jsx("p", {
1891
- className: "text-sm text-muted-foreground",
1892
- children: "Base URL for API requests through the gateway."
1893
- })]
1894
- }), gatewayBaseUrl ? /* @__PURE__ */ jsx(Value, {
1895
- className: "max-w-full w-full",
1896
- value: gatewayBaseUrl
1897
- }) : /* @__PURE__ */ jsx("p", {
1898
- className: "text-sm text-muted-foreground",
1899
- children: "Available after your API is deployed to the gateway."
1900
- })]
1901
- })]
1902
- }) : null,
1903
- /* @__PURE__ */ jsxs("div", {
1904
- className: "mt-8 flex flex-wrap gap-3",
1905
- children: [/* @__PURE__ */ jsx(Button$2, {
1906
- variant: "outline",
1907
- asChild: true,
1908
- children: /* @__PURE__ */ jsx(Link$1, {
1909
- to: "/account/subscriptions",
1910
- children: "My subscriptions"
1911
- })
1912
- }), /* @__PURE__ */ jsx(Button$2, {
1913
- variant: "outline",
1914
- asChild: true,
1915
- children: /* @__PURE__ */ jsx(Link$1, {
1916
- to: "/api",
1917
- children: "API reference"
1918
- })
1919
- })]
1920
- })
1921
- ]
1922
- });
1923
- };
1924
-
1925
- //#endregion
1926
- //#region src/utils/pricing-config.ts
1927
- function isPreviewPricingEnabled() {
1928
- if (manifestCatalog?.pricingEnabled === true) return true;
1929
- return zudokuConfig?.__meta?.pricingEnabled === true;
1930
- }
1931
-
1932
- //#endregion
1933
- //#region src/pages/PricingPage.tsx
1934
- const DEFAULT_COMPARE_ROWS = [
1935
- {
1936
- label: "Monthly requests",
1937
- values: [
1938
- "1,000",
1939
- "50,000",
1940
- "500,000",
1941
- "Unlimited"
1942
- ]
1943
- },
1944
- {
1945
- label: "Rate limit",
1946
- values: [
1947
- "10/sec",
1948
- "50/sec",
1949
- "200/sec",
1950
- "Custom"
1951
- ]
1952
- },
1953
- {
1954
- label: "Team seats",
1955
- values: [
1956
- "1",
1957
- "1",
1958
- "5",
1959
- "Unlimited"
1960
- ]
1961
- },
1962
- {
1963
- label: "Webhooks",
1964
- values: [
1965
- "—",
1966
- "Included",
1967
- "Included",
1968
- "Included"
1969
- ]
1970
- },
1971
- {
1972
- label: "SSO",
1973
- values: [
1974
- "—",
1975
- "—",
1976
- "—",
1977
- "Included"
1978
- ]
1979
- },
1980
- {
1981
- label: "Support",
1982
- values: [
1983
- "—",
1984
- "Email",
1985
- "Priority",
1986
- "Dedicated SLA"
1987
- ]
1988
- }
1989
- ];
1990
- const DEFAULT_FAQS = [{
1991
- question: "What happens if I exceed my quota?",
1992
- answer: "Requests over your monthly quota return a 429 response so you can test rate-limit handling instead of being cut off unexpectedly."
1993
- }, {
1994
- question: "Can I change plans later?",
1995
- answer: "Yes, upgrade or downgrade any time from your billing page. Changes apply on your next billing cycle."
1996
- }];
1997
- const PricingPage = () => {
1998
- const { pricing, previewPlans } = useDevPortalBillingConfig();
1999
- const isPreview = useDevPortalPreviewMode();
2000
- const pricingEnabled = isPreviewPricingEnabled() || isPreview;
2001
- const livePlans = usePlans();
2002
- const catalog = isPreview ? parseManifestPlans(previewPlans) : livePlans.data;
2003
- const isLoading = isPreview ? false : livePlans.isLoading;
2004
- const isError = isPreview ? false : livePlans.isError;
2005
- if (!pricingEnabled) return /* @__PURE__ */ jsx(Navigate, {
2006
- to: "/",
2007
- replace: true
2008
- });
2009
- const title = pricing?.title ?? "Pay for what you call";
2010
- const subtitle = pricing?.subtitle ?? "Every plan includes the full endpoint catalog. Higher tiers raise your request quota, rate limit, and support level.";
2011
- const plans = catalog?.plans ?? [];
2012
- const featuredSku = pickFeaturedPlanSku(plans);
2013
- const compareRows = pricing?.compareRows && pricing.compareRows.length > 0 ? pricing.compareRows : DEFAULT_COMPARE_ROWS;
2014
- const faqs = pricing?.faqs && pricing.faqs.length > 0 ? pricing.faqs : DEFAULT_FAQS;
2015
- const compareHeaders = plans.length > 0 ? plans.map((p) => p.displayName) : [
2016
- "Free",
2017
- "Developer",
2018
- "Team",
2019
- "Enterprise"
2020
- ];
2021
- return /* @__PURE__ */ jsxs("div", {
2022
- className: "w-full",
2023
- children: [
2024
- /* @__PURE__ */ jsxs(Head, { children: [/* @__PURE__ */ jsx("title", { children: title }), /* @__PURE__ */ jsx("meta", {
2025
- name: "description",
2026
- content: subtitle
2027
- })] }),
2028
- /* @__PURE__ */ jsxs("section", {
2029
- className: "px-6 pb-4 pt-[clamp(3rem,6vw,4.5rem)] text-center",
2030
- children: [
2031
- /* @__PURE__ */ jsx("p", {
2032
- className: "text-xs font-bold uppercase tracking-[0.08em] text-primary",
2033
- children: "Pricing"
2034
- }),
2035
- /* @__PURE__ */ jsx(Heading, {
2036
- level: 1,
2037
- className: "mt-3 text-[clamp(2rem,3.5vw,3rem)] font-extrabold tracking-[-0.02em]",
2038
- "data-testid": "title",
2039
- children: title
2040
- }),
2041
- /* @__PURE__ */ jsx("p", {
2042
- className: "mx-auto mt-3.5 max-w-[34rem] text-lg leading-relaxed text-muted-foreground",
2043
- "data-testid": "subtitle",
2044
- children: subtitle
2045
- })
2046
- ]
2047
- }),
2048
- /* @__PURE__ */ jsxs("section", {
2049
- className: "px-6 pb-16 pt-8",
2050
- children: [
2051
- isLoading ? /* @__PURE__ */ jsx("p", {
2052
- className: "text-center text-muted-foreground",
2053
- children: "Loading plans…"
2054
- }) : null,
2055
- isError ? /* @__PURE__ */ jsx("p", {
2056
- className: "text-center text-destructive",
2057
- children: "Unable to load plans."
2058
- }) : null,
2059
- plans.length > 0 ? /* @__PURE__ */ jsx("div", {
2060
- className: "mx-auto grid max-w-[1200px] grid-cols-1 items-stretch gap-5 sm:grid-cols-2 lg:grid-cols-4",
2061
- children: plans.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
2062
- plan,
2063
- mode: "public",
2064
- featured: plan.sku === featuredSku
2065
- }, plan.sku))
2066
- }) : !isLoading && !isError ? /* @__PURE__ */ jsxs("div", {
2067
- className: "mx-auto max-w-lg rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
2068
- children: [
2069
- "No plans are configured yet. Add plans in",
2070
- " ",
2071
- /* @__PURE__ */ jsx("code", {
2072
- className: "rounded bg-muted px-1.5 py-0.5",
2073
- children: "apitogo.config.json"
2074
- }),
2075
- " ",
2076
- "and set",
2077
- " ",
2078
- /* @__PURE__ */ jsx("code", {
2079
- className: "rounded bg-muted px-1.5 py-0.5",
2080
- children: "modules.userPanel.plans.enabled"
2081
- }),
2082
- " ",
2083
- "to true."
2084
- ]
2085
- }) : null
2086
- ]
2087
- }),
2088
- /* @__PURE__ */ jsx("section", {
2089
- className: "border-t border-border bg-muted/40 px-6 py-16",
2090
- "data-testid": "pricing-compare",
2091
- children: /* @__PURE__ */ jsxs("div", {
2092
- className: "mx-auto max-w-[1200px]",
2093
- children: [/* @__PURE__ */ jsx("h2", {
2094
- className: "mb-6 text-[clamp(1.5rem,2.5vw,2.25rem)] font-extrabold tracking-[-0.02em]",
2095
- children: "Compare plans"
2096
- }), /* @__PURE__ */ jsx("div", {
2097
- className: "overflow-x-auto rounded-xl border border-border bg-card",
2098
- children: /* @__PURE__ */ jsxs("div", {
2099
- className: "min-w-[720px]",
2100
- children: [/* @__PURE__ */ jsxs("div", {
2101
- 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",
2102
- style: { gridTemplateColumns: `minmax(10rem,1.4fr) repeat(${compareHeaders.length}, minmax(6rem,1fr))` },
2103
- children: [/* @__PURE__ */ jsx("div", { children: "Feature" }), compareHeaders.map((header) => /* @__PURE__ */ jsx("div", { children: header }, header))]
2104
- }), compareRows.map((row) => /* @__PURE__ */ jsxs("div", {
2105
- className: "grid gap-0 border-b border-border px-5 py-3.5 text-[0.84375rem] text-muted-foreground last:border-b-0",
2106
- style: { gridTemplateColumns: `minmax(10rem,1.4fr) repeat(${compareHeaders.length}, minmax(6rem,1fr))` },
2107
- children: [/* @__PURE__ */ jsx("div", {
2108
- className: "font-medium text-foreground",
2109
- children: row.label
2110
- }), compareHeaders.map((header, index) => /* @__PURE__ */ jsx("div", { children: row.values[index] ?? "—" }, `${row.label}-${header}`))]
2111
- }, row.label))]
2112
- })
2113
- })]
2114
- })
2115
- }),
2116
- /* @__PURE__ */ jsx("section", {
2117
- className: "px-6 py-16",
2118
- "data-testid": "pricing-faq",
2119
- children: /* @__PURE__ */ jsxs("div", {
2120
- className: "mx-auto max-w-4xl",
2121
- children: [/* @__PURE__ */ jsx("h2", {
2122
- className: "mb-8 text-center text-[clamp(1.5rem,2.5vw,2.25rem)] font-extrabold tracking-[-0.02em]",
2123
- children: "Frequently asked questions"
2124
- }), /* @__PURE__ */ jsx("div", { children: faqs.map((faq) => /* @__PURE__ */ jsxs("div", {
2125
- className: "border-b border-border py-5 last:border-b-0",
2126
- children: [/* @__PURE__ */ jsx("div", {
2127
- className: "text-[0.9375rem] font-bold",
2128
- children: faq.question
2129
- }), /* @__PURE__ */ jsx("p", {
2130
- className: "mt-2 text-sm leading-relaxed text-muted-foreground",
2131
- children: faq.answer
2132
- })]
2133
- }, faq.question)) })]
2134
- })
2135
- })
2136
- ]
2137
- });
2138
- };
2139
-
2140
- //#endregion
2141
- //#region src/hooks/useBillingPortal.ts
2142
- function useBillingPortal() {
2143
- const apiBaseUrl = useDevPortalApiBaseUrl();
2144
- return useMutation({ mutationFn: async (returnUrl) => {
2145
- if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
2146
- const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/billing-portal`, {
2147
- method: "POST",
2148
- headers: {
2149
- Accept: "application/json",
2150
- "Content-Type": "application/json"
2151
- },
2152
- credentials: "include",
2153
- body: JSON.stringify({ returnUrl: returnUrl ?? `${window.location.origin}/account/subscriptions` })
2154
- });
2155
- if (!response.ok) throw new Error("Unable to open billing portal.");
2156
- const body = await response.json();
2157
- if (!body.url) throw new Error("Billing portal URL was not returned.");
2158
- window.location.assign(body.url);
2159
- } });
2160
- }
2161
-
2162
- //#endregion
2163
- //#region src/pages/SubscriptionsPage.tsx
2164
- const SubscriptionsPage = () => {
2165
- const auth = useAuth();
2166
- const [searchParams] = useSearchParams();
2167
- const checkoutSuccess = searchParams.get("checkout") === "success";
2168
- const { data: me, isLoading, refetch } = useMySubscriptions();
2169
- const { data: catalog } = usePlans();
2170
- const billingPortal = useBillingPortal();
2171
- const { data: usage } = useUsage("mtd");
2172
- const planBySku = new Map((catalog?.plans ?? []).map((plan) => [plan.sku, plan]));
2173
- const subscriptions = me?.subscriptions ?? [];
2174
- const activeSubscription = subscriptions.find((sub) => isActiveSubscription(sub.status));
2175
- const activePlan = activeSubscription ? planBySku.get(activeSubscription.planSku) : void 0;
2176
- const confirmingPayment = checkoutSuccess && !isLoading && !activeSubscription;
2177
- useEffect(() => {
2178
- if (!confirmingPayment) return;
2179
- let attempts = 0;
2180
- const timer = setInterval(() => {
2181
- attempts += 1;
2182
- refetch();
2183
- if (attempts >= 30) clearInterval(timer);
2184
- }, 2e3);
2185
- return () => clearInterval(timer);
2186
- }, [confirmingPayment, refetch]);
2187
- const monthlyQuota = activePlan ? parseMonthlyQuotaCalls(activePlan.limitsJson) : null;
2188
- const usedRequests = usage?.requestCount ?? 0;
2189
- const quotaPct = monthlyQuota && monthlyQuota > 0 ? Math.min(100, Math.round(usedRequests / monthlyQuota * 100)) : null;
2190
- return /* @__PURE__ */ jsxs("div", {
2191
- className: "px-8 py-7",
2192
- children: [
2193
- /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Billing" }) }),
2194
- /* @__PURE__ */ jsx("h1", {
2195
- className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
2196
- children: "Billing"
2197
- }),
2198
- /* @__PURE__ */ jsx("p", {
2199
- className: "mb-8 text-muted-foreground",
2200
- children: "Manage your plan, payment method, and invoices."
2201
- }),
2202
- confirmingPayment ? /* @__PURE__ */ jsx("div", {
2203
- className: "mb-6 rounded-xl border border-border bg-card p-4 text-sm",
2204
- children: "Payment received. Confirming your subscription, this takes a few seconds…"
2205
- }) : null,
2206
- checkoutSuccess && activeSubscription ? /* @__PURE__ */ jsxs("div", {
2207
- className: "mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-sm",
2208
- children: [
2209
- "Your subscription is active.",
2210
- " ",
2211
- /* @__PURE__ */ jsx(Link$1, {
2212
- to: CREDENTIALS_PATH,
2213
- className: "font-medium underline",
2214
- children: "View your API credentials"
2215
- }),
2216
- "."
2217
- ]
2218
- }) : null,
2219
- isLoading ? /* @__PURE__ */ jsx("p", {
2220
- className: "text-muted-foreground",
2221
- children: "Loading billing details…"
2222
- }) : null,
2223
- !isLoading && activePlan && activeSubscription ? /* @__PURE__ */ jsxs("div", {
2224
- className: "mb-8 rounded-xl border border-border bg-card p-6",
2225
- children: [
2226
- /* @__PURE__ */ jsx("p", {
2227
- className: "text-xs font-medium uppercase tracking-[0.05em] text-muted-foreground",
2228
- children: "Current plan"
2229
- }),
2230
- /* @__PURE__ */ jsx("h2", {
2231
- className: "mt-2 text-2xl font-bold",
2232
- children: activePlan.displayName
2233
- }),
2234
- /* @__PURE__ */ jsxs("p", {
2235
- className: "mt-1 text-sm text-muted-foreground",
2236
- children: [
2237
- formatPlanPrice(activePlan),
2238
- " ·",
2239
- " ",
2240
- formatBillingPeriod(activePlan.billingPeriod)
2241
- ]
2242
- }),
2243
- activeSubscription.currentPeriodEnd ? /* @__PURE__ */ jsxs("p", {
2244
- className: "mt-3 text-sm text-muted-foreground",
2245
- children: [
2246
- "Renews",
2247
- " ",
2248
- new Date(activeSubscription.currentPeriodEnd).toLocaleDateString()
2249
- ]
2250
- }) : null,
2251
- quotaPct != null && monthlyQuota != null ? /* @__PURE__ */ jsxs("div", {
2252
- className: "mt-5",
2253
- children: [/* @__PURE__ */ jsxs("div", {
2254
- className: "mb-1.5 flex justify-between text-xs text-muted-foreground",
2255
- children: [/* @__PURE__ */ jsx("span", { children: "Monthly requests" }), /* @__PURE__ */ jsxs("span", { children: [
2256
- usedRequests.toLocaleString(),
2257
- " /",
2258
- " ",
2259
- monthlyQuota.toLocaleString()
2260
- ] })]
2261
- }), /* @__PURE__ */ jsx("div", {
2262
- className: "h-2 rounded-full bg-muted",
2263
- children: /* @__PURE__ */ jsx("div", {
2264
- className: "h-full rounded-full bg-primary transition-[width]",
2265
- style: { width: `${quotaPct}%` }
2266
- })
2267
- })]
2268
- }) : null
2269
- ]
2270
- }) : null,
2271
- !isLoading && subscriptions.length === 0 ? /* @__PURE__ */ jsxs("div", {
2272
- className: "mb-8 rounded-xl border border-dashed p-8 text-center",
2273
- children: [/* @__PURE__ */ jsx("p", {
2274
- className: "text-muted-foreground",
2275
- children: "You do not have an active subscription yet."
2276
- }), /* @__PURE__ */ jsx(Button$1, {
2277
- className: "mt-4",
2278
- asChild: true,
2279
- children: /* @__PURE__ */ jsx(Link$1, {
2280
- to: ACCOUNT_PLANS_PATH,
2281
- children: "View plans"
2282
- })
2283
- })]
2284
- }) : null,
2285
- !isLoading && subscriptions.length > 1 ? /* @__PURE__ */ jsx("div", {
2286
- className: "mb-8 space-y-4",
2287
- children: subscriptions.map((subscription) => {
2288
- const plan = planBySku.get(subscription.planSku);
2289
- return /* @__PURE__ */ jsx("article", {
2290
- className: "rounded-xl border bg-card p-5",
2291
- children: /* @__PURE__ */ jsxs("div", {
2292
- className: "flex flex-wrap items-start justify-between gap-4",
2293
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
2294
- className: "font-semibold",
2295
- children: plan?.displayName ?? subscription.planSku
2296
- }), plan ? /* @__PURE__ */ jsxs("p", {
2297
- className: "mt-1 text-sm text-muted-foreground",
2298
- children: [
2299
- formatPlanPrice(plan),
2300
- " ·",
2301
- " ",
2302
- formatBillingPeriod(plan.billingPeriod)
2303
- ]
2304
- }) : null] }), /* @__PURE__ */ jsx("span", {
2305
- className: "rounded-full bg-muted px-3 py-1 text-xs font-medium text-muted-foreground",
2306
- children: subscription.status
2307
- })]
2308
- })
2309
- }, `${subscription.planSku}-${subscription.status}`);
2310
- })
2311
- }) : null,
2312
- /* @__PURE__ */ jsxs("div", {
2313
- className: "flex flex-wrap gap-3",
2314
- children: [
2315
- /* @__PURE__ */ jsx(Button$1, {
2316
- variant: "outline",
2317
- asChild: true,
2318
- children: /* @__PURE__ */ jsx(Link$1, {
2319
- to: ACCOUNT_PLANS_PATH,
2320
- children: "Change plan"
2321
- })
2322
- }),
2323
- auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
2324
- variant: "default",
2325
- disabled: billingPortal.isPending,
2326
- onClick: () => {
2327
- billingPortal.mutate();
2328
- },
2329
- children: billingPortal.isPending ? /* @__PURE__ */ jsx(Spinner, {}) : "Manage payment & invoices"
2330
- }) : null,
2331
- auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
2332
- variant: "secondary",
2333
- asChild: true,
2334
- children: /* @__PURE__ */ jsx(Link$1, {
2335
- to: CREDENTIALS_PATH,
2336
- children: "API credentials"
2337
- })
2338
- }) : null
2339
- ]
2340
- }),
2341
- billingPortal.isError ? /* @__PURE__ */ jsx("p", {
2342
- className: "mt-4 text-sm text-destructive",
2343
- children: billingPortal.error instanceof Error ? billingPortal.error.message : "Unable to open billing portal."
2344
- }) : null
2345
- ]
2346
- });
2347
- };
2348
-
2349
- //#endregion
2350
- //#region src/pages/UsagePage.tsx
2351
- const RANGES = [
2352
- {
2353
- value: "24h",
2354
- label: "24h"
2355
- },
2356
- {
2357
- value: "7d",
2358
- label: "7d"
2359
- },
2360
- {
2361
- value: "30d",
2362
- label: "30d"
2363
- },
2364
- {
2365
- value: "90d",
2366
- label: "90d"
2367
- }
2368
- ];
2369
- function formatNumber(value) {
2370
- return new Intl.NumberFormat().format(value);
2371
- }
2372
- function formatPct(rate) {
2373
- return `${(rate * 100).toFixed(rate >= .1 ? 1 : 2)}%`;
2374
- }
2375
- const UsagePage = () => {
2376
- const auth = useAuth();
2377
- const [range, setRange] = useState("30d");
2378
- const { data, isLoading, isError } = useUsage(range);
2379
- const maxBar = Math.max(1, ...data?.series.map((point) => point.requestCount) ?? [1]);
2380
- const endpointTotal = data?.byEndpoint.reduce((sum, item) => sum + item.count, 0) ?? 0;
2381
- if (!auth.isAuthenticated) return /* @__PURE__ */ jsxs("div", {
2382
- className: "px-8 py-7",
2383
- children: [
2384
- /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Usage" }) }),
2385
- /* @__PURE__ */ jsx("h1", {
2386
- className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
2387
- children: "Usage"
2388
- }),
2389
- /* @__PURE__ */ jsx("p", {
2390
- className: "text-muted-foreground",
2391
- children: "Sign in to view your API usage."
2392
- }),
2393
- auth.isAuthEnabled ? /* @__PURE__ */ jsx(Button$1, {
2394
- asChild: true,
2395
- className: "mt-4",
2396
- children: /* @__PURE__ */ jsx(Link$1, {
2397
- to: "/signin",
2398
- children: "Sign in"
2399
- })
2400
- }) : null
2401
- ]
2402
- });
2403
- return /* @__PURE__ */ jsxs("div", {
2404
- className: "px-8 py-7",
2405
- children: [
2406
- /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Usage" }) }),
2407
- /* @__PURE__ */ jsxs("div", {
2408
- className: "mb-6 flex flex-wrap items-center justify-between gap-3",
2409
- children: [/* @__PURE__ */ jsx("h1", {
2410
- className: "text-[1.6rem] font-bold tracking-[-0.01em]",
2411
- children: "Usage"
2412
- }), /* @__PURE__ */ jsx("div", {
2413
- className: "flex gap-1 rounded-lg border border-border bg-card p-1",
2414
- children: RANGES.map((item) => /* @__PURE__ */ jsx("button", {
2415
- type: "button",
2416
- onClick: () => setRange(item.value),
2417
- 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"}`,
2418
- children: item.label
2419
- }, item.value))
2420
- })]
2421
- }),
2422
- isLoading ? /* @__PURE__ */ jsx("p", {
2423
- className: "text-sm text-muted-foreground",
2424
- children: "Loading usage…"
2425
- }) : null,
2426
- isError ? /* @__PURE__ */ jsx("p", {
2427
- className: "text-sm text-destructive",
2428
- children: "Unable to load usage data."
2429
- }) : null,
2430
- /* @__PURE__ */ jsxs("div", {
2431
- className: "mb-5 grid grid-cols-1 gap-4 sm:grid-cols-3",
2432
- children: [
2433
- /* @__PURE__ */ jsxs("div", {
2434
- className: "rounded-xl border border-border bg-card p-4",
2435
- children: [/* @__PURE__ */ jsx("div", {
2436
- className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
2437
- children: "Requests"
2438
- }), /* @__PURE__ */ jsx("div", {
2439
- className: "mt-1.5 text-2xl font-bold",
2440
- children: data ? formatNumber(data.requestCount) : "—"
2441
- })]
2442
- }),
2443
- /* @__PURE__ */ jsxs("div", {
2444
- className: "rounded-xl border border-border bg-card p-4",
2445
- children: [/* @__PURE__ */ jsx("div", {
2446
- className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
2447
- children: "Avg latency"
2448
- }), /* @__PURE__ */ jsx("div", {
2449
- className: "mt-1.5 text-2xl font-bold",
2450
- children: data?.avgLatencyMs != null ? `${data.avgLatencyMs} ms` : "—"
2451
- })]
2452
- }),
2453
- /* @__PURE__ */ jsxs("div", {
2454
- className: "rounded-xl border border-border bg-card p-4",
2455
- children: [/* @__PURE__ */ jsx("div", {
2456
- className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
2457
- children: "Error rate"
2458
- }), /* @__PURE__ */ jsx("div", {
2459
- className: "mt-1.5 text-2xl font-bold",
2460
- children: data ? formatPct(data.errorRate) : "—"
2461
- })]
2462
- })
2463
- ]
2464
- }),
2465
- /* @__PURE__ */ jsxs("div", {
2466
- className: "mb-5 rounded-xl border border-border bg-card p-6",
2467
- children: [/* @__PURE__ */ jsxs("h2", {
2468
- className: "mb-4 text-lg font-semibold",
2469
- children: [
2470
- "Requests over time (",
2471
- range,
2472
- ")"
2473
- ]
2474
- }), data && data.series.some((p) => p.requestCount > 0) ? /* @__PURE__ */ jsx("div", {
2475
- className: "flex h-40 items-end gap-1.5",
2476
- children: data.series.map((point) => /* @__PURE__ */ jsx("div", {
2477
- title: `${point.dayUtc}: ${point.requestCount}`,
2478
- className: "min-w-0 flex-1 rounded-t-sm bg-primary",
2479
- style: { height: `${Math.max(4, point.requestCount / maxBar * 100)}%` }
2480
- }, point.dayUtc))
2481
- }) : /* @__PURE__ */ jsx("p", {
2482
- className: "text-sm text-muted-foreground",
2483
- children: "No request data for this range yet."
2484
- })]
2485
- }),
2486
- /* @__PURE__ */ jsxs("div", {
2487
- className: "grid grid-cols-1 gap-5 lg:grid-cols-[1.2fr_1fr]",
2488
- children: [/* @__PURE__ */ jsxs("div", {
2489
- className: "rounded-xl border border-border bg-card p-6",
2490
- children: [/* @__PURE__ */ jsx("h2", {
2491
- className: "mb-4 text-lg font-semibold",
2492
- children: "By endpoint"
2493
- }), data && data.byEndpoint.length > 0 ? /* @__PURE__ */ jsx("div", {
2494
- className: "space-y-3.5",
2495
- children: data.byEndpoint.map((item) => {
2496
- const pct = endpointTotal > 0 ? Math.round(item.count / endpointTotal * 100) : 0;
2497
- return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
2498
- className: "mb-1 flex justify-between text-[13px]",
2499
- children: [/* @__PURE__ */ jsx("code", {
2500
- className: "font-mono text-foreground",
2501
- children: item.path
2502
- }), /* @__PURE__ */ jsxs("span", {
2503
- className: "text-muted-foreground",
2504
- children: [pct, "%"]
2505
- })]
2506
- }), /* @__PURE__ */ jsx("div", {
2507
- className: "h-1.5 rounded-full bg-muted",
2508
- children: /* @__PURE__ */ jsx("div", {
2509
- className: "h-full rounded-full bg-primary",
2510
- style: { width: `${pct}%` }
2511
- })
2512
- })] }, item.path);
2513
- })
2514
- }) : /* @__PURE__ */ jsx("p", {
2515
- className: "text-sm text-muted-foreground",
2516
- children: "No endpoint data."
2517
- })]
2518
- }), /* @__PURE__ */ jsxs("div", {
2519
- className: "rounded-xl border border-border bg-card p-6",
2520
- children: [/* @__PURE__ */ jsx("h2", {
2521
- className: "mb-4 text-lg font-semibold",
2522
- children: "Status codes"
2523
- }), data && data.byStatus.length > 0 ? /* @__PURE__ */ jsx("div", { children: data.byStatus.map((item) => /* @__PURE__ */ jsxs("div", {
2524
- className: "flex items-center justify-between border-b border-border py-2.5 last:border-0",
2525
- children: [/* @__PURE__ */ jsx("span", {
2526
- className: "text-sm font-medium",
2527
- children: item.label
2528
- }), /* @__PURE__ */ jsx("span", {
2529
- className: "text-sm font-bold",
2530
- children: formatNumber(item.count)
2531
- })]
2532
- }, item.label)) }) : /* @__PURE__ */ jsx("p", {
2533
- className: "text-sm text-muted-foreground",
2534
- children: "No status data."
2535
- })]
2536
- })]
2537
- })
2538
- ]
2539
- });
2540
- };
2541
-
2542
248
  //#endregion
2543
249
  //#region src/utils/auth-config.ts
2544
250
  async function resolveAuthenticationNavEnabled(config) {
@@ -2623,101 +329,114 @@ function buildDevPortalHeaderNavigation({ navigation, pricingEnabled, authNavEna
2623
329
 
2624
330
  //#endregion
2625
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"));
2626
339
  function resolvePricingEnabled(config) {
2627
340
  if (config.__meta?.pricingEnabled !== void 0) return config.__meta.pricingEnabled;
2628
341
  return isPreviewPricingEnabled();
2629
342
  }
2630
- const devPortalBillingPlugin = createPlugin((options = {}) => ({
2631
- transformConfig: async ({ config, merge }) => {
2632
- const pricingEnabled = resolvePricingEnabled(config);
2633
- const authNavEnabled = !!config.authentication && await resolveAuthenticationNavEnabled(config);
2634
- const headerNavigation = buildDevPortalHeaderNavigation({
2635
- navigation: config.header?.navigation,
2636
- pricingEnabled,
2637
- authNavEnabled
2638
- });
2639
- return merge({
2640
- navigation: pricingEnabled ? withPricingNavigation(config.navigation) : withoutPricingNavigation(config.navigation),
2641
- header: {
2642
- ...config.header,
2643
- navigation: headerNavigation
2644
- }
2645
- });
2646
- },
2647
- getProfileMenuItems: () => [{
2648
- label: "My Subscriptions",
2649
- path: SUBSCRIPTIONS_PATH,
2650
- icon: StarsIcon,
2651
- category: "middle",
2652
- weight: 25
2653
- }, {
2654
- label: "API credentials",
2655
- path: CREDENTIALS_PATH,
2656
- icon: KeyRoundIcon,
2657
- category: "middle",
2658
- weight: 26
2659
- }],
2660
- getRoutes: () => [{
2661
- element: /* @__PURE__ */ jsx(DevPortalBillingWrapper, { options }),
2662
- handle: { layout: "none" },
2663
- children: [
2664
- {
2665
- path: PRICING_PATH,
2666
- handle: { layout: "default" },
2667
- element: /* @__PURE__ */ jsx(PricingPage, {})
2668
- },
2669
- {
2670
- path: CHECKOUT_PATH,
2671
- handle: { layout: "default" },
2672
- element: /* @__PURE__ */ jsx(CheckoutPage, {})
2673
- },
2674
- {
2675
- path: CONTACT_SALES_PATH,
2676
- handle: { layout: "default" },
2677
- element: /* @__PURE__ */ jsx(ContactSalesPage, {})
2678
- },
2679
- {
2680
- path: ACCOUNT_PLANS_PATH,
2681
- handle: { layout: "default" },
2682
- element: /* @__PURE__ */ jsx(AccountLayout, {
2683
- includeBillingNav: true,
2684
- children: /* @__PURE__ */ jsx(AccountPlansPage, {})
2685
- })
2686
- },
2687
- {
2688
- path: SUBSCRIPTIONS_PATH,
2689
- handle: { layout: "default" },
2690
- element: /* @__PURE__ */ jsx(AccountLayout, {
2691
- includeBillingNav: true,
2692
- children: /* @__PURE__ */ jsx(SubscriptionsPage, {})
2693
- })
2694
- },
2695
- {
2696
- path: CREDENTIALS_PATH,
2697
- handle: { layout: "default" },
2698
- element: /* @__PURE__ */ jsx(AccountLayout, {
2699
- includeBillingNav: true,
2700
- children: /* @__PURE__ */ jsx(CredentialsPage, {})
2701
- })
2702
- },
2703
- {
2704
- path: USAGE_PATH,
2705
- handle: { layout: "default" },
2706
- element: /* @__PURE__ */ jsx(AccountLayout, {
2707
- includeBillingNav: true,
2708
- children: /* @__PURE__ */ jsx(UsagePage, {})
2709
- })
2710
- }
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
2711
437
  ]
2712
- }],
2713
- getProtectedRoutes: () => [
2714
- CHECKOUT_PATH,
2715
- ACCOUNT_PLANS_PATH,
2716
- SUBSCRIPTIONS_PATH,
2717
- CREDENTIALS_PATH,
2718
- USAGE_PATH
2719
- ]
2720
- }));
438
+ };
439
+ });
2721
440
 
2722
441
  //#endregion
2723
442
  //#region src/hooks/usePlansPreview.ts