@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.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 <= 0;
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 "Monthly";
97
- if (normalized === "year" || normalized === "annual") return "Annual";
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 summarizeLimits(limitsJson) {
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 limits = JSON.parse(limitsJson);
111
- const rateSummary = formatRateLimit(limits.rateLimit);
112
- if (rateSummary) return rateSummary;
113
- const entries = Object.entries(limits).filter(([, value]) => value != null && typeof value !== "object");
114
- if (entries.length === 0) return null;
115
- return entries.slice(0, 3).map(([key, value]) => `${key}: ${String(value)}`).join(" · ");
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 PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, isCanceling = false, cancelDisabled = false }) => {
127
- const limitsSummary = summarizeLimits(plan.limitsJson);
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 w-full max-w-sm flex-col rounded-xl border bg-card/60 p-6 shadow-xs", isActive && mode === "account" && "border-primary/40 ring-1 ring-primary/20"),
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-start justify-between gap-2",
135
- children: [/* @__PURE__ */ jsx("h2", {
136
- className: "text-xl font-semibold",
137
- children: plan.displayName
138
- }), isActive && mode === "account" ? /* @__PURE__ */ jsx("span", {
139
- className: "shrink-0 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary",
140
- "data-testid": `subscribed-badge-${plan.sku}`,
141
- children: "Subscribed"
142
- }) : null]
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-2 text-3xl font-bold",
146
- children: formatPlanPrice(plan)
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-1 text-sm text-muted-foreground",
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: "mt-6 space-y-2",
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: isPlanFree(plan) ? "Get started" : "Subscribe"
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 ?? "Pricing";
1150
- const subtitle = pricing?.subtitle ?? "Compare plans and subscribe when you are ready to integrate.";
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 px-4 pt-(--padding-content-top) pb-(--padding-content-bottom)",
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("div", {
1159
- className: "mb-12 space-y-4 text-center",
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
- "No plans are configured yet. Add plans in",
1189
- " ",
1190
- /* @__PURE__ */ jsx("code", {
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
- "and set",
1196
- " ",
1197
- /* @__PURE__ */ jsx("code", {
1198
- className: "rounded bg-muted px-1.5 py-0.5",
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
- "to true."
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
- }) : null
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: "mx-auto max-w-screen-lg px-4 pb-16 pt-(--padding-content-top) lg:px-8",
1822
+ className: "px-8 py-7",
1221
1823
  children: [
1222
- /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "My subscriptions" }) }),
1223
- /* @__PURE__ */ jsxs("div", {
1224
- className: "mb-8 space-y-2",
1225
- children: [/* @__PURE__ */ jsx("h1", {
1226
- className: "text-3xl font-semibold tracking-tight",
1227
- children: "My subscriptions"
1228
- }), /* @__PURE__ */ jsx("p", {
1229
- className: "text-muted-foreground",
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 subscriptions…"
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$2, {
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 > 0 ? /* @__PURE__ */ jsx("div", {
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__ */ jsxs("article", {
1269
- className: "rounded-xl border bg-card/60 p-6",
1270
- children: [/* @__PURE__ */ jsxs("div", {
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("h2", {
1273
- className: "text-lg font-medium",
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: isActiveSubscription(subscription.status) ? "rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary" : "rounded-full bg-muted px-3 py-1 text-xs font-medium text-muted-foreground",
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
- }), subscription.currentPeriodEnd ? /* @__PURE__ */ jsxs("p", {
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: "mt-8 flex flex-wrap gap-3",
1940
+ className: "flex flex-wrap gap-3",
1300
1941
  children: [
1301
- /* @__PURE__ */ jsx(Button$2, {
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$2, {
1310
- variant: "outline",
1311
- asChild: true,
1312
- children: /* @__PURE__ */ jsx(Link, {
1313
- to: "/account/plans",
1314
- children: "Billing settings"
1315
- })
1316
- }),
1317
- auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$2, {
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(SubscriptionsPage, {})
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(CredentialsPage, {})
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 };