@lukoweb/apitogo-plugin-dev-portal-billing 0.1.87 → 0.1.89
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.d.mts +33 -18
- package/dist/index.mjs +1030 -177
- package/package.json +5 -3
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Button, Head, Heading, Spinner } from "@lukoweb/apitogo/components";
|
|
2
|
-
import { CreditCardIcon, KeyRoundIcon, ShieldIcon, StarsIcon, TrashIcon } from "@lukoweb/apitogo/icons";
|
|
1
|
+
import { Button, Head, Heading, Link, Spinner } from "@lukoweb/apitogo/components";
|
|
2
|
+
import { ArrowRightIcon, CreditCardIcon, KeyRoundIcon, ShieldIcon, StarsIcon, TrashIcon } from "@lukoweb/apitogo/icons";
|
|
3
3
|
import { createContext, useContext, useEffect, useState } from "react";
|
|
4
|
-
import { Link, Navigate, Outlet, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
|
|
4
|
+
import { Link as Link$1, Navigate, Outlet, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
|
|
5
5
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@lukoweb/apitogo/ui/AlertDialog.js";
|
|
6
6
|
import { cn } from "@lukoweb/apitogo/ui/util.js";
|
|
7
7
|
import manifestCatalog from "virtual:apitogo-local-manifest";
|
|
@@ -9,11 +9,12 @@ import zudokuConfig from "virtual:zudoku-config";
|
|
|
9
9
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
import { useMutation, useQuery, useQueryClient } from "@lukoweb/apitogo/react-query";
|
|
11
11
|
import { useAuth } from "@lukoweb/apitogo/hooks";
|
|
12
|
-
import { Alert, AlertTitle } from "@lukoweb/apitogo/ui/Alert.js";
|
|
13
12
|
import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
|
|
13
|
+
import { Alert, AlertTitle } from "@lukoweb/apitogo/ui/Alert.js";
|
|
14
14
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@lukoweb/apitogo/ui/Dialog.js";
|
|
15
15
|
import { Input } from "@lukoweb/apitogo/ui/Input.js";
|
|
16
16
|
import { Secret } from "@lukoweb/apitogo/ui/Secret.js";
|
|
17
|
+
import { AccountLayout } from "@lukoweb/apitogo-module-user-panel";
|
|
17
18
|
import { createPlugin } from "@lukoweb/apitogo/plugins";
|
|
18
19
|
import { Alert as Alert$1, AlertAction, AlertDescription } from "@lukoweb/apitogo/ui/Alert";
|
|
19
20
|
import { Button as Button$2 } from "@lukoweb/apitogo/ui/Button";
|
|
@@ -39,7 +40,8 @@ function mapPreviewPlans(plans) {
|
|
|
39
40
|
priceCurrency: plan.priceCurrency,
|
|
40
41
|
billingPeriod: plan.billingPeriod,
|
|
41
42
|
limitsJson: plan.limitsJson,
|
|
42
|
-
includedActionKeys: plan.includedActionKeys
|
|
43
|
+
includedActionKeys: plan.includedActionKeys,
|
|
44
|
+
features: plan.features
|
|
43
45
|
}));
|
|
44
46
|
}
|
|
45
47
|
function readModulePlanItems() {
|
|
@@ -54,7 +56,8 @@ function parseManifestPlans(configPlans) {
|
|
|
54
56
|
priceCurrency: plan.priceCurrency ?? "usd",
|
|
55
57
|
billingPeriod: plan.billingPeriod ?? "monthly",
|
|
56
58
|
limitsJson: plan.limitsJson ?? null,
|
|
57
|
-
includedActionKeys: plan.includedActionKeys ?? []
|
|
59
|
+
includedActionKeys: plan.includedActionKeys ?? [],
|
|
60
|
+
features: plan.features
|
|
58
61
|
}))) };
|
|
59
62
|
if (manifestCatalog?.plans?.length) return { plans: mapPreviewPlans(manifestCatalog.plans) };
|
|
60
63
|
const metaPlans = zudokuConfig?.__meta?.previewPlans;
|
|
@@ -67,7 +70,8 @@ function parseManifestPlans(configPlans) {
|
|
|
67
70
|
priceCurrency: plan.priceCurrency ?? "usd",
|
|
68
71
|
billingPeriod: plan.billingPeriod ?? "monthly",
|
|
69
72
|
limitsJson: plan.limitsJson ?? null,
|
|
70
|
-
includedActionKeys: plan.includedActionKeys ?? []
|
|
73
|
+
includedActionKeys: plan.includedActionKeys ?? [],
|
|
74
|
+
features: plan.features
|
|
71
75
|
}))) };
|
|
72
76
|
return { plans: [] };
|
|
73
77
|
}
|
|
@@ -77,6 +81,7 @@ function formatActionKey(actionKey) {
|
|
|
77
81
|
return `${actionKey.slice(0, separatorIndex).toUpperCase()} ${actionKey.slice(separatorIndex + 1)}`;
|
|
78
82
|
}
|
|
79
83
|
function formatPlanPrice(plan) {
|
|
84
|
+
if (plan.priceAmount < 0) return "Custom";
|
|
80
85
|
if (plan.priceAmount <= 0) return "Free";
|
|
81
86
|
const amount = new Intl.NumberFormat(void 0, {
|
|
82
87
|
style: "currency",
|
|
@@ -89,89 +94,127 @@ function formatPlanPrice(plan) {
|
|
|
89
94
|
return amount;
|
|
90
95
|
}
|
|
91
96
|
function isPlanFree(plan) {
|
|
92
|
-
return plan.priceAmount
|
|
97
|
+
return plan.priceAmount === 0;
|
|
93
98
|
}
|
|
94
99
|
function formatBillingPeriod(period) {
|
|
95
100
|
const normalized = period.toLowerCase();
|
|
96
|
-
if (normalized === "month" || normalized === "monthly") return "
|
|
97
|
-
if (normalized === "year" || normalized === "annual") return "
|
|
101
|
+
if (normalized === "month" || normalized === "monthly") return "per month";
|
|
102
|
+
if (normalized === "year" || normalized === "annual") return "per year";
|
|
103
|
+
if (normalized === "custom") return "talk to us";
|
|
98
104
|
return period;
|
|
99
105
|
}
|
|
100
106
|
function formatRateLimit(rate) {
|
|
101
107
|
if (!rate || typeof rate !== "object") return null;
|
|
102
108
|
const { calls, renewalPeriodSeconds } = rate;
|
|
103
109
|
if (calls == null || renewalPeriodSeconds == null) return null;
|
|
110
|
+
if (renewalPeriodSeconds === 1) return `${calls} req/sec rate limit`;
|
|
104
111
|
const window = renewalPeriodSeconds === 60 ? "minute" : `${renewalPeriodSeconds} seconds`;
|
|
105
112
|
return `${calls} request${calls === 1 ? "" : "s"} per ${window}`;
|
|
106
113
|
}
|
|
107
|
-
function
|
|
114
|
+
function formatQuota(quota) {
|
|
115
|
+
if (!quota || typeof quota !== "object") return null;
|
|
116
|
+
const { calls, renewalPeriodDays } = quota;
|
|
117
|
+
if (calls == null) return null;
|
|
118
|
+
if (calls < 0) return "Unlimited requests";
|
|
119
|
+
const formatted = calls.toLocaleString();
|
|
120
|
+
if (renewalPeriodDays === 30 || renewalPeriodDays === 31) return `${formatted} requests / month`;
|
|
121
|
+
if (renewalPeriodDays === 1) return `${formatted} requests / day`;
|
|
122
|
+
if (renewalPeriodDays != null) return `${formatted} requests / ${renewalPeriodDays} days`;
|
|
123
|
+
return `${formatted} requests`;
|
|
124
|
+
}
|
|
125
|
+
/** Marketing bullets keyed by common sample SKUs when limitsJson is sparse. */
|
|
126
|
+
const SAMPLE_PLAN_FEATURE_EXTRAS = {
|
|
127
|
+
free: ["All endpoints", "Community support"],
|
|
128
|
+
developer: ["Webhooks included", "Email support"],
|
|
129
|
+
team: ["5 team seats", "Priority support"],
|
|
130
|
+
enterprise: [
|
|
131
|
+
"Unlimited requests",
|
|
132
|
+
"Custom rate limits",
|
|
133
|
+
"SSO & audit logs",
|
|
134
|
+
"Dedicated SLA"
|
|
135
|
+
]
|
|
136
|
+
};
|
|
137
|
+
function listPlanFeatureLines(plan) {
|
|
138
|
+
if (plan.features && plan.features.length > 0) return plan.features;
|
|
139
|
+
const lines = [];
|
|
140
|
+
if (plan.limitsJson?.trim()) try {
|
|
141
|
+
const limits = JSON.parse(plan.limitsJson);
|
|
142
|
+
const quotaLine = formatQuota(limits.quota);
|
|
143
|
+
if (quotaLine) lines.push(quotaLine);
|
|
144
|
+
const rateLine = formatRateLimit(limits.rateLimit);
|
|
145
|
+
if (rateLine) lines.push(rateLine);
|
|
146
|
+
} catch {}
|
|
147
|
+
const extras = SAMPLE_PLAN_FEATURE_EXTRAS[plan.sku.toLowerCase()];
|
|
148
|
+
if (extras) {
|
|
149
|
+
for (const extra of extras) if (!lines.includes(extra)) lines.push(extra);
|
|
150
|
+
}
|
|
151
|
+
if (plan.includedActionKeys && plan.includedActionKeys.length > 0) for (const actionKey of plan.includedActionKeys) lines.push(formatActionKey(actionKey));
|
|
152
|
+
if (lines.length === 0 && isPlanFree(plan)) lines.push("API access not included");
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
function isActiveSubscription(status) {
|
|
156
|
+
return status.toLowerCase() === "active";
|
|
157
|
+
}
|
|
158
|
+
/** Returns monthly call quota from plan limitsJson, or null when unknown/unlimited. */
|
|
159
|
+
function parseMonthlyQuotaCalls(limitsJson) {
|
|
108
160
|
if (!limitsJson?.trim()) return null;
|
|
109
161
|
try {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
return
|
|
162
|
+
const quota = JSON.parse(limitsJson).quota;
|
|
163
|
+
if (!quota || typeof quota !== "object") return null;
|
|
164
|
+
const { calls, renewalPeriodDays } = quota;
|
|
165
|
+
if (calls == null || calls < 0) return null;
|
|
166
|
+
if (renewalPeriodDays == null || renewalPeriodDays === 30 || renewalPeriodDays === 31) return calls;
|
|
167
|
+
return null;
|
|
116
168
|
} catch {
|
|
117
169
|
return null;
|
|
118
170
|
}
|
|
119
171
|
}
|
|
120
|
-
function isActiveSubscription(status) {
|
|
121
|
-
return status.toLowerCase() === "active";
|
|
122
|
-
}
|
|
123
172
|
|
|
124
173
|
//#endregion
|
|
125
174
|
//#region src/components/PricingCard.tsx
|
|
126
|
-
const
|
|
127
|
-
|
|
175
|
+
const publicCtaLabel = (plan) => {
|
|
176
|
+
if (plan.priceAmount < 0) return "Contact sales";
|
|
177
|
+
if (isPlanFree(plan)) return "Start for free";
|
|
178
|
+
return "Start free trial";
|
|
179
|
+
};
|
|
180
|
+
const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, isCanceling = false, cancelDisabled = false, featured = false }) => {
|
|
128
181
|
const isActive = isSubscribed || isActiveSubscription(subscriptionStatus ?? "");
|
|
182
|
+
const features = listPlanFeatureLines(plan);
|
|
183
|
+
const priceLabel = formatPlanPrice(plan);
|
|
184
|
+
const isCustom = plan.priceAmount < 0;
|
|
185
|
+
const displayPrice = isCustom ? "Custom" : isPlanFree(plan) ? "$0" : priceLabel.replace(/\/mo$/, "").replace(/\/yr$/, "") || priceLabel;
|
|
129
186
|
return /* @__PURE__ */ jsxs("article", {
|
|
130
|
-
className: cn("flex
|
|
187
|
+
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"),
|
|
131
188
|
"data-testid": `pricing-card-${plan.sku}`,
|
|
132
189
|
children: [
|
|
133
190
|
/* @__PURE__ */ jsxs("div", {
|
|
134
|
-
className: "flex items-
|
|
135
|
-
children: [
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"
|
|
141
|
-
|
|
142
|
-
|
|
191
|
+
className: "flex items-center justify-between gap-2",
|
|
192
|
+
children: [
|
|
193
|
+
/* @__PURE__ */ jsx("h2", {
|
|
194
|
+
className: "text-sm font-bold uppercase tracking-[0.04em]",
|
|
195
|
+
children: plan.displayName
|
|
196
|
+
}),
|
|
197
|
+
featured && !isActive ? /* @__PURE__ */ jsx("span", {
|
|
198
|
+
className: "shrink-0 rounded-md bg-primary/10 px-2.5 py-0.5 text-xs font-semibold text-primary",
|
|
199
|
+
children: "Popular"
|
|
200
|
+
}) : null,
|
|
201
|
+
isActive && mode === "account" ? /* @__PURE__ */ jsx("span", {
|
|
202
|
+
className: "shrink-0 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary",
|
|
203
|
+
"data-testid": `subscribed-badge-${plan.sku}`,
|
|
204
|
+
children: "Subscribed"
|
|
205
|
+
}) : null
|
|
206
|
+
]
|
|
143
207
|
}),
|
|
144
208
|
/* @__PURE__ */ jsx("p", {
|
|
145
|
-
className: "mt-
|
|
146
|
-
children:
|
|
209
|
+
className: "mt-3.5 text-[2.1rem] font-extrabold tracking-[-0.02em]",
|
|
210
|
+
children: displayPrice
|
|
147
211
|
}),
|
|
148
212
|
/* @__PURE__ */ jsx("p", {
|
|
149
|
-
className: "mt-
|
|
150
|
-
children: formatBillingPeriod(plan.billingPeriod)
|
|
213
|
+
className: "mb-[18px] mt-0.5 text-[0.78125rem] text-muted-foreground",
|
|
214
|
+
children: isCustom ? "talk to us" : formatBillingPeriod(plan.billingPeriod)
|
|
151
215
|
}),
|
|
152
|
-
limitsSummary ? /* @__PURE__ */ jsx("p", {
|
|
153
|
-
className: "mt-4 text-sm text-muted-foreground",
|
|
154
|
-
children: limitsSummary
|
|
155
|
-
}) : null,
|
|
156
|
-
plan.includedActionKeys && plan.includedActionKeys.length > 0 ? /* @__PURE__ */ jsxs("div", {
|
|
157
|
-
className: "mt-4",
|
|
158
|
-
"data-testid": `included-endpoints-${plan.sku}`,
|
|
159
|
-
children: [/* @__PURE__ */ jsx("p", {
|
|
160
|
-
className: "text-xs font-medium uppercase tracking-wide text-muted-foreground",
|
|
161
|
-
children: "Included endpoints"
|
|
162
|
-
}), /* @__PURE__ */ jsx("ul", {
|
|
163
|
-
className: "mt-2 space-y-1 text-sm text-muted-foreground",
|
|
164
|
-
children: plan.includedActionKeys.map((actionKey) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("code", {
|
|
165
|
-
className: "rounded bg-muted px-1.5 py-0.5 text-xs",
|
|
166
|
-
children: formatActionKey(actionKey)
|
|
167
|
-
}) }, actionKey))
|
|
168
|
-
})]
|
|
169
|
-
}) : isPlanFree(plan) ? /* @__PURE__ */ jsx("p", {
|
|
170
|
-
className: "mt-4 text-sm text-muted-foreground",
|
|
171
|
-
children: "API access not included"
|
|
172
|
-
}) : null,
|
|
173
216
|
/* @__PURE__ */ jsx("div", {
|
|
174
|
-
className: "
|
|
217
|
+
className: "space-y-2",
|
|
175
218
|
children: mode === "account" ? isActive ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
176
219
|
/* @__PURE__ */ jsx(Button, {
|
|
177
220
|
variant: "secondary",
|
|
@@ -202,10 +245,10 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
|
|
|
202
245
|
children: "Unsubscribe is available after the portal is published."
|
|
203
246
|
}) : null
|
|
204
247
|
] }) : /* @__PURE__ */ jsx(Button, {
|
|
205
|
-
variant: "default",
|
|
248
|
+
variant: featured ? "default" : "outline",
|
|
206
249
|
className: "w-full",
|
|
207
250
|
asChild: true,
|
|
208
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
251
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
209
252
|
to: `/checkout?planSku=${encodeURIComponent(plan.sku)}`,
|
|
210
253
|
children: isPlanFree(plan) ? "Get started" : "Subscribe"
|
|
211
254
|
})
|
|
@@ -213,20 +256,32 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
|
|
|
213
256
|
variant: "secondary",
|
|
214
257
|
className: "w-full",
|
|
215
258
|
asChild: true,
|
|
216
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
259
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
217
260
|
to: "/account/subscriptions",
|
|
218
261
|
children: "Manage subscription"
|
|
219
262
|
})
|
|
220
263
|
}) : /* @__PURE__ */ jsx(Button, {
|
|
221
|
-
variant: "default",
|
|
264
|
+
variant: featured ? "default" : "outline",
|
|
222
265
|
className: "w-full",
|
|
223
266
|
asChild: true,
|
|
224
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
267
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
225
268
|
to: `/checkout?planSku=${encodeURIComponent(plan.sku)}`,
|
|
226
|
-
children:
|
|
269
|
+
children: publicCtaLabel(plan)
|
|
227
270
|
})
|
|
228
271
|
})
|
|
229
|
-
})
|
|
272
|
+
}),
|
|
273
|
+
features.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
274
|
+
className: "mt-5 flex flex-1 flex-col gap-[11px] border-t border-border pt-[18px]",
|
|
275
|
+
"data-testid": plan.includedActionKeys && plan.includedActionKeys.length > 0 ? `included-endpoints-${plan.sku}` : void 0,
|
|
276
|
+
children: features.map((feat) => /* @__PURE__ */ jsxs("div", {
|
|
277
|
+
className: "flex items-start gap-2.5 text-[0.84375rem] text-muted-foreground",
|
|
278
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
279
|
+
className: "mt-0.5 shrink-0 text-emerald-600",
|
|
280
|
+
"aria-hidden": true,
|
|
281
|
+
children: "✓"
|
|
282
|
+
}), /* @__PURE__ */ jsx("span", { children: feat })]
|
|
283
|
+
}, feat))
|
|
284
|
+
}) : null
|
|
230
285
|
]
|
|
231
286
|
});
|
|
232
287
|
};
|
|
@@ -308,6 +363,9 @@ function createDemoMeResponse(plans) {
|
|
|
308
363
|
return {
|
|
309
364
|
userId: "00000000-0000-0000-0000-000000000001",
|
|
310
365
|
displayName: "Demo User",
|
|
366
|
+
email: "demo@example.com",
|
|
367
|
+
emailVerified: true,
|
|
368
|
+
hasNativePassword: true,
|
|
311
369
|
subscriptions: resolveDemoPlans(plans).map((plan) => ({
|
|
312
370
|
planSku: plan.sku,
|
|
313
371
|
status: "Active",
|
|
@@ -594,6 +652,394 @@ async function readProblemDetail(response) {
|
|
|
594
652
|
}
|
|
595
653
|
}
|
|
596
654
|
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/hooks/useUsage.ts
|
|
657
|
+
function createDemoUsage(range) {
|
|
658
|
+
const now = /* @__PURE__ */ new Date();
|
|
659
|
+
const days = range === "24h" ? 1 : range === "7d" ? 7 : range === "90d" ? 90 : range === "mtd" ? Math.max(1, now.getUTCDate()) : 30;
|
|
660
|
+
const series = [];
|
|
661
|
+
let total = 0;
|
|
662
|
+
const today = /* @__PURE__ */ new Date();
|
|
663
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
664
|
+
const day = new Date(today);
|
|
665
|
+
day.setUTCDate(day.getUTCDate() - i);
|
|
666
|
+
const requestCount = 40 + i * 17 % 80;
|
|
667
|
+
total += requestCount;
|
|
668
|
+
series.push({
|
|
669
|
+
dayUtc: day.toISOString().slice(0, 10),
|
|
670
|
+
requestCount,
|
|
671
|
+
errorCount: Math.floor(requestCount * .02),
|
|
672
|
+
avgLatencyMs: 40 + i % 20
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
range,
|
|
677
|
+
fromDayUtc: series[0]?.dayUtc ?? today.toISOString().slice(0, 10),
|
|
678
|
+
toDayUtc: series.at(-1)?.dayUtc ?? today.toISOString().slice(0, 10),
|
|
679
|
+
requestCount: total,
|
|
680
|
+
avgLatencyMs: 48,
|
|
681
|
+
errorRate: .018,
|
|
682
|
+
series,
|
|
683
|
+
byEndpoint: [
|
|
684
|
+
{
|
|
685
|
+
path: "GET /pets",
|
|
686
|
+
count: Math.floor(total * .45)
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
path: "POST /pets",
|
|
690
|
+
count: Math.floor(total * .25)
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
path: "GET /pets/{id}",
|
|
694
|
+
count: Math.floor(total * .2)
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
path: "DELETE /pets/{id}",
|
|
698
|
+
count: Math.floor(total * .1)
|
|
699
|
+
}
|
|
700
|
+
],
|
|
701
|
+
byStatus: [
|
|
702
|
+
{
|
|
703
|
+
label: "2xx",
|
|
704
|
+
count: Math.floor(total * .96)
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
label: "4xx",
|
|
708
|
+
count: Math.floor(total * .03)
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
label: "5xx",
|
|
712
|
+
count: Math.floor(total * .01)
|
|
713
|
+
}
|
|
714
|
+
]
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function createDemoRecent() {
|
|
718
|
+
const now = Date.now();
|
|
719
|
+
return { items: [
|
|
720
|
+
{
|
|
721
|
+
occurredAtUtc: (/* @__PURE__ */ new Date(now - 6e4)).toISOString(),
|
|
722
|
+
method: "GET",
|
|
723
|
+
path: "/pets",
|
|
724
|
+
statusCode: 200,
|
|
725
|
+
latencyMs: 42
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
occurredAtUtc: (/* @__PURE__ */ new Date(now - 12e4)).toISOString(),
|
|
729
|
+
method: "POST",
|
|
730
|
+
path: "/pets",
|
|
731
|
+
statusCode: 201,
|
|
732
|
+
latencyMs: 88
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
occurredAtUtc: (/* @__PURE__ */ new Date(now - 18e4)).toISOString(),
|
|
736
|
+
method: "GET",
|
|
737
|
+
path: "/pets/1",
|
|
738
|
+
statusCode: 404,
|
|
739
|
+
latencyMs: 31
|
|
740
|
+
}
|
|
741
|
+
] };
|
|
742
|
+
}
|
|
743
|
+
function useUsage(range = "30d") {
|
|
744
|
+
const auth = useAuth();
|
|
745
|
+
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
746
|
+
const isPreview = useDevPortalPreviewMode();
|
|
747
|
+
const isDemoSession = useIsDemoSession();
|
|
748
|
+
const liveQuery = useQuery({
|
|
749
|
+
queryKey: [
|
|
750
|
+
"dev-portal-usage",
|
|
751
|
+
apiBaseUrl,
|
|
752
|
+
range
|
|
753
|
+
],
|
|
754
|
+
enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
|
|
755
|
+
queryFn: async () => {
|
|
756
|
+
const response = await fetch(`${apiBaseUrl}/api/v1/usage?range=${encodeURIComponent(range)}`, {
|
|
757
|
+
method: "GET",
|
|
758
|
+
headers: { Accept: "application/json" },
|
|
759
|
+
credentials: "include"
|
|
760
|
+
});
|
|
761
|
+
if (!response.ok) throw new Error("Failed to load usage.");
|
|
762
|
+
return await response.json();
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
if (isDemoSession && auth.isAuthenticated) return {
|
|
766
|
+
...liveQuery,
|
|
767
|
+
data: createDemoUsage(range),
|
|
768
|
+
error: null,
|
|
769
|
+
isError: false,
|
|
770
|
+
isFetching: false,
|
|
771
|
+
isLoading: false,
|
|
772
|
+
isPending: false,
|
|
773
|
+
isSuccess: true,
|
|
774
|
+
status: "success"
|
|
775
|
+
};
|
|
776
|
+
return liveQuery;
|
|
777
|
+
}
|
|
778
|
+
function useUsageRecent(limit = 20) {
|
|
779
|
+
const auth = useAuth();
|
|
780
|
+
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
781
|
+
const isPreview = useDevPortalPreviewMode();
|
|
782
|
+
const isDemoSession = useIsDemoSession();
|
|
783
|
+
const liveQuery = useQuery({
|
|
784
|
+
queryKey: [
|
|
785
|
+
"dev-portal-usage-recent",
|
|
786
|
+
apiBaseUrl,
|
|
787
|
+
limit
|
|
788
|
+
],
|
|
789
|
+
enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
|
|
790
|
+
queryFn: async () => {
|
|
791
|
+
const response = await fetch(`${apiBaseUrl}/api/v1/usage/recent?limit=${limit}`, {
|
|
792
|
+
method: "GET",
|
|
793
|
+
headers: { Accept: "application/json" },
|
|
794
|
+
credentials: "include"
|
|
795
|
+
});
|
|
796
|
+
if (!response.ok) throw new Error("Failed to load recent requests.");
|
|
797
|
+
return await response.json();
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
if (isDemoSession && auth.isAuthenticated) return {
|
|
801
|
+
...liveQuery,
|
|
802
|
+
data: createDemoRecent(),
|
|
803
|
+
error: null,
|
|
804
|
+
isError: false,
|
|
805
|
+
isFetching: false,
|
|
806
|
+
isLoading: false,
|
|
807
|
+
isPending: false,
|
|
808
|
+
isSuccess: true,
|
|
809
|
+
status: "success"
|
|
810
|
+
};
|
|
811
|
+
return liveQuery;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
//#endregion
|
|
815
|
+
//#region src/paths.ts
|
|
816
|
+
const PRICING_PATH = "/pricing";
|
|
817
|
+
const CHECKOUT_PATH = "/checkout";
|
|
818
|
+
const SUBSCRIPTIONS_PATH = "/account/subscriptions";
|
|
819
|
+
const CREDENTIALS_PATH = "/account/credentials";
|
|
820
|
+
const USAGE_PATH = "/account/usage";
|
|
821
|
+
const LOGIN_NAV_PATH = "/signin";
|
|
822
|
+
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region src/pages/OverviewDashboardPage.tsx
|
|
825
|
+
function StatCard({ label, value, sub, subClassName }) {
|
|
826
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
827
|
+
className: "rounded-xl border border-border bg-card p-5",
|
|
828
|
+
children: [
|
|
829
|
+
/* @__PURE__ */ jsx("div", {
|
|
830
|
+
className: "text-xs font-medium uppercase tracking-[0.05em] text-muted-foreground",
|
|
831
|
+
children: label
|
|
832
|
+
}),
|
|
833
|
+
/* @__PURE__ */ jsx("div", {
|
|
834
|
+
className: "mt-1.5 text-[1.7rem] font-bold tracking-[-0.02em] text-foreground",
|
|
835
|
+
children: value
|
|
836
|
+
}),
|
|
837
|
+
/* @__PURE__ */ jsx("div", {
|
|
838
|
+
className: `mt-1 text-xs ${subClassName ?? "text-muted-foreground"}`,
|
|
839
|
+
children: sub
|
|
840
|
+
})
|
|
841
|
+
]
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
function formatNumber$1(value) {
|
|
845
|
+
return new Intl.NumberFormat().format(value);
|
|
846
|
+
}
|
|
847
|
+
const OverviewDashboardPage = () => {
|
|
848
|
+
const auth = useAuth();
|
|
849
|
+
const { data: apiKeys, isLoading: keysLoading } = useApiKeys();
|
|
850
|
+
const { data: me, isLoading: subsLoading } = useMySubscriptions();
|
|
851
|
+
const { data: catalog } = usePlans();
|
|
852
|
+
const { data: usage } = useUsage("30d");
|
|
853
|
+
const { data: recent } = useUsageRecent(8);
|
|
854
|
+
const activeKeyCount = apiKeys?.plans.reduce((total, plan) => total + plan.keys.length, 0) ?? 0;
|
|
855
|
+
const activeSubscription = (me?.subscriptions ?? []).find((sub) => isActiveSubscription(sub.status));
|
|
856
|
+
const planSku = activeSubscription?.planSku;
|
|
857
|
+
const planName = catalog?.plans.find((plan) => plan.sku === planSku)?.displayName ?? planSku ?? "—";
|
|
858
|
+
const renewLabel = activeSubscription?.currentPeriodEnd ? `Renews ${new Date(activeSubscription.currentPeriodEnd).toLocaleDateString(void 0, {
|
|
859
|
+
month: "short",
|
|
860
|
+
day: "numeric"
|
|
861
|
+
})}` : "No active subscription";
|
|
862
|
+
const statsLoading = auth.isAuthenticated && (keysLoading || subsLoading) && !apiKeys && !me;
|
|
863
|
+
const chartSeries = (usage?.series ?? []).slice(-14);
|
|
864
|
+
const maxBar = Math.max(1, ...chartSeries.map((point) => point.requestCount));
|
|
865
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
866
|
+
className: "px-8 py-7",
|
|
867
|
+
children: [
|
|
868
|
+
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Overview" }) }),
|
|
869
|
+
/* @__PURE__ */ jsx("h1", {
|
|
870
|
+
className: "mb-6 text-[1.6rem] font-bold tracking-[-0.01em] text-foreground",
|
|
871
|
+
children: "Overview"
|
|
872
|
+
}),
|
|
873
|
+
/* @__PURE__ */ jsxs("div", {
|
|
874
|
+
className: "mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4",
|
|
875
|
+
children: [
|
|
876
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
877
|
+
label: "Requests (30d)",
|
|
878
|
+
value: usage ? formatNumber$1(usage.requestCount) : statsLoading ? "…" : "—",
|
|
879
|
+
sub: usage ? `${formatNumber$1(usage.series.reduce((sum, p) => sum + p.errorCount, 0))} errors` : "No usage data yet"
|
|
880
|
+
}),
|
|
881
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
882
|
+
label: "Active keys",
|
|
883
|
+
value: statsLoading ? "…" : String(activeKeyCount),
|
|
884
|
+
sub: activeKeyCount === 1 ? "1 key on your account" : `${activeKeyCount} keys on your account`
|
|
885
|
+
}),
|
|
886
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
887
|
+
label: "Plan",
|
|
888
|
+
value: statsLoading ? "…" : planName,
|
|
889
|
+
sub: renewLabel
|
|
890
|
+
}),
|
|
891
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
892
|
+
label: "Error rate",
|
|
893
|
+
value: usage ? `${(usage.errorRate * 100).toFixed(usage.errorRate >= .1 ? 1 : 2)}%` : "—",
|
|
894
|
+
sub: usage?.avgLatencyMs != null ? `${usage.avgLatencyMs} ms avg latency` : "No telemetry yet"
|
|
895
|
+
})
|
|
896
|
+
]
|
|
897
|
+
}),
|
|
898
|
+
/* @__PURE__ */ jsxs("div", {
|
|
899
|
+
className: "mb-5 grid grid-cols-1 gap-5 lg:grid-cols-2",
|
|
900
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
901
|
+
className: "rounded-xl border border-border bg-card p-6",
|
|
902
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
903
|
+
className: "mb-4 flex items-center justify-between gap-3",
|
|
904
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
905
|
+
className: "text-lg font-semibold text-foreground",
|
|
906
|
+
children: "Requests, last 14 days"
|
|
907
|
+
}), /* @__PURE__ */ jsx(Link, {
|
|
908
|
+
to: USAGE_PATH,
|
|
909
|
+
className: "text-sm font-semibold text-primary hover:underline",
|
|
910
|
+
children: "View usage"
|
|
911
|
+
})]
|
|
912
|
+
}), chartSeries.some((p) => p.requestCount > 0) ? /* @__PURE__ */ jsx("div", {
|
|
913
|
+
className: "flex h-[150px] items-end gap-1.5",
|
|
914
|
+
children: chartSeries.map((point) => /* @__PURE__ */ jsx("div", {
|
|
915
|
+
title: `${point.dayUtc}: ${point.requestCount}`,
|
|
916
|
+
className: "min-w-0 flex-1 rounded-t-sm bg-primary",
|
|
917
|
+
style: { height: `${Math.max(4, point.requestCount / maxBar * 100)}%` }
|
|
918
|
+
}, point.dayUtc))
|
|
919
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
920
|
+
className: "flex min-h-[150px] items-center justify-center rounded-lg border border-dashed border-border bg-muted/30 px-4 text-center text-sm text-muted-foreground",
|
|
921
|
+
children: "No request traffic in the last 14 days."
|
|
922
|
+
})]
|
|
923
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
924
|
+
className: "rounded-xl border border-border bg-card p-6",
|
|
925
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
926
|
+
className: "mb-4 text-lg font-semibold text-foreground",
|
|
927
|
+
children: "Quick links"
|
|
928
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
929
|
+
className: "flex flex-col gap-3",
|
|
930
|
+
children: [
|
|
931
|
+
/* @__PURE__ */ jsx(Button$1, {
|
|
932
|
+
asChild: true,
|
|
933
|
+
variant: "outline",
|
|
934
|
+
className: "justify-between",
|
|
935
|
+
children: /* @__PURE__ */ jsxs(Link, {
|
|
936
|
+
to: CREDENTIALS_PATH,
|
|
937
|
+
children: ["Manage API keys", /* @__PURE__ */ jsx(ArrowRightIcon, { size: 16 })]
|
|
938
|
+
})
|
|
939
|
+
}),
|
|
940
|
+
/* @__PURE__ */ jsx(Button$1, {
|
|
941
|
+
asChild: true,
|
|
942
|
+
variant: "outline",
|
|
943
|
+
className: "justify-between",
|
|
944
|
+
children: /* @__PURE__ */ jsxs(Link, {
|
|
945
|
+
to: USAGE_PATH,
|
|
946
|
+
children: ["Usage analytics", /* @__PURE__ */ jsx(ArrowRightIcon, { size: 16 })]
|
|
947
|
+
})
|
|
948
|
+
}),
|
|
949
|
+
/* @__PURE__ */ jsx(Button$1, {
|
|
950
|
+
asChild: true,
|
|
951
|
+
variant: "outline",
|
|
952
|
+
className: "justify-between",
|
|
953
|
+
children: /* @__PURE__ */ jsxs(Link, {
|
|
954
|
+
to: SUBSCRIPTIONS_PATH,
|
|
955
|
+
children: ["Billing & subscriptions", /* @__PURE__ */ jsx(ArrowRightIcon, { size: 16 })]
|
|
956
|
+
})
|
|
957
|
+
}),
|
|
958
|
+
/* @__PURE__ */ jsx(Button$1, {
|
|
959
|
+
asChild: true,
|
|
960
|
+
variant: "outline",
|
|
961
|
+
className: "justify-between",
|
|
962
|
+
children: /* @__PURE__ */ jsxs(Link, {
|
|
963
|
+
to: "/introduction",
|
|
964
|
+
children: ["Documentation", /* @__PURE__ */ jsx(ArrowRightIcon, { size: 16 })]
|
|
965
|
+
})
|
|
966
|
+
})
|
|
967
|
+
]
|
|
968
|
+
})]
|
|
969
|
+
})]
|
|
970
|
+
}),
|
|
971
|
+
/* @__PURE__ */ jsxs("div", {
|
|
972
|
+
className: "rounded-xl border border-border bg-card p-6",
|
|
973
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
974
|
+
className: "mb-4 text-lg font-semibold text-foreground",
|
|
975
|
+
children: "Recent requests"
|
|
976
|
+
}), recent && recent.items.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
977
|
+
className: "overflow-x-auto",
|
|
978
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
979
|
+
className: "w-full text-left text-sm",
|
|
980
|
+
children: [/* @__PURE__ */ jsx("thead", {
|
|
981
|
+
className: "text-xs uppercase tracking-wide text-muted-foreground",
|
|
982
|
+
children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
983
|
+
/* @__PURE__ */ jsx("th", {
|
|
984
|
+
className: "pb-2 font-medium",
|
|
985
|
+
children: "Time"
|
|
986
|
+
}),
|
|
987
|
+
/* @__PURE__ */ jsx("th", {
|
|
988
|
+
className: "pb-2 font-medium",
|
|
989
|
+
children: "Method"
|
|
990
|
+
}),
|
|
991
|
+
/* @__PURE__ */ jsx("th", {
|
|
992
|
+
className: "pb-2 font-medium",
|
|
993
|
+
children: "Path"
|
|
994
|
+
}),
|
|
995
|
+
/* @__PURE__ */ jsx("th", {
|
|
996
|
+
className: "pb-2 font-medium",
|
|
997
|
+
children: "Status"
|
|
998
|
+
}),
|
|
999
|
+
/* @__PURE__ */ jsx("th", {
|
|
1000
|
+
className: "pb-2 font-medium",
|
|
1001
|
+
children: "Latency"
|
|
1002
|
+
})
|
|
1003
|
+
] })
|
|
1004
|
+
}), /* @__PURE__ */ jsx("tbody", { children: recent.items.map((item) => /* @__PURE__ */ jsxs("tr", {
|
|
1005
|
+
className: "border-t border-border",
|
|
1006
|
+
children: [
|
|
1007
|
+
/* @__PURE__ */ jsx("td", {
|
|
1008
|
+
className: "py-2 text-muted-foreground",
|
|
1009
|
+
children: new Date(item.occurredAtUtc).toLocaleString()
|
|
1010
|
+
}),
|
|
1011
|
+
/* @__PURE__ */ jsx("td", {
|
|
1012
|
+
className: "py-2 font-mono",
|
|
1013
|
+
children: item.method
|
|
1014
|
+
}),
|
|
1015
|
+
/* @__PURE__ */ jsx("td", {
|
|
1016
|
+
className: "py-2 font-mono",
|
|
1017
|
+
children: item.path
|
|
1018
|
+
}),
|
|
1019
|
+
/* @__PURE__ */ jsx("td", {
|
|
1020
|
+
className: "py-2",
|
|
1021
|
+
children: item.statusCode
|
|
1022
|
+
}),
|
|
1023
|
+
/* @__PURE__ */ jsx("td", {
|
|
1024
|
+
className: "py-2 text-muted-foreground",
|
|
1025
|
+
children: item.latencyMs != null ? `${item.latencyMs} ms` : "—"
|
|
1026
|
+
})
|
|
1027
|
+
]
|
|
1028
|
+
}, `${item.occurredAtUtc}-${item.method}-${item.path}`)) })]
|
|
1029
|
+
})
|
|
1030
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
1031
|
+
className: "text-sm text-muted-foreground",
|
|
1032
|
+
children: "No recent requests yet."
|
|
1033
|
+
})]
|
|
1034
|
+
}),
|
|
1035
|
+
!auth.isAuthenticated && auth.isAuthEnabled ? /* @__PURE__ */ jsx("p", {
|
|
1036
|
+
className: "mt-6 text-sm text-muted-foreground",
|
|
1037
|
+
children: "Sign in to see personalized account metrics."
|
|
1038
|
+
}) : null
|
|
1039
|
+
]
|
|
1040
|
+
});
|
|
1041
|
+
};
|
|
1042
|
+
|
|
597
1043
|
//#endregion
|
|
598
1044
|
//#region src/components/ApiKeysPanel.tsx
|
|
599
1045
|
/**
|
|
@@ -818,14 +1264,6 @@ const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
|
|
|
818
1264
|
});
|
|
819
1265
|
};
|
|
820
1266
|
|
|
821
|
-
//#endregion
|
|
822
|
-
//#region src/paths.ts
|
|
823
|
-
const PRICING_PATH = "/pricing";
|
|
824
|
-
const CHECKOUT_PATH = "/checkout";
|
|
825
|
-
const SUBSCRIPTIONS_PATH = "/account/subscriptions";
|
|
826
|
-
const CREDENTIALS_PATH = "/account/credentials";
|
|
827
|
-
const LOGIN_NAV_PATH = "/signin";
|
|
828
|
-
|
|
829
1267
|
//#endregion
|
|
830
1268
|
//#region src/pages/CheckoutPage.tsx
|
|
831
1269
|
const ACCOUNT_PLANS_PATH = "/account/plans";
|
|
@@ -873,7 +1311,7 @@ const CheckoutPage = () => {
|
|
|
873
1311
|
children: [
|
|
874
1312
|
"Missing plan SKU.",
|
|
875
1313
|
" ",
|
|
876
|
-
/* @__PURE__ */ jsx(Link, {
|
|
1314
|
+
/* @__PURE__ */ jsx(Link$1, {
|
|
877
1315
|
to: plansPath,
|
|
878
1316
|
className: "text-primary underline",
|
|
879
1317
|
children: "Back to plans"
|
|
@@ -894,7 +1332,7 @@ const CheckoutPage = () => {
|
|
|
894
1332
|
/* @__PURE__ */ jsx(Button$2, {
|
|
895
1333
|
asChild: true,
|
|
896
1334
|
variant: "outline",
|
|
897
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1335
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
898
1336
|
to: "/account/subscriptions",
|
|
899
1337
|
children: "View demo subscriptions"
|
|
900
1338
|
})
|
|
@@ -915,7 +1353,7 @@ const CheckoutPage = () => {
|
|
|
915
1353
|
/* @__PURE__ */ jsx(Button$2, {
|
|
916
1354
|
asChild: true,
|
|
917
1355
|
variant: "outline",
|
|
918
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1356
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
919
1357
|
to: plansPath,
|
|
920
1358
|
children: "Back to plans"
|
|
921
1359
|
})
|
|
@@ -937,7 +1375,7 @@ const CheckoutPage = () => {
|
|
|
937
1375
|
variant: "outline",
|
|
938
1376
|
size: "xs",
|
|
939
1377
|
asChild: true,
|
|
940
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1378
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
941
1379
|
to: plansPath,
|
|
942
1380
|
children: "Back to plans"
|
|
943
1381
|
})
|
|
@@ -1033,7 +1471,7 @@ const CredentialsPage = () => {
|
|
|
1033
1471
|
}), /* @__PURE__ */ jsx(Button$2, {
|
|
1034
1472
|
className: "mt-4",
|
|
1035
1473
|
asChild: true,
|
|
1036
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1474
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1037
1475
|
to: "/account/plans",
|
|
1038
1476
|
children: "View plans"
|
|
1039
1477
|
})
|
|
@@ -1051,7 +1489,7 @@ const CredentialsPage = () => {
|
|
|
1051
1489
|
}), /* @__PURE__ */ jsx(Button$2, {
|
|
1052
1490
|
className: "mt-4",
|
|
1053
1491
|
asChild: true,
|
|
1054
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1492
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1055
1493
|
to: "/account/plans",
|
|
1056
1494
|
children: "View plans"
|
|
1057
1495
|
})
|
|
@@ -1067,7 +1505,7 @@ const CredentialsPage = () => {
|
|
|
1067
1505
|
}), /* @__PURE__ */ jsx(Button$2, {
|
|
1068
1506
|
className: "mt-4",
|
|
1069
1507
|
asChild: true,
|
|
1070
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1508
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1071
1509
|
to: "/account/plans",
|
|
1072
1510
|
children: "View plans"
|
|
1073
1511
|
})
|
|
@@ -1101,14 +1539,14 @@ const CredentialsPage = () => {
|
|
|
1101
1539
|
children: [/* @__PURE__ */ jsx(Button$2, {
|
|
1102
1540
|
variant: "outline",
|
|
1103
1541
|
asChild: true,
|
|
1104
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1542
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1105
1543
|
to: "/account/subscriptions",
|
|
1106
1544
|
children: "My subscriptions"
|
|
1107
1545
|
})
|
|
1108
1546
|
}), /* @__PURE__ */ jsx(Button$2, {
|
|
1109
1547
|
variant: "outline",
|
|
1110
1548
|
asChild: true,
|
|
1111
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1549
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1112
1550
|
to: "/api",
|
|
1113
1551
|
children: "API reference"
|
|
1114
1552
|
})
|
|
@@ -1127,6 +1565,69 @@ function isPreviewPricingEnabled() {
|
|
|
1127
1565
|
|
|
1128
1566
|
//#endregion
|
|
1129
1567
|
//#region src/pages/PricingPage.tsx
|
|
1568
|
+
const DEFAULT_COMPARE_ROWS = [
|
|
1569
|
+
{
|
|
1570
|
+
label: "Monthly requests",
|
|
1571
|
+
values: [
|
|
1572
|
+
"1,000",
|
|
1573
|
+
"50,000",
|
|
1574
|
+
"500,000",
|
|
1575
|
+
"Unlimited"
|
|
1576
|
+
]
|
|
1577
|
+
},
|
|
1578
|
+
{
|
|
1579
|
+
label: "Rate limit",
|
|
1580
|
+
values: [
|
|
1581
|
+
"10/sec",
|
|
1582
|
+
"50/sec",
|
|
1583
|
+
"200/sec",
|
|
1584
|
+
"Custom"
|
|
1585
|
+
]
|
|
1586
|
+
},
|
|
1587
|
+
{
|
|
1588
|
+
label: "Team seats",
|
|
1589
|
+
values: [
|
|
1590
|
+
"1",
|
|
1591
|
+
"1",
|
|
1592
|
+
"5",
|
|
1593
|
+
"Unlimited"
|
|
1594
|
+
]
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
label: "Webhooks",
|
|
1598
|
+
values: [
|
|
1599
|
+
"—",
|
|
1600
|
+
"Included",
|
|
1601
|
+
"Included",
|
|
1602
|
+
"Included"
|
|
1603
|
+
]
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
label: "SSO",
|
|
1607
|
+
values: [
|
|
1608
|
+
"—",
|
|
1609
|
+
"—",
|
|
1610
|
+
"—",
|
|
1611
|
+
"Included"
|
|
1612
|
+
]
|
|
1613
|
+
},
|
|
1614
|
+
{
|
|
1615
|
+
label: "Support",
|
|
1616
|
+
values: [
|
|
1617
|
+
"Community",
|
|
1618
|
+
"Email",
|
|
1619
|
+
"Priority",
|
|
1620
|
+
"Dedicated SLA"
|
|
1621
|
+
]
|
|
1622
|
+
}
|
|
1623
|
+
];
|
|
1624
|
+
const DEFAULT_FAQS = [{
|
|
1625
|
+
question: "What happens if I exceed my quota?",
|
|
1626
|
+
answer: "Requests over your monthly quota return a 429 response so you can test rate-limit handling instead of being cut off unexpectedly."
|
|
1627
|
+
}, {
|
|
1628
|
+
question: "Can I change plans later?",
|
|
1629
|
+
answer: "Yes, upgrade or downgrade any time from your billing page. Changes apply on your next billing cycle."
|
|
1630
|
+
}];
|
|
1130
1631
|
const PricingPage = () => {
|
|
1131
1632
|
const { pricing, previewPlans } = useDevPortalBillingConfig();
|
|
1132
1633
|
const auth = useAuth();
|
|
@@ -1146,66 +1647,160 @@ const PricingPage = () => {
|
|
|
1146
1647
|
replace: true
|
|
1147
1648
|
});
|
|
1148
1649
|
const activeSkus = new Set((me?.subscriptions ?? []).filter((sub) => isActiveSubscription(sub.status)).map((sub) => sub.planSku));
|
|
1149
|
-
const title = pricing?.title ?? "
|
|
1150
|
-
const subtitle = pricing?.subtitle ?? "
|
|
1650
|
+
const title = pricing?.title ?? "Pay for what you call";
|
|
1651
|
+
const subtitle = pricing?.subtitle ?? "Every plan includes the full endpoint catalog. Higher tiers raise your request quota, rate limit, and support level.";
|
|
1652
|
+
const plans = catalog?.plans ?? [];
|
|
1653
|
+
const featuredSku = plans.find((p) => p.sku === "developer")?.sku ?? plans.find((p) => p.priceAmount > 0 && p.priceAmount < 50)?.sku ?? plans.find((p) => p.priceAmount > 0)?.sku;
|
|
1654
|
+
const compareRows = pricing?.compareRows && pricing.compareRows.length > 0 ? pricing.compareRows : DEFAULT_COMPARE_ROWS;
|
|
1655
|
+
const faqs = pricing?.faqs && pricing.faqs.length > 0 ? pricing.faqs : DEFAULT_FAQS;
|
|
1656
|
+
const compareHeaders = plans.length > 0 ? plans.map((p) => p.displayName) : [
|
|
1657
|
+
"Free",
|
|
1658
|
+
"Developer",
|
|
1659
|
+
"Team",
|
|
1660
|
+
"Enterprise"
|
|
1661
|
+
];
|
|
1151
1662
|
return /* @__PURE__ */ jsxs("div", {
|
|
1152
|
-
className: "w-full
|
|
1663
|
+
className: "w-full",
|
|
1153
1664
|
children: [
|
|
1154
1665
|
/* @__PURE__ */ jsxs(Head, { children: [/* @__PURE__ */ jsx("title", { children: title }), /* @__PURE__ */ jsx("meta", {
|
|
1155
1666
|
name: "description",
|
|
1156
1667
|
content: subtitle
|
|
1157
1668
|
})] }),
|
|
1158
|
-
/* @__PURE__ */ jsxs("
|
|
1159
|
-
className: "
|
|
1160
|
-
children: [/* @__PURE__ */ jsx(Heading, {
|
|
1161
|
-
level: 1,
|
|
1162
|
-
"data-testid": "title",
|
|
1163
|
-
children: title
|
|
1164
|
-
}), /* @__PURE__ */ jsx("p", {
|
|
1165
|
-
className: "text-muted-foreground",
|
|
1166
|
-
"data-testid": "subtitle",
|
|
1167
|
-
children: subtitle
|
|
1168
|
-
})]
|
|
1169
|
-
}),
|
|
1170
|
-
isLoading ? /* @__PURE__ */ jsx("p", {
|
|
1171
|
-
className: "text-center text-muted-foreground",
|
|
1172
|
-
children: "Loading plans…"
|
|
1173
|
-
}) : null,
|
|
1174
|
-
isError ? /* @__PURE__ */ jsx("p", {
|
|
1175
|
-
className: "text-center text-destructive",
|
|
1176
|
-
children: "Unable to load plans."
|
|
1177
|
-
}) : null,
|
|
1178
|
-
catalog && catalog.plans.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
1179
|
-
className: "grid grid-cols-1 justify-center gap-6 sm:grid-cols-[repeat(auto-fit,minmax(280px,max-content))]",
|
|
1180
|
-
children: catalog.plans.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
|
|
1181
|
-
plan,
|
|
1182
|
-
mode: "public",
|
|
1183
|
-
isSubscribed: activeSkus.has(plan.sku)
|
|
1184
|
-
}, plan.sku))
|
|
1185
|
-
}) : !isLoading && !isError ? /* @__PURE__ */ jsxs("div", {
|
|
1186
|
-
className: "mx-auto max-w-lg rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
|
|
1669
|
+
/* @__PURE__ */ jsxs("section", {
|
|
1670
|
+
className: "px-6 pb-4 pt-[clamp(3rem,6vw,4.5rem)] text-center",
|
|
1187
1671
|
children: [
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
className: "rounded bg-muted px-1.5 py-0.5",
|
|
1192
|
-
children: "apitogo.config.json"
|
|
1672
|
+
/* @__PURE__ */ jsx("p", {
|
|
1673
|
+
className: "text-xs font-bold uppercase tracking-[0.08em] text-primary",
|
|
1674
|
+
children: "Pricing"
|
|
1193
1675
|
}),
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
children: "modules.userPanel.plans.enabled"
|
|
1676
|
+
/* @__PURE__ */ jsx(Heading, {
|
|
1677
|
+
level: 1,
|
|
1678
|
+
className: "mt-3 text-[clamp(2rem,3.5vw,3rem)] font-extrabold tracking-[-0.02em]",
|
|
1679
|
+
"data-testid": "title",
|
|
1680
|
+
children: title
|
|
1200
1681
|
}),
|
|
1201
|
-
|
|
1202
|
-
|
|
1682
|
+
/* @__PURE__ */ jsx("p", {
|
|
1683
|
+
className: "mx-auto mt-3.5 max-w-[34rem] text-lg leading-relaxed text-muted-foreground",
|
|
1684
|
+
"data-testid": "subtitle",
|
|
1685
|
+
children: subtitle
|
|
1686
|
+
})
|
|
1203
1687
|
]
|
|
1204
|
-
})
|
|
1688
|
+
}),
|
|
1689
|
+
/* @__PURE__ */ jsxs("section", {
|
|
1690
|
+
className: "px-6 pb-16 pt-8",
|
|
1691
|
+
children: [
|
|
1692
|
+
isLoading ? /* @__PURE__ */ jsx("p", {
|
|
1693
|
+
className: "text-center text-muted-foreground",
|
|
1694
|
+
children: "Loading plans…"
|
|
1695
|
+
}) : null,
|
|
1696
|
+
isError ? /* @__PURE__ */ jsx("p", {
|
|
1697
|
+
className: "text-center text-destructive",
|
|
1698
|
+
children: "Unable to load plans."
|
|
1699
|
+
}) : null,
|
|
1700
|
+
plans.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
1701
|
+
className: "mx-auto grid max-w-[1200px] grid-cols-1 items-stretch gap-5 sm:grid-cols-2 lg:grid-cols-4",
|
|
1702
|
+
children: plans.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
|
|
1703
|
+
plan,
|
|
1704
|
+
mode: "public",
|
|
1705
|
+
isSubscribed: activeSkus.has(plan.sku),
|
|
1706
|
+
featured: plan.sku === featuredSku
|
|
1707
|
+
}, plan.sku))
|
|
1708
|
+
}) : !isLoading && !isError ? /* @__PURE__ */ jsxs("div", {
|
|
1709
|
+
className: "mx-auto max-w-lg rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
|
|
1710
|
+
children: [
|
|
1711
|
+
"No plans are configured yet. Add plans in",
|
|
1712
|
+
" ",
|
|
1713
|
+
/* @__PURE__ */ jsx("code", {
|
|
1714
|
+
className: "rounded bg-muted px-1.5 py-0.5",
|
|
1715
|
+
children: "apitogo.config.json"
|
|
1716
|
+
}),
|
|
1717
|
+
" ",
|
|
1718
|
+
"and set",
|
|
1719
|
+
" ",
|
|
1720
|
+
/* @__PURE__ */ jsx("code", {
|
|
1721
|
+
className: "rounded bg-muted px-1.5 py-0.5",
|
|
1722
|
+
children: "modules.userPanel.plans.enabled"
|
|
1723
|
+
}),
|
|
1724
|
+
" ",
|
|
1725
|
+
"to true."
|
|
1726
|
+
]
|
|
1727
|
+
}) : null
|
|
1728
|
+
]
|
|
1729
|
+
}),
|
|
1730
|
+
/* @__PURE__ */ jsx("section", {
|
|
1731
|
+
className: "border-t border-border bg-muted/40 px-6 py-16",
|
|
1732
|
+
"data-testid": "pricing-compare",
|
|
1733
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1734
|
+
className: "mx-auto max-w-[1200px]",
|
|
1735
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
1736
|
+
className: "mb-6 text-[clamp(1.5rem,2.5vw,2.25rem)] font-extrabold tracking-[-0.02em]",
|
|
1737
|
+
children: "Compare plans"
|
|
1738
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1739
|
+
className: "overflow-x-auto rounded-xl border border-border bg-card",
|
|
1740
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1741
|
+
className: "min-w-[720px]",
|
|
1742
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1743
|
+
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",
|
|
1744
|
+
style: { gridTemplateColumns: `minmax(10rem,1.4fr) repeat(${compareHeaders.length}, minmax(6rem,1fr))` },
|
|
1745
|
+
children: [/* @__PURE__ */ jsx("div", { children: "Feature" }), compareHeaders.map((header) => /* @__PURE__ */ jsx("div", { children: header }, header))]
|
|
1746
|
+
}), compareRows.map((row) => /* @__PURE__ */ jsxs("div", {
|
|
1747
|
+
className: "grid gap-0 border-b border-border px-5 py-3.5 text-[0.84375rem] text-muted-foreground last:border-b-0",
|
|
1748
|
+
style: { gridTemplateColumns: `minmax(10rem,1.4fr) repeat(${compareHeaders.length}, minmax(6rem,1fr))` },
|
|
1749
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1750
|
+
className: "font-medium text-foreground",
|
|
1751
|
+
children: row.label
|
|
1752
|
+
}), compareHeaders.map((_, index) => /* @__PURE__ */ jsx("div", { children: row.values[index] ?? "—" }, `${row.label}-${index}`))]
|
|
1753
|
+
}, row.label))]
|
|
1754
|
+
})
|
|
1755
|
+
})]
|
|
1756
|
+
})
|
|
1757
|
+
}),
|
|
1758
|
+
/* @__PURE__ */ jsx("section", {
|
|
1759
|
+
className: "px-6 py-16",
|
|
1760
|
+
"data-testid": "pricing-faq",
|
|
1761
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1762
|
+
className: "mx-auto max-w-4xl",
|
|
1763
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
1764
|
+
className: "mb-8 text-center text-[clamp(1.5rem,2.5vw,2.25rem)] font-extrabold tracking-[-0.02em]",
|
|
1765
|
+
children: "Frequently asked questions"
|
|
1766
|
+
}), /* @__PURE__ */ jsx("div", { children: faqs.map((faq) => /* @__PURE__ */ jsxs("div", {
|
|
1767
|
+
className: "border-b border-border py-5 last:border-b-0",
|
|
1768
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1769
|
+
className: "text-[0.9375rem] font-bold",
|
|
1770
|
+
children: faq.question
|
|
1771
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1772
|
+
className: "mt-2 text-sm leading-relaxed text-muted-foreground",
|
|
1773
|
+
children: faq.answer
|
|
1774
|
+
})]
|
|
1775
|
+
}, faq.question)) })]
|
|
1776
|
+
})
|
|
1777
|
+
})
|
|
1205
1778
|
]
|
|
1206
1779
|
});
|
|
1207
1780
|
};
|
|
1208
1781
|
|
|
1782
|
+
//#endregion
|
|
1783
|
+
//#region src/hooks/useBillingPortal.ts
|
|
1784
|
+
function useBillingPortal() {
|
|
1785
|
+
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
1786
|
+
return useMutation({ mutationFn: async (returnUrl) => {
|
|
1787
|
+
if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
|
|
1788
|
+
const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/billing-portal`, {
|
|
1789
|
+
method: "POST",
|
|
1790
|
+
headers: {
|
|
1791
|
+
Accept: "application/json",
|
|
1792
|
+
"Content-Type": "application/json"
|
|
1793
|
+
},
|
|
1794
|
+
credentials: "include",
|
|
1795
|
+
body: JSON.stringify({ returnUrl: returnUrl ?? `${window.location.origin}/account/subscriptions` })
|
|
1796
|
+
});
|
|
1797
|
+
if (!response.ok) throw new Error("Unable to open billing portal.");
|
|
1798
|
+
const body = await response.json();
|
|
1799
|
+
if (!body.url) throw new Error("Billing portal URL was not returned.");
|
|
1800
|
+
window.location.assign(body.url);
|
|
1801
|
+
} });
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1209
1804
|
//#endregion
|
|
1210
1805
|
//#region src/pages/SubscriptionsPage.tsx
|
|
1211
1806
|
const SubscriptionsPage = () => {
|
|
@@ -1214,28 +1809,33 @@ const SubscriptionsPage = () => {
|
|
|
1214
1809
|
const checkoutSuccess = searchParams.get("checkout") === "success";
|
|
1215
1810
|
const { data: me, isLoading } = useMySubscriptions();
|
|
1216
1811
|
const { data: catalog } = usePlans();
|
|
1812
|
+
const billingPortal = useBillingPortal();
|
|
1813
|
+
const { data: usage } = useUsage("mtd");
|
|
1217
1814
|
const planBySku = new Map((catalog?.plans ?? []).map((plan) => [plan.sku, plan]));
|
|
1218
1815
|
const subscriptions = me?.subscriptions ?? [];
|
|
1816
|
+
const activeSubscription = subscriptions.find((sub) => isActiveSubscription(sub.status));
|
|
1817
|
+
const activePlan = activeSubscription ? planBySku.get(activeSubscription.planSku) : void 0;
|
|
1818
|
+
const monthlyQuota = activePlan ? parseMonthlyQuotaCalls(activePlan.limitsJson) : null;
|
|
1819
|
+
const usedRequests = usage?.requestCount ?? 0;
|
|
1820
|
+
const quotaPct = monthlyQuota && monthlyQuota > 0 ? Math.min(100, Math.round(usedRequests / monthlyQuota * 100)) : null;
|
|
1219
1821
|
return /* @__PURE__ */ jsxs("div", {
|
|
1220
|
-
className: "
|
|
1822
|
+
className: "px-8 py-7",
|
|
1221
1823
|
children: [
|
|
1222
|
-
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "
|
|
1223
|
-
/* @__PURE__ */
|
|
1224
|
-
className: "mb-
|
|
1225
|
-
children:
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
children: "Review your active plans and manage billing."
|
|
1231
|
-
})]
|
|
1824
|
+
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Billing" }) }),
|
|
1825
|
+
/* @__PURE__ */ jsx("h1", {
|
|
1826
|
+
className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
|
|
1827
|
+
children: "Billing"
|
|
1828
|
+
}),
|
|
1829
|
+
/* @__PURE__ */ jsx("p", {
|
|
1830
|
+
className: "mb-8 text-muted-foreground",
|
|
1831
|
+
children: "Manage your plan, payment method, and invoices."
|
|
1232
1832
|
}),
|
|
1233
1833
|
checkoutSuccess ? /* @__PURE__ */ jsxs("div", {
|
|
1234
1834
|
className: "mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-sm",
|
|
1235
1835
|
children: [
|
|
1236
1836
|
"Your subscription is active.",
|
|
1237
1837
|
" ",
|
|
1238
|
-
/* @__PURE__ */ jsx(Link, {
|
|
1838
|
+
/* @__PURE__ */ jsx(Link$1, {
|
|
1239
1839
|
to: CREDENTIALS_PATH,
|
|
1240
1840
|
className: "font-medium underline",
|
|
1241
1841
|
children: "View your API credentials"
|
|
@@ -1245,32 +1845,80 @@ const SubscriptionsPage = () => {
|
|
|
1245
1845
|
}) : null,
|
|
1246
1846
|
isLoading ? /* @__PURE__ */ jsx("p", {
|
|
1247
1847
|
className: "text-muted-foreground",
|
|
1248
|
-
children: "Loading
|
|
1848
|
+
children: "Loading billing details…"
|
|
1849
|
+
}) : null,
|
|
1850
|
+
!isLoading && activePlan && activeSubscription ? /* @__PURE__ */ jsxs("div", {
|
|
1851
|
+
className: "mb-8 rounded-xl border border-border bg-card p-6",
|
|
1852
|
+
children: [
|
|
1853
|
+
/* @__PURE__ */ jsx("p", {
|
|
1854
|
+
className: "text-xs font-medium uppercase tracking-[0.05em] text-muted-foreground",
|
|
1855
|
+
children: "Current plan"
|
|
1856
|
+
}),
|
|
1857
|
+
/* @__PURE__ */ jsx("h2", {
|
|
1858
|
+
className: "mt-2 text-2xl font-bold",
|
|
1859
|
+
children: activePlan.displayName
|
|
1860
|
+
}),
|
|
1861
|
+
/* @__PURE__ */ jsxs("p", {
|
|
1862
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
1863
|
+
children: [
|
|
1864
|
+
formatPlanPrice(activePlan),
|
|
1865
|
+
" ·",
|
|
1866
|
+
" ",
|
|
1867
|
+
formatBillingPeriod(activePlan.billingPeriod)
|
|
1868
|
+
]
|
|
1869
|
+
}),
|
|
1870
|
+
activeSubscription.currentPeriodEnd ? /* @__PURE__ */ jsxs("p", {
|
|
1871
|
+
className: "mt-3 text-sm text-muted-foreground",
|
|
1872
|
+
children: [
|
|
1873
|
+
"Renews",
|
|
1874
|
+
" ",
|
|
1875
|
+
new Date(activeSubscription.currentPeriodEnd).toLocaleDateString()
|
|
1876
|
+
]
|
|
1877
|
+
}) : null,
|
|
1878
|
+
quotaPct != null && monthlyQuota != null ? /* @__PURE__ */ jsxs("div", {
|
|
1879
|
+
className: "mt-5",
|
|
1880
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1881
|
+
className: "mb-1.5 flex justify-between text-xs text-muted-foreground",
|
|
1882
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Monthly requests" }), /* @__PURE__ */ jsxs("span", { children: [
|
|
1883
|
+
usedRequests.toLocaleString(),
|
|
1884
|
+
" /",
|
|
1885
|
+
" ",
|
|
1886
|
+
monthlyQuota.toLocaleString()
|
|
1887
|
+
] })]
|
|
1888
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1889
|
+
className: "h-2 rounded-full bg-muted",
|
|
1890
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1891
|
+
className: "h-full rounded-full bg-primary transition-[width]",
|
|
1892
|
+
style: { width: `${quotaPct}%` }
|
|
1893
|
+
})
|
|
1894
|
+
})]
|
|
1895
|
+
}) : null
|
|
1896
|
+
]
|
|
1249
1897
|
}) : null,
|
|
1250
1898
|
!isLoading && subscriptions.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
1251
|
-
className: "rounded-xl border border-dashed p-8 text-center",
|
|
1899
|
+
className: "mb-8 rounded-xl border border-dashed p-8 text-center",
|
|
1252
1900
|
children: [/* @__PURE__ */ jsx("p", {
|
|
1253
1901
|
className: "text-muted-foreground",
|
|
1254
1902
|
children: "You do not have an active subscription yet."
|
|
1255
|
-
}), /* @__PURE__ */ jsx(Button$
|
|
1903
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
1256
1904
|
className: "mt-4",
|
|
1257
1905
|
asChild: true,
|
|
1258
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1906
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1259
1907
|
to: "/account/plans",
|
|
1260
1908
|
children: "View plans"
|
|
1261
1909
|
})
|
|
1262
1910
|
})]
|
|
1263
1911
|
}) : null,
|
|
1264
|
-
subscriptions.length >
|
|
1265
|
-
className: "space-y-4",
|
|
1912
|
+
!isLoading && subscriptions.length > 1 ? /* @__PURE__ */ jsx("div", {
|
|
1913
|
+
className: "mb-8 space-y-4",
|
|
1266
1914
|
children: subscriptions.map((subscription) => {
|
|
1267
1915
|
const plan = planBySku.get(subscription.planSku);
|
|
1268
|
-
return /* @__PURE__ */
|
|
1269
|
-
className: "rounded-xl border bg-card
|
|
1270
|
-
children:
|
|
1916
|
+
return /* @__PURE__ */ jsx("article", {
|
|
1917
|
+
className: "rounded-xl border bg-card p-5",
|
|
1918
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1271
1919
|
className: "flex flex-wrap items-start justify-between gap-4",
|
|
1272
|
-
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("
|
|
1273
|
-
className: "
|
|
1920
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
1921
|
+
className: "font-semibold",
|
|
1274
1922
|
children: plan?.displayName ?? subscription.planSku
|
|
1275
1923
|
}), plan ? /* @__PURE__ */ jsxs("p", {
|
|
1276
1924
|
className: "mt-1 text-sm text-muted-foreground",
|
|
@@ -1281,48 +1929,238 @@ const SubscriptionsPage = () => {
|
|
|
1281
1929
|
formatBillingPeriod(plan.billingPeriod)
|
|
1282
1930
|
]
|
|
1283
1931
|
}) : null] }), /* @__PURE__ */ jsx("span", {
|
|
1284
|
-
className:
|
|
1932
|
+
className: "rounded-full bg-muted px-3 py-1 text-xs font-medium text-muted-foreground",
|
|
1285
1933
|
children: subscription.status
|
|
1286
1934
|
})]
|
|
1287
|
-
})
|
|
1288
|
-
className: "mt-3 text-sm text-muted-foreground",
|
|
1289
|
-
children: [
|
|
1290
|
-
"Current period ends",
|
|
1291
|
-
" ",
|
|
1292
|
-
new Date(subscription.currentPeriodEnd).toLocaleDateString()
|
|
1293
|
-
]
|
|
1294
|
-
}) : null]
|
|
1935
|
+
})
|
|
1295
1936
|
}, `${subscription.planSku}-${subscription.status}`);
|
|
1296
1937
|
})
|
|
1297
1938
|
}) : null,
|
|
1298
1939
|
/* @__PURE__ */ jsxs("div", {
|
|
1299
|
-
className: "
|
|
1940
|
+
className: "flex flex-wrap gap-3",
|
|
1300
1941
|
children: [
|
|
1301
|
-
/* @__PURE__ */ jsx(Button$
|
|
1942
|
+
/* @__PURE__ */ jsx(Button$1, {
|
|
1302
1943
|
variant: "outline",
|
|
1303
1944
|
asChild: true,
|
|
1304
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1945
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1305
1946
|
to: "/account/plans",
|
|
1306
1947
|
children: "Change plan"
|
|
1307
1948
|
})
|
|
1308
1949
|
}),
|
|
1309
|
-
/* @__PURE__ */ jsx(Button$
|
|
1310
|
-
variant: "
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
})
|
|
1316
|
-
}),
|
|
1317
|
-
auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$
|
|
1950
|
+
auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
|
|
1951
|
+
variant: "default",
|
|
1952
|
+
disabled: billingPortal.isPending,
|
|
1953
|
+
onClick: () => {
|
|
1954
|
+
billingPortal.mutate();
|
|
1955
|
+
},
|
|
1956
|
+
children: billingPortal.isPending ? /* @__PURE__ */ jsx(Spinner, {}) : "Manage payment & invoices"
|
|
1957
|
+
}) : null,
|
|
1958
|
+
auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
|
|
1318
1959
|
variant: "secondary",
|
|
1319
1960
|
asChild: true,
|
|
1320
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1961
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1321
1962
|
to: CREDENTIALS_PATH,
|
|
1322
1963
|
children: "API credentials"
|
|
1323
1964
|
})
|
|
1324
1965
|
}) : null
|
|
1325
1966
|
]
|
|
1967
|
+
}),
|
|
1968
|
+
billingPortal.isError ? /* @__PURE__ */ jsx("p", {
|
|
1969
|
+
className: "mt-4 text-sm text-destructive",
|
|
1970
|
+
children: billingPortal.error instanceof Error ? billingPortal.error.message : "Unable to open billing portal."
|
|
1971
|
+
}) : null
|
|
1972
|
+
]
|
|
1973
|
+
});
|
|
1974
|
+
};
|
|
1975
|
+
|
|
1976
|
+
//#endregion
|
|
1977
|
+
//#region src/pages/UsagePage.tsx
|
|
1978
|
+
const RANGES = [
|
|
1979
|
+
{
|
|
1980
|
+
value: "24h",
|
|
1981
|
+
label: "24h"
|
|
1982
|
+
},
|
|
1983
|
+
{
|
|
1984
|
+
value: "7d",
|
|
1985
|
+
label: "7d"
|
|
1986
|
+
},
|
|
1987
|
+
{
|
|
1988
|
+
value: "30d",
|
|
1989
|
+
label: "30d"
|
|
1990
|
+
},
|
|
1991
|
+
{
|
|
1992
|
+
value: "90d",
|
|
1993
|
+
label: "90d"
|
|
1994
|
+
}
|
|
1995
|
+
];
|
|
1996
|
+
function formatNumber(value) {
|
|
1997
|
+
return new Intl.NumberFormat().format(value);
|
|
1998
|
+
}
|
|
1999
|
+
function formatPct(rate) {
|
|
2000
|
+
return `${(rate * 100).toFixed(rate >= .1 ? 1 : 2)}%`;
|
|
2001
|
+
}
|
|
2002
|
+
const UsagePage = () => {
|
|
2003
|
+
const auth = useAuth();
|
|
2004
|
+
const [range, setRange] = useState("30d");
|
|
2005
|
+
const { data, isLoading, isError } = useUsage(range);
|
|
2006
|
+
const maxBar = Math.max(1, ...data?.series.map((point) => point.requestCount) ?? [1]);
|
|
2007
|
+
const endpointTotal = data?.byEndpoint.reduce((sum, item) => sum + item.count, 0) ?? 0;
|
|
2008
|
+
if (!auth.isAuthenticated) return /* @__PURE__ */ jsxs("div", {
|
|
2009
|
+
className: "px-8 py-7",
|
|
2010
|
+
children: [
|
|
2011
|
+
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Usage" }) }),
|
|
2012
|
+
/* @__PURE__ */ jsx("h1", {
|
|
2013
|
+
className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
|
|
2014
|
+
children: "Usage"
|
|
2015
|
+
}),
|
|
2016
|
+
/* @__PURE__ */ jsx("p", {
|
|
2017
|
+
className: "text-muted-foreground",
|
|
2018
|
+
children: "Sign in to view your API usage."
|
|
2019
|
+
}),
|
|
2020
|
+
auth.isAuthEnabled ? /* @__PURE__ */ jsx(Button$1, {
|
|
2021
|
+
asChild: true,
|
|
2022
|
+
className: "mt-4",
|
|
2023
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
2024
|
+
to: "/signin",
|
|
2025
|
+
children: "Sign in"
|
|
2026
|
+
})
|
|
2027
|
+
}) : null
|
|
2028
|
+
]
|
|
2029
|
+
});
|
|
2030
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2031
|
+
className: "px-8 py-7",
|
|
2032
|
+
children: [
|
|
2033
|
+
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Usage" }) }),
|
|
2034
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2035
|
+
className: "mb-6 flex flex-wrap items-center justify-between gap-3",
|
|
2036
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
2037
|
+
className: "text-[1.6rem] font-bold tracking-[-0.01em]",
|
|
2038
|
+
children: "Usage"
|
|
2039
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2040
|
+
className: "flex gap-1 rounded-lg border border-border bg-card p-1",
|
|
2041
|
+
children: RANGES.map((item) => /* @__PURE__ */ jsx("button", {
|
|
2042
|
+
type: "button",
|
|
2043
|
+
onClick: () => setRange(item.value),
|
|
2044
|
+
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"}`,
|
|
2045
|
+
children: item.label
|
|
2046
|
+
}, item.value))
|
|
2047
|
+
})]
|
|
2048
|
+
}),
|
|
2049
|
+
isLoading ? /* @__PURE__ */ jsx("p", {
|
|
2050
|
+
className: "text-sm text-muted-foreground",
|
|
2051
|
+
children: "Loading usage…"
|
|
2052
|
+
}) : null,
|
|
2053
|
+
isError ? /* @__PURE__ */ jsx("p", {
|
|
2054
|
+
className: "text-sm text-destructive",
|
|
2055
|
+
children: "Unable to load usage data."
|
|
2056
|
+
}) : null,
|
|
2057
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2058
|
+
className: "mb-5 grid grid-cols-1 gap-4 sm:grid-cols-3",
|
|
2059
|
+
children: [
|
|
2060
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2061
|
+
className: "rounded-xl border border-border bg-card p-4",
|
|
2062
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2063
|
+
className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
|
|
2064
|
+
children: "Requests"
|
|
2065
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2066
|
+
className: "mt-1.5 text-2xl font-bold",
|
|
2067
|
+
children: data ? formatNumber(data.requestCount) : "—"
|
|
2068
|
+
})]
|
|
2069
|
+
}),
|
|
2070
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2071
|
+
className: "rounded-xl border border-border bg-card p-4",
|
|
2072
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2073
|
+
className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
|
|
2074
|
+
children: "Avg latency"
|
|
2075
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2076
|
+
className: "mt-1.5 text-2xl font-bold",
|
|
2077
|
+
children: data?.avgLatencyMs != null ? `${data.avgLatencyMs} ms` : "—"
|
|
2078
|
+
})]
|
|
2079
|
+
}),
|
|
2080
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2081
|
+
className: "rounded-xl border border-border bg-card p-4",
|
|
2082
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2083
|
+
className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
|
|
2084
|
+
children: "Error rate"
|
|
2085
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2086
|
+
className: "mt-1.5 text-2xl font-bold",
|
|
2087
|
+
children: data ? formatPct(data.errorRate) : "—"
|
|
2088
|
+
})]
|
|
2089
|
+
})
|
|
2090
|
+
]
|
|
2091
|
+
}),
|
|
2092
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2093
|
+
className: "mb-5 rounded-xl border border-border bg-card p-6",
|
|
2094
|
+
children: [/* @__PURE__ */ jsxs("h2", {
|
|
2095
|
+
className: "mb-4 text-lg font-semibold",
|
|
2096
|
+
children: [
|
|
2097
|
+
"Requests over time (",
|
|
2098
|
+
range,
|
|
2099
|
+
")"
|
|
2100
|
+
]
|
|
2101
|
+
}), data && data.series.some((p) => p.requestCount > 0) ? /* @__PURE__ */ jsx("div", {
|
|
2102
|
+
className: "flex h-40 items-end gap-1.5",
|
|
2103
|
+
children: data.series.map((point) => /* @__PURE__ */ jsx("div", {
|
|
2104
|
+
title: `${point.dayUtc}: ${point.requestCount}`,
|
|
2105
|
+
className: "min-w-0 flex-1 rounded-t-sm bg-primary",
|
|
2106
|
+
style: { height: `${Math.max(4, point.requestCount / maxBar * 100)}%` }
|
|
2107
|
+
}, point.dayUtc))
|
|
2108
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
2109
|
+
className: "text-sm text-muted-foreground",
|
|
2110
|
+
children: "No request data for this range yet."
|
|
2111
|
+
})]
|
|
2112
|
+
}),
|
|
2113
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2114
|
+
className: "grid grid-cols-1 gap-5 lg:grid-cols-[1.2fr_1fr]",
|
|
2115
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2116
|
+
className: "rounded-xl border border-border bg-card p-6",
|
|
2117
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
2118
|
+
className: "mb-4 text-lg font-semibold",
|
|
2119
|
+
children: "By endpoint"
|
|
2120
|
+
}), data && data.byEndpoint.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
2121
|
+
className: "space-y-3.5",
|
|
2122
|
+
children: data.byEndpoint.map((item) => {
|
|
2123
|
+
const pct = endpointTotal > 0 ? Math.round(item.count / endpointTotal * 100) : 0;
|
|
2124
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
2125
|
+
className: "mb-1 flex justify-between text-[13px]",
|
|
2126
|
+
children: [/* @__PURE__ */ jsx("code", {
|
|
2127
|
+
className: "font-mono text-foreground",
|
|
2128
|
+
children: item.path
|
|
2129
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
2130
|
+
className: "text-muted-foreground",
|
|
2131
|
+
children: [pct, "%"]
|
|
2132
|
+
})]
|
|
2133
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2134
|
+
className: "h-1.5 rounded-full bg-muted",
|
|
2135
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2136
|
+
className: "h-full rounded-full bg-primary",
|
|
2137
|
+
style: { width: `${pct}%` }
|
|
2138
|
+
})
|
|
2139
|
+
})] }, item.path);
|
|
2140
|
+
})
|
|
2141
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
2142
|
+
className: "text-sm text-muted-foreground",
|
|
2143
|
+
children: "No endpoint data."
|
|
2144
|
+
})]
|
|
2145
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2146
|
+
className: "rounded-xl border border-border bg-card p-6",
|
|
2147
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
2148
|
+
className: "mb-4 text-lg font-semibold",
|
|
2149
|
+
children: "Status codes"
|
|
2150
|
+
}), data && data.byStatus.length > 0 ? /* @__PURE__ */ jsx("div", { children: data.byStatus.map((item) => /* @__PURE__ */ jsxs("div", {
|
|
2151
|
+
className: "flex items-center justify-between border-b border-border py-2.5 last:border-0",
|
|
2152
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2153
|
+
className: "text-sm font-medium",
|
|
2154
|
+
children: item.label
|
|
2155
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2156
|
+
className: "text-sm font-bold",
|
|
2157
|
+
children: formatNumber(item.count)
|
|
2158
|
+
})]
|
|
2159
|
+
}, item.label)) }) : /* @__PURE__ */ jsx("p", {
|
|
2160
|
+
className: "text-sm text-muted-foreground",
|
|
2161
|
+
children: "No status data."
|
|
2162
|
+
})]
|
|
2163
|
+
})]
|
|
1326
2164
|
})
|
|
1327
2165
|
]
|
|
1328
2166
|
});
|
|
@@ -1445,21 +2283,36 @@ const devPortalBillingPlugin = createPlugin((options = {}) => ({
|
|
|
1445
2283
|
{
|
|
1446
2284
|
path: SUBSCRIPTIONS_PATH,
|
|
1447
2285
|
handle: { layout: "default" },
|
|
1448
|
-
element: /* @__PURE__ */ jsx(
|
|
2286
|
+
element: /* @__PURE__ */ jsx(AccountLayout, {
|
|
2287
|
+
includeBillingNav: true,
|
|
2288
|
+
children: /* @__PURE__ */ jsx(SubscriptionsPage, {})
|
|
2289
|
+
})
|
|
1449
2290
|
},
|
|
1450
2291
|
{
|
|
1451
2292
|
path: CREDENTIALS_PATH,
|
|
1452
2293
|
handle: { layout: "default" },
|
|
1453
|
-
element: /* @__PURE__ */ jsx(
|
|
2294
|
+
element: /* @__PURE__ */ jsx(AccountLayout, {
|
|
2295
|
+
includeBillingNav: true,
|
|
2296
|
+
children: /* @__PURE__ */ jsx(CredentialsPage, {})
|
|
2297
|
+
})
|
|
2298
|
+
},
|
|
2299
|
+
{
|
|
2300
|
+
path: USAGE_PATH,
|
|
2301
|
+
handle: { layout: "default" },
|
|
2302
|
+
element: /* @__PURE__ */ jsx(AccountLayout, {
|
|
2303
|
+
includeBillingNav: true,
|
|
2304
|
+
children: /* @__PURE__ */ jsx(UsagePage, {})
|
|
2305
|
+
})
|
|
1454
2306
|
}
|
|
1455
2307
|
]
|
|
1456
2308
|
}],
|
|
1457
2309
|
getProtectedRoutes: () => [
|
|
1458
2310
|
CHECKOUT_PATH,
|
|
1459
2311
|
SUBSCRIPTIONS_PATH,
|
|
1460
|
-
CREDENTIALS_PATH
|
|
2312
|
+
CREDENTIALS_PATH,
|
|
2313
|
+
USAGE_PATH
|
|
1461
2314
|
]
|
|
1462
2315
|
}));
|
|
1463
2316
|
|
|
1464
2317
|
//#endregion
|
|
1465
|
-
export { API_KEY_CAP_REACHED, AccountPlansPage, ApiKeysPanel, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin, useApiKeys, useCreateApiKey, useRevokeApiKey };
|
|
2318
|
+
export { API_KEY_CAP_REACHED, AccountPlansPage, ApiKeysPanel, CHECKOUT_PATH, CREDENTIALS_PATH, OverviewDashboardPage, PRICING_PATH, SUBSCRIPTIONS_PATH, USAGE_PATH, devPortalBillingPlugin, useApiKeys, useCreateApiKey, useRevokeApiKey };
|