@lukoweb/apitogo-plugin-dev-portal-billing 0.1.88 → 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 +7 -3
- package/dist/index.mjs +751 -73
- package/package.json +5 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
-
import * as
|
|
2
|
+
import * as _lukoweb_apitogo0 from "@lukoweb/apitogo";
|
|
3
3
|
import * as _tanstack_react_query0 from "@tanstack/react-query";
|
|
4
4
|
|
|
5
5
|
//#region src/pages/AccountPlansPage.d.ts
|
|
6
6
|
declare const AccountPlansPage: () => react_jsx_runtime0.JSX.Element;
|
|
7
7
|
//#endregion
|
|
8
|
+
//#region src/pages/OverviewDashboardPage.d.ts
|
|
9
|
+
declare const OverviewDashboardPage: () => react_jsx_runtime0.JSX.Element;
|
|
10
|
+
//#endregion
|
|
8
11
|
//#region src/types.d.ts
|
|
9
12
|
type DevPortalPricingCompareRow = {
|
|
10
13
|
label: string;
|
|
@@ -75,9 +78,10 @@ declare const PRICING_PATH = "/pricing";
|
|
|
75
78
|
declare const CHECKOUT_PATH = "/checkout";
|
|
76
79
|
declare const SUBSCRIPTIONS_PATH = "/account/subscriptions";
|
|
77
80
|
declare const CREDENTIALS_PATH = "/account/credentials";
|
|
81
|
+
declare const USAGE_PATH = "/account/usage";
|
|
78
82
|
//#endregion
|
|
79
83
|
//#region src/DevPortalBillingPlugin.d.ts
|
|
80
|
-
declare const devPortalBillingPlugin: (options?: DevPortalBillingConfig | undefined) =>
|
|
84
|
+
declare const devPortalBillingPlugin: (options?: DevPortalBillingConfig | undefined) => _lukoweb_apitogo0.ApitogoPlugin;
|
|
81
85
|
//#endregion
|
|
82
86
|
//#region src/hooks/useApiKeys.d.ts
|
|
83
87
|
/** Thrown when the developer has hit their plan's per-developer key cap. */
|
|
@@ -197,4 +201,4 @@ declare function useCreateApiKey(): _tanstack_react_query0.UseMutationResult<Dev
|
|
|
197
201
|
}, unknown>;
|
|
198
202
|
declare function useRevokeApiKey(): _tanstack_react_query0.UseMutationResult<void, Error, string, unknown>;
|
|
199
203
|
//#endregion
|
|
200
|
-
export { API_KEY_CAP_REACHED, AccountPlansPage, ApiKeysPanel, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin, useApiKeys, useCreateApiKey, useRevokeApiKey };
|
|
204
|
+
export { API_KEY_CAP_REACHED, AccountPlansPage, ApiKeysPanel, CHECKOUT_PATH, CREDENTIALS_PATH, OverviewDashboardPage, PRICING_PATH, SUBSCRIPTIONS_PATH, USAGE_PATH, devPortalBillingPlugin, useApiKeys, useCreateApiKey, useRevokeApiKey };
|
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";
|
|
@@ -154,6 +155,20 @@ function listPlanFeatureLines(plan) {
|
|
|
154
155
|
function isActiveSubscription(status) {
|
|
155
156
|
return status.toLowerCase() === "active";
|
|
156
157
|
}
|
|
158
|
+
/** Returns monthly call quota from plan limitsJson, or null when unknown/unlimited. */
|
|
159
|
+
function parseMonthlyQuotaCalls(limitsJson) {
|
|
160
|
+
if (!limitsJson?.trim()) return null;
|
|
161
|
+
try {
|
|
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;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
157
172
|
|
|
158
173
|
//#endregion
|
|
159
174
|
//#region src/components/PricingCard.tsx
|
|
@@ -233,7 +248,7 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
|
|
|
233
248
|
variant: featured ? "default" : "outline",
|
|
234
249
|
className: "w-full",
|
|
235
250
|
asChild: true,
|
|
236
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
251
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
237
252
|
to: `/checkout?planSku=${encodeURIComponent(plan.sku)}`,
|
|
238
253
|
children: isPlanFree(plan) ? "Get started" : "Subscribe"
|
|
239
254
|
})
|
|
@@ -241,7 +256,7 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
|
|
|
241
256
|
variant: "secondary",
|
|
242
257
|
className: "w-full",
|
|
243
258
|
asChild: true,
|
|
244
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
259
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
245
260
|
to: "/account/subscriptions",
|
|
246
261
|
children: "Manage subscription"
|
|
247
262
|
})
|
|
@@ -249,7 +264,7 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
|
|
|
249
264
|
variant: featured ? "default" : "outline",
|
|
250
265
|
className: "w-full",
|
|
251
266
|
asChild: true,
|
|
252
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
267
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
253
268
|
to: `/checkout?planSku=${encodeURIComponent(plan.sku)}`,
|
|
254
269
|
children: publicCtaLabel(plan)
|
|
255
270
|
})
|
|
@@ -348,6 +363,9 @@ function createDemoMeResponse(plans) {
|
|
|
348
363
|
return {
|
|
349
364
|
userId: "00000000-0000-0000-0000-000000000001",
|
|
350
365
|
displayName: "Demo User",
|
|
366
|
+
email: "demo@example.com",
|
|
367
|
+
emailVerified: true,
|
|
368
|
+
hasNativePassword: true,
|
|
351
369
|
subscriptions: resolveDemoPlans(plans).map((plan) => ({
|
|
352
370
|
planSku: plan.sku,
|
|
353
371
|
status: "Active",
|
|
@@ -634,6 +652,394 @@ async function readProblemDetail(response) {
|
|
|
634
652
|
}
|
|
635
653
|
}
|
|
636
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
|
+
|
|
637
1043
|
//#endregion
|
|
638
1044
|
//#region src/components/ApiKeysPanel.tsx
|
|
639
1045
|
/**
|
|
@@ -858,14 +1264,6 @@ const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
|
|
|
858
1264
|
});
|
|
859
1265
|
};
|
|
860
1266
|
|
|
861
|
-
//#endregion
|
|
862
|
-
//#region src/paths.ts
|
|
863
|
-
const PRICING_PATH = "/pricing";
|
|
864
|
-
const CHECKOUT_PATH = "/checkout";
|
|
865
|
-
const SUBSCRIPTIONS_PATH = "/account/subscriptions";
|
|
866
|
-
const CREDENTIALS_PATH = "/account/credentials";
|
|
867
|
-
const LOGIN_NAV_PATH = "/signin";
|
|
868
|
-
|
|
869
1267
|
//#endregion
|
|
870
1268
|
//#region src/pages/CheckoutPage.tsx
|
|
871
1269
|
const ACCOUNT_PLANS_PATH = "/account/plans";
|
|
@@ -913,7 +1311,7 @@ const CheckoutPage = () => {
|
|
|
913
1311
|
children: [
|
|
914
1312
|
"Missing plan SKU.",
|
|
915
1313
|
" ",
|
|
916
|
-
/* @__PURE__ */ jsx(Link, {
|
|
1314
|
+
/* @__PURE__ */ jsx(Link$1, {
|
|
917
1315
|
to: plansPath,
|
|
918
1316
|
className: "text-primary underline",
|
|
919
1317
|
children: "Back to plans"
|
|
@@ -934,7 +1332,7 @@ const CheckoutPage = () => {
|
|
|
934
1332
|
/* @__PURE__ */ jsx(Button$2, {
|
|
935
1333
|
asChild: true,
|
|
936
1334
|
variant: "outline",
|
|
937
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1335
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
938
1336
|
to: "/account/subscriptions",
|
|
939
1337
|
children: "View demo subscriptions"
|
|
940
1338
|
})
|
|
@@ -955,7 +1353,7 @@ const CheckoutPage = () => {
|
|
|
955
1353
|
/* @__PURE__ */ jsx(Button$2, {
|
|
956
1354
|
asChild: true,
|
|
957
1355
|
variant: "outline",
|
|
958
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1356
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
959
1357
|
to: plansPath,
|
|
960
1358
|
children: "Back to plans"
|
|
961
1359
|
})
|
|
@@ -977,7 +1375,7 @@ const CheckoutPage = () => {
|
|
|
977
1375
|
variant: "outline",
|
|
978
1376
|
size: "xs",
|
|
979
1377
|
asChild: true,
|
|
980
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1378
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
981
1379
|
to: plansPath,
|
|
982
1380
|
children: "Back to plans"
|
|
983
1381
|
})
|
|
@@ -1073,7 +1471,7 @@ const CredentialsPage = () => {
|
|
|
1073
1471
|
}), /* @__PURE__ */ jsx(Button$2, {
|
|
1074
1472
|
className: "mt-4",
|
|
1075
1473
|
asChild: true,
|
|
1076
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1474
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1077
1475
|
to: "/account/plans",
|
|
1078
1476
|
children: "View plans"
|
|
1079
1477
|
})
|
|
@@ -1091,7 +1489,7 @@ const CredentialsPage = () => {
|
|
|
1091
1489
|
}), /* @__PURE__ */ jsx(Button$2, {
|
|
1092
1490
|
className: "mt-4",
|
|
1093
1491
|
asChild: true,
|
|
1094
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1492
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1095
1493
|
to: "/account/plans",
|
|
1096
1494
|
children: "View plans"
|
|
1097
1495
|
})
|
|
@@ -1107,7 +1505,7 @@ const CredentialsPage = () => {
|
|
|
1107
1505
|
}), /* @__PURE__ */ jsx(Button$2, {
|
|
1108
1506
|
className: "mt-4",
|
|
1109
1507
|
asChild: true,
|
|
1110
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1508
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1111
1509
|
to: "/account/plans",
|
|
1112
1510
|
children: "View plans"
|
|
1113
1511
|
})
|
|
@@ -1141,14 +1539,14 @@ const CredentialsPage = () => {
|
|
|
1141
1539
|
children: [/* @__PURE__ */ jsx(Button$2, {
|
|
1142
1540
|
variant: "outline",
|
|
1143
1541
|
asChild: true,
|
|
1144
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1542
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1145
1543
|
to: "/account/subscriptions",
|
|
1146
1544
|
children: "My subscriptions"
|
|
1147
1545
|
})
|
|
1148
1546
|
}), /* @__PURE__ */ jsx(Button$2, {
|
|
1149
1547
|
variant: "outline",
|
|
1150
1548
|
asChild: true,
|
|
1151
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1549
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1152
1550
|
to: "/api",
|
|
1153
1551
|
children: "API reference"
|
|
1154
1552
|
})
|
|
@@ -1381,6 +1779,28 @@ const PricingPage = () => {
|
|
|
1381
1779
|
});
|
|
1382
1780
|
};
|
|
1383
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
|
+
|
|
1384
1804
|
//#endregion
|
|
1385
1805
|
//#region src/pages/SubscriptionsPage.tsx
|
|
1386
1806
|
const SubscriptionsPage = () => {
|
|
@@ -1389,28 +1809,33 @@ const SubscriptionsPage = () => {
|
|
|
1389
1809
|
const checkoutSuccess = searchParams.get("checkout") === "success";
|
|
1390
1810
|
const { data: me, isLoading } = useMySubscriptions();
|
|
1391
1811
|
const { data: catalog } = usePlans();
|
|
1812
|
+
const billingPortal = useBillingPortal();
|
|
1813
|
+
const { data: usage } = useUsage("mtd");
|
|
1392
1814
|
const planBySku = new Map((catalog?.plans ?? []).map((plan) => [plan.sku, plan]));
|
|
1393
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;
|
|
1394
1821
|
return /* @__PURE__ */ jsxs("div", {
|
|
1395
|
-
className: "
|
|
1822
|
+
className: "px-8 py-7",
|
|
1396
1823
|
children: [
|
|
1397
|
-
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "
|
|
1398
|
-
/* @__PURE__ */
|
|
1399
|
-
className: "mb-
|
|
1400
|
-
children:
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
children: "Review your active plans and manage billing."
|
|
1406
|
-
})]
|
|
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."
|
|
1407
1832
|
}),
|
|
1408
1833
|
checkoutSuccess ? /* @__PURE__ */ jsxs("div", {
|
|
1409
1834
|
className: "mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-sm",
|
|
1410
1835
|
children: [
|
|
1411
1836
|
"Your subscription is active.",
|
|
1412
1837
|
" ",
|
|
1413
|
-
/* @__PURE__ */ jsx(Link, {
|
|
1838
|
+
/* @__PURE__ */ jsx(Link$1, {
|
|
1414
1839
|
to: CREDENTIALS_PATH,
|
|
1415
1840
|
className: "font-medium underline",
|
|
1416
1841
|
children: "View your API credentials"
|
|
@@ -1420,32 +1845,80 @@ const SubscriptionsPage = () => {
|
|
|
1420
1845
|
}) : null,
|
|
1421
1846
|
isLoading ? /* @__PURE__ */ jsx("p", {
|
|
1422
1847
|
className: "text-muted-foreground",
|
|
1423
|
-
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
|
+
]
|
|
1424
1897
|
}) : null,
|
|
1425
1898
|
!isLoading && subscriptions.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
1426
|
-
className: "rounded-xl border border-dashed p-8 text-center",
|
|
1899
|
+
className: "mb-8 rounded-xl border border-dashed p-8 text-center",
|
|
1427
1900
|
children: [/* @__PURE__ */ jsx("p", {
|
|
1428
1901
|
className: "text-muted-foreground",
|
|
1429
1902
|
children: "You do not have an active subscription yet."
|
|
1430
|
-
}), /* @__PURE__ */ jsx(Button$
|
|
1903
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
1431
1904
|
className: "mt-4",
|
|
1432
1905
|
asChild: true,
|
|
1433
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1906
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1434
1907
|
to: "/account/plans",
|
|
1435
1908
|
children: "View plans"
|
|
1436
1909
|
})
|
|
1437
1910
|
})]
|
|
1438
1911
|
}) : null,
|
|
1439
|
-
subscriptions.length >
|
|
1440
|
-
className: "space-y-4",
|
|
1912
|
+
!isLoading && subscriptions.length > 1 ? /* @__PURE__ */ jsx("div", {
|
|
1913
|
+
className: "mb-8 space-y-4",
|
|
1441
1914
|
children: subscriptions.map((subscription) => {
|
|
1442
1915
|
const plan = planBySku.get(subscription.planSku);
|
|
1443
|
-
return /* @__PURE__ */
|
|
1444
|
-
className: "rounded-xl border bg-card
|
|
1445
|
-
children:
|
|
1916
|
+
return /* @__PURE__ */ jsx("article", {
|
|
1917
|
+
className: "rounded-xl border bg-card p-5",
|
|
1918
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1446
1919
|
className: "flex flex-wrap items-start justify-between gap-4",
|
|
1447
|
-
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("
|
|
1448
|
-
className: "
|
|
1920
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
1921
|
+
className: "font-semibold",
|
|
1449
1922
|
children: plan?.displayName ?? subscription.planSku
|
|
1450
1923
|
}), plan ? /* @__PURE__ */ jsxs("p", {
|
|
1451
1924
|
className: "mt-1 text-sm text-muted-foreground",
|
|
@@ -1456,48 +1929,238 @@ const SubscriptionsPage = () => {
|
|
|
1456
1929
|
formatBillingPeriod(plan.billingPeriod)
|
|
1457
1930
|
]
|
|
1458
1931
|
}) : null] }), /* @__PURE__ */ jsx("span", {
|
|
1459
|
-
className:
|
|
1932
|
+
className: "rounded-full bg-muted px-3 py-1 text-xs font-medium text-muted-foreground",
|
|
1460
1933
|
children: subscription.status
|
|
1461
1934
|
})]
|
|
1462
|
-
})
|
|
1463
|
-
className: "mt-3 text-sm text-muted-foreground",
|
|
1464
|
-
children: [
|
|
1465
|
-
"Current period ends",
|
|
1466
|
-
" ",
|
|
1467
|
-
new Date(subscription.currentPeriodEnd).toLocaleDateString()
|
|
1468
|
-
]
|
|
1469
|
-
}) : null]
|
|
1935
|
+
})
|
|
1470
1936
|
}, `${subscription.planSku}-${subscription.status}`);
|
|
1471
1937
|
})
|
|
1472
1938
|
}) : null,
|
|
1473
1939
|
/* @__PURE__ */ jsxs("div", {
|
|
1474
|
-
className: "
|
|
1940
|
+
className: "flex flex-wrap gap-3",
|
|
1475
1941
|
children: [
|
|
1476
|
-
/* @__PURE__ */ jsx(Button$
|
|
1942
|
+
/* @__PURE__ */ jsx(Button$1, {
|
|
1477
1943
|
variant: "outline",
|
|
1478
1944
|
asChild: true,
|
|
1479
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1945
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1480
1946
|
to: "/account/plans",
|
|
1481
1947
|
children: "Change plan"
|
|
1482
1948
|
})
|
|
1483
1949
|
}),
|
|
1484
|
-
/* @__PURE__ */ jsx(Button$
|
|
1485
|
-
variant: "
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
})
|
|
1491
|
-
}),
|
|
1492
|
-
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, {
|
|
1493
1959
|
variant: "secondary",
|
|
1494
1960
|
asChild: true,
|
|
1495
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1961
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1496
1962
|
to: CREDENTIALS_PATH,
|
|
1497
1963
|
children: "API credentials"
|
|
1498
1964
|
})
|
|
1499
1965
|
}) : null
|
|
1500
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
|
+
})]
|
|
1501
2164
|
})
|
|
1502
2165
|
]
|
|
1503
2166
|
});
|
|
@@ -1620,21 +2283,36 @@ const devPortalBillingPlugin = createPlugin((options = {}) => ({
|
|
|
1620
2283
|
{
|
|
1621
2284
|
path: SUBSCRIPTIONS_PATH,
|
|
1622
2285
|
handle: { layout: "default" },
|
|
1623
|
-
element: /* @__PURE__ */ jsx(
|
|
2286
|
+
element: /* @__PURE__ */ jsx(AccountLayout, {
|
|
2287
|
+
includeBillingNav: true,
|
|
2288
|
+
children: /* @__PURE__ */ jsx(SubscriptionsPage, {})
|
|
2289
|
+
})
|
|
1624
2290
|
},
|
|
1625
2291
|
{
|
|
1626
2292
|
path: CREDENTIALS_PATH,
|
|
1627
2293
|
handle: { layout: "default" },
|
|
1628
|
-
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
|
+
})
|
|
1629
2306
|
}
|
|
1630
2307
|
]
|
|
1631
2308
|
}],
|
|
1632
2309
|
getProtectedRoutes: () => [
|
|
1633
2310
|
CHECKOUT_PATH,
|
|
1634
2311
|
SUBSCRIPTIONS_PATH,
|
|
1635
|
-
CREDENTIALS_PATH
|
|
2312
|
+
CREDENTIALS_PATH,
|
|
2313
|
+
USAGE_PATH
|
|
1636
2314
|
]
|
|
1637
2315
|
}));
|
|
1638
2316
|
|
|
1639
2317
|
//#endregion
|
|
1640
|
-
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lukoweb/apitogo-plugin-dev-portal-billing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.89",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/zuplo/zudoku",
|
|
@@ -46,11 +46,13 @@
|
|
|
46
46
|
"react": "19.2.4",
|
|
47
47
|
"react-dom": "19.2.4",
|
|
48
48
|
"tsdown": "0.20.3",
|
|
49
|
-
"@lukoweb/apitogo": "0.1.
|
|
49
|
+
"@lukoweb/apitogo": "0.1.89",
|
|
50
|
+
"@lukoweb/apitogo-module-user-panel": "0.1.89"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|
|
52
53
|
"react": ">=19.2.0",
|
|
53
54
|
"react-dom": ">=19.2.0",
|
|
54
|
-
"@lukoweb/apitogo": "*"
|
|
55
|
+
"@lukoweb/apitogo": "*",
|
|
56
|
+
"@lukoweb/apitogo-module-user-panel": "*"
|
|
55
57
|
}
|
|
56
58
|
}
|