@lukoweb/apitogo-plugin-dev-portal-billing 0.2.30 → 0.2.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AccountPlansPage-CXOfTwlX.mjs +208 -0
- package/dist/ApiKeysPanel-DrArkHXf.mjs +285 -0
- package/dist/CheckoutPage-CW_EBGog.mjs +174 -0
- package/dist/ContactSalesPage-Vjzm-RxO.mjs +256 -0
- package/dist/CredentialsPage-CtIEboEy.mjs +189 -0
- package/dist/DevPortalBillingContext-CR83s4LO.mjs +13 -0
- package/dist/PricingCard-Wra2FUX9.mjs +145 -0
- package/dist/PricingPage-MTslSnCh.mjs +233 -0
- package/dist/SubscriptionsPage-9lXnVks7.mjs +226 -0
- package/dist/UsagePage--mg_NzrA.mjs +203 -0
- package/dist/billing-state-pnT0H1Lm.mjs +37 -0
- package/dist/demo-session-Cez740bi.mjs +65 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +121 -2378
- package/dist/paths-ujy9wjVK.mjs +12 -0
- package/dist/pricing-config-CfhRX9tk.mjs +11 -0
- package/dist/useDevPortalApiBaseUrl-CgygF2z5.mjs +241 -0
- package/dist/useMySubscriptions-CLh3eFY4.mjs +42 -0
- package/dist/usePlans-DUex2tzT.mjs +69 -0
- package/dist/useUsage-Cx2GB3wg.mjs +164 -0
- package/package.json +3 -3
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { h as pickFeaturedPlanSku, n as useDevPortalPreviewMode, p as parseManifestPlans } from "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
|
|
2
|
+
import { t as PricingCard } from "./PricingCard-Wra2FUX9.mjs";
|
|
3
|
+
import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
|
|
4
|
+
import { t as usePlans } from "./usePlans-DUex2tzT.mjs";
|
|
5
|
+
import { t as isPreviewPricingEnabled } from "./pricing-config-CfhRX9tk.mjs";
|
|
6
|
+
import { i as isPricingLoading, n as buildCompareRows } from "./billing-state-pnT0H1Lm.mjs";
|
|
7
|
+
import { Head, Heading } from "@lukoweb/apitogo/components";
|
|
8
|
+
import { Navigate } from "@lukoweb/apitogo/router";
|
|
9
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
import { useAuth } from "@lukoweb/apitogo/hooks";
|
|
11
|
+
|
|
12
|
+
//#region src/pages/PricingPage.tsx
|
|
13
|
+
const DEFAULT_COMPARE_ROWS = [
|
|
14
|
+
{
|
|
15
|
+
label: "Monthly requests",
|
|
16
|
+
values: [
|
|
17
|
+
"1,000",
|
|
18
|
+
"50,000",
|
|
19
|
+
"500,000",
|
|
20
|
+
"Unlimited"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: "Rate limit",
|
|
25
|
+
values: [
|
|
26
|
+
"10/sec",
|
|
27
|
+
"50/sec",
|
|
28
|
+
"200/sec",
|
|
29
|
+
"Custom"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
label: "Team seats",
|
|
34
|
+
values: [
|
|
35
|
+
"1",
|
|
36
|
+
"1",
|
|
37
|
+
"5",
|
|
38
|
+
"Unlimited"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: "Webhooks",
|
|
43
|
+
values: [
|
|
44
|
+
"—",
|
|
45
|
+
"Included",
|
|
46
|
+
"Included",
|
|
47
|
+
"Included"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: "SSO",
|
|
52
|
+
values: [
|
|
53
|
+
"—",
|
|
54
|
+
"—",
|
|
55
|
+
"—",
|
|
56
|
+
"Included"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
label: "Support",
|
|
61
|
+
values: [
|
|
62
|
+
"—",
|
|
63
|
+
"Email",
|
|
64
|
+
"Priority",
|
|
65
|
+
"Dedicated SLA"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
const DEFAULT_FAQS = [{
|
|
70
|
+
question: "What happens if I exceed my quota?",
|
|
71
|
+
answer: "Requests over your monthly quota return a 429 response so you can test rate-limit handling instead of being cut off unexpectedly."
|
|
72
|
+
}, {
|
|
73
|
+
question: "Can I change plans later?",
|
|
74
|
+
answer: "Yes, upgrade or downgrade any time from your billing page. Changes apply on your next billing cycle."
|
|
75
|
+
}];
|
|
76
|
+
const PricingPage = () => {
|
|
77
|
+
const { pricing, previewPlans } = useDevPortalBillingConfig();
|
|
78
|
+
const isPreview = useDevPortalPreviewMode();
|
|
79
|
+
const pricingEnabled = isPreviewPricingEnabled() || isPreview;
|
|
80
|
+
const livePlans = usePlans();
|
|
81
|
+
const catalog = isPreview ? parseManifestPlans(previewPlans) : livePlans.data;
|
|
82
|
+
const { isPending: authPending } = useAuth();
|
|
83
|
+
const isLoading = isPricingLoading({
|
|
84
|
+
isPreview,
|
|
85
|
+
plansLoading: livePlans.isLoading,
|
|
86
|
+
hasPlans: Boolean(catalog?.plans?.length),
|
|
87
|
+
plansFailed: livePlans.isError,
|
|
88
|
+
authPending
|
|
89
|
+
});
|
|
90
|
+
const isError = isPreview ? false : livePlans.isError;
|
|
91
|
+
if (!pricingEnabled) return /* @__PURE__ */ jsx(Navigate, {
|
|
92
|
+
to: "/",
|
|
93
|
+
replace: true
|
|
94
|
+
});
|
|
95
|
+
const title = pricing?.title ?? "Pay for what you call";
|
|
96
|
+
const subtitle = pricing?.subtitle ?? "Every plan includes the full endpoint catalog. Higher tiers raise your request quota, rate limit, and support level.";
|
|
97
|
+
const plans = catalog?.plans ?? [];
|
|
98
|
+
const featuredSku = pickFeaturedPlanSku(plans);
|
|
99
|
+
const compareRows = buildCompareRows({
|
|
100
|
+
plans,
|
|
101
|
+
configuredRows: pricing?.compareRows,
|
|
102
|
+
isPreview,
|
|
103
|
+
sampleRows: DEFAULT_COMPARE_ROWS
|
|
104
|
+
});
|
|
105
|
+
const faqs = pricing?.faqs && pricing.faqs.length > 0 ? pricing.faqs : DEFAULT_FAQS;
|
|
106
|
+
const compareHeaders = plans.length > 0 ? plans.map((p) => p.displayName) : [
|
|
107
|
+
"Free",
|
|
108
|
+
"Developer",
|
|
109
|
+
"Team",
|
|
110
|
+
"Enterprise"
|
|
111
|
+
];
|
|
112
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
113
|
+
className: "w-full",
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ jsxs(Head, { children: [/* @__PURE__ */ jsx("title", { children: title }), /* @__PURE__ */ jsx("meta", {
|
|
116
|
+
name: "description",
|
|
117
|
+
content: subtitle
|
|
118
|
+
})] }),
|
|
119
|
+
/* @__PURE__ */ jsxs("section", {
|
|
120
|
+
className: "px-6 pb-4 pt-[clamp(3rem,6vw,4.5rem)] text-center",
|
|
121
|
+
children: [
|
|
122
|
+
/* @__PURE__ */ jsx("p", {
|
|
123
|
+
className: "text-xs font-bold uppercase tracking-[0.08em] text-primary",
|
|
124
|
+
children: "Pricing"
|
|
125
|
+
}),
|
|
126
|
+
/* @__PURE__ */ jsx(Heading, {
|
|
127
|
+
level: 1,
|
|
128
|
+
className: "mt-3 text-[clamp(2rem,3.5vw,3rem)] font-extrabold tracking-[-0.02em]",
|
|
129
|
+
"data-testid": "title",
|
|
130
|
+
children: title
|
|
131
|
+
}),
|
|
132
|
+
/* @__PURE__ */ jsx("p", {
|
|
133
|
+
className: "mx-auto mt-3.5 max-w-[34rem] text-lg leading-relaxed text-muted-foreground",
|
|
134
|
+
"data-testid": "subtitle",
|
|
135
|
+
children: subtitle
|
|
136
|
+
})
|
|
137
|
+
]
|
|
138
|
+
}),
|
|
139
|
+
/* @__PURE__ */ jsxs("section", {
|
|
140
|
+
className: "px-6 pb-16 pt-8",
|
|
141
|
+
children: [
|
|
142
|
+
isLoading ? /* @__PURE__ */ jsx("p", {
|
|
143
|
+
className: "text-center text-muted-foreground",
|
|
144
|
+
children: "Loading plans…"
|
|
145
|
+
}) : null,
|
|
146
|
+
isError ? /* @__PURE__ */ jsx("p", {
|
|
147
|
+
className: "text-center text-destructive",
|
|
148
|
+
children: "Unable to load plans."
|
|
149
|
+
}) : null,
|
|
150
|
+
plans.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
151
|
+
className: "mx-auto grid max-w-[1200px] grid-cols-1 items-stretch gap-5 sm:grid-cols-2 lg:grid-cols-4",
|
|
152
|
+
children: plans.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
|
|
153
|
+
plan,
|
|
154
|
+
mode: "public",
|
|
155
|
+
featured: plan.sku === featuredSku
|
|
156
|
+
}, plan.sku))
|
|
157
|
+
}) : !isLoading && !isError ? /* @__PURE__ */ jsx("div", {
|
|
158
|
+
className: "mx-auto max-w-lg rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
|
|
159
|
+
"data-testid": "pricing-empty",
|
|
160
|
+
children: isPreview ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
161
|
+
"No plans are configured yet. Add plans in",
|
|
162
|
+
" ",
|
|
163
|
+
/* @__PURE__ */ jsx("code", {
|
|
164
|
+
className: "rounded bg-muted px-1.5 py-0.5",
|
|
165
|
+
children: "apitogo.config.json"
|
|
166
|
+
}),
|
|
167
|
+
" ",
|
|
168
|
+
"and set",
|
|
169
|
+
" ",
|
|
170
|
+
/* @__PURE__ */ jsx("code", {
|
|
171
|
+
className: "rounded bg-muted px-1.5 py-0.5",
|
|
172
|
+
children: "modules.userPanel.plans.enabled"
|
|
173
|
+
}),
|
|
174
|
+
" ",
|
|
175
|
+
"to true."
|
|
176
|
+
] }) : "Pricing is not available yet. Please check back soon."
|
|
177
|
+
}) : null
|
|
178
|
+
]
|
|
179
|
+
}),
|
|
180
|
+
isLoading || compareRows.length === 0 ? null : /* @__PURE__ */ jsx("section", {
|
|
181
|
+
className: "border-t border-border bg-muted/40 px-6 py-16",
|
|
182
|
+
"data-testid": "pricing-compare",
|
|
183
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
184
|
+
className: "mx-auto max-w-[1200px]",
|
|
185
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
186
|
+
className: "mb-6 text-[clamp(1.5rem,2.5vw,2.25rem)] font-extrabold tracking-[-0.02em]",
|
|
187
|
+
children: "Compare plans"
|
|
188
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
189
|
+
className: "overflow-x-auto rounded-xl border border-border bg-card",
|
|
190
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
191
|
+
className: "min-w-[720px]",
|
|
192
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
193
|
+
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",
|
|
194
|
+
style: { gridTemplateColumns: `minmax(10rem,1.4fr) repeat(${compareHeaders.length}, minmax(6rem,1fr))` },
|
|
195
|
+
children: [/* @__PURE__ */ jsx("div", { children: "Feature" }), compareHeaders.map((header) => /* @__PURE__ */ jsx("div", { children: header }, header))]
|
|
196
|
+
}), compareRows.map((row) => /* @__PURE__ */ jsxs("div", {
|
|
197
|
+
className: "grid gap-0 border-b border-border px-5 py-3.5 text-[0.84375rem] text-muted-foreground last:border-b-0",
|
|
198
|
+
style: { gridTemplateColumns: `minmax(10rem,1.4fr) repeat(${compareHeaders.length}, minmax(6rem,1fr))` },
|
|
199
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
200
|
+
className: "font-medium text-foreground",
|
|
201
|
+
children: row.label
|
|
202
|
+
}), compareHeaders.map((header, index) => /* @__PURE__ */ jsx("div", { children: row.values[index] ?? "—" }, `${row.label}-${header}`))]
|
|
203
|
+
}, row.label))]
|
|
204
|
+
})
|
|
205
|
+
})]
|
|
206
|
+
})
|
|
207
|
+
}),
|
|
208
|
+
/* @__PURE__ */ jsx("section", {
|
|
209
|
+
className: "px-6 py-16",
|
|
210
|
+
"data-testid": "pricing-faq",
|
|
211
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
212
|
+
className: "mx-auto max-w-4xl",
|
|
213
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
214
|
+
className: "mb-8 text-center text-[clamp(1.5rem,2.5vw,2.25rem)] font-extrabold tracking-[-0.02em]",
|
|
215
|
+
children: "Frequently asked questions"
|
|
216
|
+
}), /* @__PURE__ */ jsx("div", { children: faqs.map((faq) => /* @__PURE__ */ jsxs("div", {
|
|
217
|
+
className: "border-b border-border py-5 last:border-b-0",
|
|
218
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
219
|
+
className: "text-[0.9375rem] font-bold",
|
|
220
|
+
children: faq.question
|
|
221
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
222
|
+
className: "mt-2 text-sm leading-relaxed text-muted-foreground",
|
|
223
|
+
children: faq.answer
|
|
224
|
+
})]
|
|
225
|
+
}, faq.question)) })]
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
]
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
//#endregion
|
|
233
|
+
export { PricingPage as default };
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { i as CREDENTIALS_PATH, t as ACCOUNT_PLANS_PATH } from "./paths-ujy9wjVK.mjs";
|
|
2
|
+
import { c as isActiveSubscription, i as formatBillingPeriod, m as parseMonthlyQuotaCalls, o as formatPlanPrice, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
|
|
3
|
+
import "./DevPortalBillingContext-CR83s4LO.mjs";
|
|
4
|
+
import { t as useMySubscriptions } from "./useMySubscriptions-CLh3eFY4.mjs";
|
|
5
|
+
import { t as usePlans } from "./usePlans-DUex2tzT.mjs";
|
|
6
|
+
import { t as useUsage } from "./useUsage-Cx2GB3wg.mjs";
|
|
7
|
+
import { r as checkoutConfirmingText } from "./billing-state-pnT0H1Lm.mjs";
|
|
8
|
+
import { Head, Spinner } from "@lukoweb/apitogo/components";
|
|
9
|
+
import { Link as Link$1, useSearchParams } from "@lukoweb/apitogo/router";
|
|
10
|
+
import { useEffect } from "react";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
import { useMutation } from "@lukoweb/apitogo/react-query";
|
|
13
|
+
import { useAuth } from "@lukoweb/apitogo/hooks";
|
|
14
|
+
import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
|
|
15
|
+
|
|
16
|
+
//#region src/hooks/useBillingPortal.ts
|
|
17
|
+
function useBillingPortal() {
|
|
18
|
+
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
19
|
+
return useMutation({ mutationFn: async (returnUrl) => {
|
|
20
|
+
if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
|
|
21
|
+
const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/billing-portal`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
Accept: "application/json",
|
|
25
|
+
"Content-Type": "application/json"
|
|
26
|
+
},
|
|
27
|
+
credentials: "include",
|
|
28
|
+
body: JSON.stringify({ returnUrl: returnUrl ?? `${window.location.origin}/account/subscriptions` })
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) throw new Error("Unable to open billing portal.");
|
|
31
|
+
const body = await response.json();
|
|
32
|
+
if (!body.url) throw new Error("Billing portal URL was not returned.");
|
|
33
|
+
window.location.assign(body.url);
|
|
34
|
+
} });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/pages/SubscriptionsPage.tsx
|
|
39
|
+
const SubscriptionsPage = () => {
|
|
40
|
+
const auth = useAuth();
|
|
41
|
+
const [searchParams] = useSearchParams();
|
|
42
|
+
const checkoutSuccess = searchParams.get("checkout") === "success";
|
|
43
|
+
const checkoutPaid = searchParams.get("paid") !== "0";
|
|
44
|
+
const { data: me, isLoading, refetch } = useMySubscriptions();
|
|
45
|
+
const { data: catalog } = usePlans();
|
|
46
|
+
const billingPortal = useBillingPortal();
|
|
47
|
+
const { data: usage } = useUsage("mtd");
|
|
48
|
+
const planBySku = new Map((catalog?.plans ?? []).map((plan) => [plan.sku, plan]));
|
|
49
|
+
const subscriptions = me?.subscriptions ?? [];
|
|
50
|
+
const activeSubscription = subscriptions.find((sub) => isActiveSubscription(sub.status));
|
|
51
|
+
const activePlan = activeSubscription ? planBySku.get(activeSubscription.planSku) : void 0;
|
|
52
|
+
const confirmingPayment = checkoutSuccess && !isLoading && !activeSubscription;
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!confirmingPayment) return;
|
|
55
|
+
let attempts = 0;
|
|
56
|
+
const timer = setInterval(() => {
|
|
57
|
+
attempts += 1;
|
|
58
|
+
refetch();
|
|
59
|
+
if (attempts >= 30) clearInterval(timer);
|
|
60
|
+
}, 2e3);
|
|
61
|
+
return () => clearInterval(timer);
|
|
62
|
+
}, [confirmingPayment, refetch]);
|
|
63
|
+
const monthlyQuota = activePlan ? parseMonthlyQuotaCalls(activePlan.limitsJson) : null;
|
|
64
|
+
const usedRequests = usage?.requestCount ?? 0;
|
|
65
|
+
const quotaPct = monthlyQuota && monthlyQuota > 0 ? Math.min(100, Math.round(usedRequests / monthlyQuota * 100)) : null;
|
|
66
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
67
|
+
className: "px-8 py-7",
|
|
68
|
+
children: [
|
|
69
|
+
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Billing" }) }),
|
|
70
|
+
/* @__PURE__ */ jsx("h1", {
|
|
71
|
+
className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
|
|
72
|
+
children: "Billing"
|
|
73
|
+
}),
|
|
74
|
+
/* @__PURE__ */ jsx("p", {
|
|
75
|
+
className: "mb-8 text-muted-foreground",
|
|
76
|
+
children: "Manage your plan, payment method, and invoices."
|
|
77
|
+
}),
|
|
78
|
+
confirmingPayment ? /* @__PURE__ */ jsx("div", {
|
|
79
|
+
className: "mb-6 rounded-xl border border-border bg-card p-4 text-sm",
|
|
80
|
+
children: checkoutConfirmingText(checkoutPaid)
|
|
81
|
+
}) : null,
|
|
82
|
+
checkoutSuccess && activeSubscription ? /* @__PURE__ */ jsxs("div", {
|
|
83
|
+
className: "mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-sm",
|
|
84
|
+
children: [
|
|
85
|
+
"Your subscription is active.",
|
|
86
|
+
" ",
|
|
87
|
+
/* @__PURE__ */ jsx(Link$1, {
|
|
88
|
+
to: CREDENTIALS_PATH,
|
|
89
|
+
className: "font-medium underline",
|
|
90
|
+
children: "View your API credentials"
|
|
91
|
+
}),
|
|
92
|
+
"."
|
|
93
|
+
]
|
|
94
|
+
}) : null,
|
|
95
|
+
isLoading ? /* @__PURE__ */ jsx("p", {
|
|
96
|
+
className: "text-muted-foreground",
|
|
97
|
+
children: "Loading billing details…"
|
|
98
|
+
}) : null,
|
|
99
|
+
!isLoading && activePlan && activeSubscription ? /* @__PURE__ */ jsxs("div", {
|
|
100
|
+
className: "mb-8 rounded-xl border border-border bg-card p-6",
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ jsx("p", {
|
|
103
|
+
className: "text-xs font-medium uppercase tracking-[0.05em] text-muted-foreground",
|
|
104
|
+
children: "Current plan"
|
|
105
|
+
}),
|
|
106
|
+
/* @__PURE__ */ jsx("h2", {
|
|
107
|
+
className: "mt-2 text-2xl font-bold",
|
|
108
|
+
children: activePlan.displayName
|
|
109
|
+
}),
|
|
110
|
+
/* @__PURE__ */ jsxs("p", {
|
|
111
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
112
|
+
children: [
|
|
113
|
+
formatPlanPrice(activePlan),
|
|
114
|
+
" ·",
|
|
115
|
+
" ",
|
|
116
|
+
formatBillingPeriod(activePlan.billingPeriod)
|
|
117
|
+
]
|
|
118
|
+
}),
|
|
119
|
+
activeSubscription.currentPeriodEnd ? /* @__PURE__ */ jsxs("p", {
|
|
120
|
+
className: "mt-3 text-sm text-muted-foreground",
|
|
121
|
+
children: [
|
|
122
|
+
"Renews",
|
|
123
|
+
" ",
|
|
124
|
+
new Date(activeSubscription.currentPeriodEnd).toLocaleDateString()
|
|
125
|
+
]
|
|
126
|
+
}) : null,
|
|
127
|
+
quotaPct != null && monthlyQuota != null ? /* @__PURE__ */ jsxs("div", {
|
|
128
|
+
className: "mt-5",
|
|
129
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
130
|
+
className: "mb-1.5 flex justify-between text-xs text-muted-foreground",
|
|
131
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Monthly requests" }), /* @__PURE__ */ jsxs("span", { children: [
|
|
132
|
+
usedRequests.toLocaleString(),
|
|
133
|
+
" /",
|
|
134
|
+
" ",
|
|
135
|
+
monthlyQuota.toLocaleString()
|
|
136
|
+
] })]
|
|
137
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
138
|
+
className: "h-2 rounded-full bg-muted",
|
|
139
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
140
|
+
className: "h-full rounded-full bg-primary transition-[width]",
|
|
141
|
+
style: { width: `${quotaPct}%` }
|
|
142
|
+
})
|
|
143
|
+
})]
|
|
144
|
+
}) : null
|
|
145
|
+
]
|
|
146
|
+
}) : null,
|
|
147
|
+
!isLoading && subscriptions.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
148
|
+
className: "mb-8 rounded-xl border border-dashed p-8 text-center",
|
|
149
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
150
|
+
className: "text-muted-foreground",
|
|
151
|
+
children: "You do not have an active subscription yet."
|
|
152
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
153
|
+
className: "mt-4",
|
|
154
|
+
asChild: true,
|
|
155
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
156
|
+
to: ACCOUNT_PLANS_PATH,
|
|
157
|
+
children: "View plans"
|
|
158
|
+
})
|
|
159
|
+
})]
|
|
160
|
+
}) : null,
|
|
161
|
+
!isLoading && subscriptions.length > 1 ? /* @__PURE__ */ jsx("div", {
|
|
162
|
+
className: "mb-8 space-y-4",
|
|
163
|
+
children: subscriptions.map((subscription) => {
|
|
164
|
+
const plan = planBySku.get(subscription.planSku);
|
|
165
|
+
return /* @__PURE__ */ jsx("article", {
|
|
166
|
+
className: "rounded-xl border bg-card p-5",
|
|
167
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
168
|
+
className: "flex flex-wrap items-start justify-between gap-4",
|
|
169
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
170
|
+
className: "font-semibold",
|
|
171
|
+
children: plan?.displayName ?? subscription.planSku
|
|
172
|
+
}), plan ? /* @__PURE__ */ jsxs("p", {
|
|
173
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
174
|
+
children: [
|
|
175
|
+
formatPlanPrice(plan),
|
|
176
|
+
" ·",
|
|
177
|
+
" ",
|
|
178
|
+
formatBillingPeriod(plan.billingPeriod)
|
|
179
|
+
]
|
|
180
|
+
}) : null] }), /* @__PURE__ */ jsx("span", {
|
|
181
|
+
className: "rounded-full bg-muted px-3 py-1 text-xs font-medium text-muted-foreground",
|
|
182
|
+
children: subscription.status
|
|
183
|
+
})]
|
|
184
|
+
})
|
|
185
|
+
}, `${subscription.planSku}-${subscription.status}`);
|
|
186
|
+
})
|
|
187
|
+
}) : null,
|
|
188
|
+
/* @__PURE__ */ jsxs("div", {
|
|
189
|
+
className: "flex flex-wrap gap-3",
|
|
190
|
+
children: [
|
|
191
|
+
/* @__PURE__ */ jsx(Button$1, {
|
|
192
|
+
variant: "outline",
|
|
193
|
+
asChild: true,
|
|
194
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
195
|
+
to: ACCOUNT_PLANS_PATH,
|
|
196
|
+
children: "Change plan"
|
|
197
|
+
})
|
|
198
|
+
}),
|
|
199
|
+
auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
|
|
200
|
+
variant: "default",
|
|
201
|
+
disabled: billingPortal.isPending,
|
|
202
|
+
onClick: () => {
|
|
203
|
+
billingPortal.mutate();
|
|
204
|
+
},
|
|
205
|
+
children: billingPortal.isPending ? /* @__PURE__ */ jsx(Spinner, {}) : "Manage payment & invoices"
|
|
206
|
+
}) : null,
|
|
207
|
+
auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
|
|
208
|
+
variant: "secondary",
|
|
209
|
+
asChild: true,
|
|
210
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
211
|
+
to: CREDENTIALS_PATH,
|
|
212
|
+
children: "API credentials"
|
|
213
|
+
})
|
|
214
|
+
}) : null
|
|
215
|
+
]
|
|
216
|
+
}),
|
|
217
|
+
billingPortal.isError ? /* @__PURE__ */ jsx("p", {
|
|
218
|
+
className: "mt-4 text-sm text-destructive",
|
|
219
|
+
children: billingPortal.error instanceof Error ? billingPortal.error.message : "Unable to open billing portal."
|
|
220
|
+
}) : null
|
|
221
|
+
]
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
//#endregion
|
|
226
|
+
export { SubscriptionsPage as default };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
|
|
2
|
+
import { t as useUsage } from "./useUsage-Cx2GB3wg.mjs";
|
|
3
|
+
import { Head } from "@lukoweb/apitogo/components";
|
|
4
|
+
import { Link as Link$1 } from "@lukoweb/apitogo/router";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { useAuth } from "@lukoweb/apitogo/hooks";
|
|
8
|
+
import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
|
|
9
|
+
|
|
10
|
+
//#region src/pages/UsagePage.tsx
|
|
11
|
+
const RANGES = [
|
|
12
|
+
{
|
|
13
|
+
value: "24h",
|
|
14
|
+
label: "24h"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
value: "7d",
|
|
18
|
+
label: "7d"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
value: "30d",
|
|
22
|
+
label: "30d"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
value: "90d",
|
|
26
|
+
label: "90d"
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
function formatNumber(value) {
|
|
30
|
+
return new Intl.NumberFormat().format(value);
|
|
31
|
+
}
|
|
32
|
+
function formatPct(rate) {
|
|
33
|
+
return `${(rate * 100).toFixed(rate >= .1 ? 1 : 2)}%`;
|
|
34
|
+
}
|
|
35
|
+
const UsagePage = () => {
|
|
36
|
+
const auth = useAuth();
|
|
37
|
+
const [range, setRange] = useState("30d");
|
|
38
|
+
const { data, isLoading, isError } = useUsage(range);
|
|
39
|
+
const maxBar = Math.max(1, ...data?.series.map((point) => point.requestCount) ?? [1]);
|
|
40
|
+
const endpointTotal = data?.byEndpoint.reduce((sum, item) => sum + item.count, 0) ?? 0;
|
|
41
|
+
if (!auth.isAuthenticated) return /* @__PURE__ */ jsxs("div", {
|
|
42
|
+
className: "px-8 py-7",
|
|
43
|
+
children: [
|
|
44
|
+
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Usage" }) }),
|
|
45
|
+
/* @__PURE__ */ jsx("h1", {
|
|
46
|
+
className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
|
|
47
|
+
children: "Usage"
|
|
48
|
+
}),
|
|
49
|
+
/* @__PURE__ */ jsx("p", {
|
|
50
|
+
className: "text-muted-foreground",
|
|
51
|
+
children: "Sign in to view your API usage."
|
|
52
|
+
}),
|
|
53
|
+
auth.isAuthEnabled ? /* @__PURE__ */ jsx(Button$1, {
|
|
54
|
+
asChild: true,
|
|
55
|
+
className: "mt-4",
|
|
56
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
57
|
+
to: "/signin",
|
|
58
|
+
children: "Sign in"
|
|
59
|
+
})
|
|
60
|
+
}) : null
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
64
|
+
className: "px-8 py-7",
|
|
65
|
+
children: [
|
|
66
|
+
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Usage" }) }),
|
|
67
|
+
/* @__PURE__ */ jsxs("div", {
|
|
68
|
+
className: "mb-6 flex flex-wrap items-center justify-between gap-3",
|
|
69
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
70
|
+
className: "text-[1.6rem] font-bold tracking-[-0.01em]",
|
|
71
|
+
children: "Usage"
|
|
72
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
73
|
+
className: "flex gap-1 rounded-lg border border-border bg-card p-1",
|
|
74
|
+
children: RANGES.map((item) => /* @__PURE__ */ jsx("button", {
|
|
75
|
+
type: "button",
|
|
76
|
+
onClick: () => setRange(item.value),
|
|
77
|
+
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"}`,
|
|
78
|
+
children: item.label
|
|
79
|
+
}, item.value))
|
|
80
|
+
})]
|
|
81
|
+
}),
|
|
82
|
+
isLoading ? /* @__PURE__ */ jsx("p", {
|
|
83
|
+
className: "text-sm text-muted-foreground",
|
|
84
|
+
children: "Loading usage…"
|
|
85
|
+
}) : null,
|
|
86
|
+
isError ? /* @__PURE__ */ jsx("p", {
|
|
87
|
+
className: "text-sm text-destructive",
|
|
88
|
+
children: "Unable to load usage data."
|
|
89
|
+
}) : null,
|
|
90
|
+
/* @__PURE__ */ jsxs("div", {
|
|
91
|
+
className: "mb-5 grid grid-cols-1 gap-4 sm:grid-cols-3",
|
|
92
|
+
children: [
|
|
93
|
+
/* @__PURE__ */ jsxs("div", {
|
|
94
|
+
className: "rounded-xl border border-border bg-card p-4",
|
|
95
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
96
|
+
className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
|
|
97
|
+
children: "Requests"
|
|
98
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
99
|
+
className: "mt-1.5 text-2xl font-bold",
|
|
100
|
+
children: data ? formatNumber(data.requestCount) : "—"
|
|
101
|
+
})]
|
|
102
|
+
}),
|
|
103
|
+
/* @__PURE__ */ jsxs("div", {
|
|
104
|
+
className: "rounded-xl border border-border bg-card p-4",
|
|
105
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
106
|
+
className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
|
|
107
|
+
children: "Avg latency"
|
|
108
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
109
|
+
className: "mt-1.5 text-2xl font-bold",
|
|
110
|
+
children: data?.avgLatencyMs != null ? `${data.avgLatencyMs} ms` : "—"
|
|
111
|
+
})]
|
|
112
|
+
}),
|
|
113
|
+
/* @__PURE__ */ jsxs("div", {
|
|
114
|
+
className: "rounded-xl border border-border bg-card p-4",
|
|
115
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
116
|
+
className: "text-xs uppercase tracking-[0.05em] text-muted-foreground",
|
|
117
|
+
children: "Error rate"
|
|
118
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
119
|
+
className: "mt-1.5 text-2xl font-bold",
|
|
120
|
+
children: data ? formatPct(data.errorRate) : "—"
|
|
121
|
+
})]
|
|
122
|
+
})
|
|
123
|
+
]
|
|
124
|
+
}),
|
|
125
|
+
/* @__PURE__ */ jsxs("div", {
|
|
126
|
+
className: "mb-5 rounded-xl border border-border bg-card p-6",
|
|
127
|
+
children: [/* @__PURE__ */ jsxs("h2", {
|
|
128
|
+
className: "mb-4 text-lg font-semibold",
|
|
129
|
+
children: [
|
|
130
|
+
"Requests over time (",
|
|
131
|
+
range,
|
|
132
|
+
")"
|
|
133
|
+
]
|
|
134
|
+
}), data && data.series.some((p) => p.requestCount > 0) ? /* @__PURE__ */ jsx("div", {
|
|
135
|
+
className: "flex h-40 items-end gap-1.5",
|
|
136
|
+
children: data.series.map((point) => /* @__PURE__ */ jsx("div", {
|
|
137
|
+
title: `${point.dayUtc}: ${point.requestCount}`,
|
|
138
|
+
className: "min-w-0 flex-1 rounded-t-sm bg-primary",
|
|
139
|
+
style: { height: `${Math.max(4, point.requestCount / maxBar * 100)}%` }
|
|
140
|
+
}, point.dayUtc))
|
|
141
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
142
|
+
className: "text-sm text-muted-foreground",
|
|
143
|
+
children: "No request data for this range yet."
|
|
144
|
+
})]
|
|
145
|
+
}),
|
|
146
|
+
/* @__PURE__ */ jsxs("div", {
|
|
147
|
+
className: "grid grid-cols-1 gap-5 lg:grid-cols-[1.2fr_1fr]",
|
|
148
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
149
|
+
className: "rounded-xl border border-border bg-card p-6",
|
|
150
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
151
|
+
className: "mb-4 text-lg font-semibold",
|
|
152
|
+
children: "By endpoint"
|
|
153
|
+
}), data && data.byEndpoint.length > 0 ? /* @__PURE__ */ jsx("div", {
|
|
154
|
+
className: "space-y-3.5",
|
|
155
|
+
children: data.byEndpoint.map((item) => {
|
|
156
|
+
const pct = endpointTotal > 0 ? Math.round(item.count / endpointTotal * 100) : 0;
|
|
157
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
158
|
+
className: "mb-1 flex justify-between text-[13px]",
|
|
159
|
+
children: [/* @__PURE__ */ jsx("code", {
|
|
160
|
+
className: "font-mono text-foreground",
|
|
161
|
+
children: item.path
|
|
162
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
163
|
+
className: "text-muted-foreground",
|
|
164
|
+
children: [pct, "%"]
|
|
165
|
+
})]
|
|
166
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
167
|
+
className: "h-1.5 rounded-full bg-muted",
|
|
168
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
169
|
+
className: "h-full rounded-full bg-primary",
|
|
170
|
+
style: { width: `${pct}%` }
|
|
171
|
+
})
|
|
172
|
+
})] }, item.path);
|
|
173
|
+
})
|
|
174
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
175
|
+
className: "text-sm text-muted-foreground",
|
|
176
|
+
children: "No endpoint data."
|
|
177
|
+
})]
|
|
178
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
179
|
+
className: "rounded-xl border border-border bg-card p-6",
|
|
180
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
181
|
+
className: "mb-4 text-lg font-semibold",
|
|
182
|
+
children: "Status codes"
|
|
183
|
+
}), data && data.byStatus.length > 0 ? /* @__PURE__ */ jsx("div", { children: data.byStatus.map((item) => /* @__PURE__ */ jsxs("div", {
|
|
184
|
+
className: "flex items-center justify-between border-b border-border py-2.5 last:border-0",
|
|
185
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
186
|
+
className: "text-sm font-medium",
|
|
187
|
+
children: item.label
|
|
188
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
189
|
+
className: "text-sm font-bold",
|
|
190
|
+
children: formatNumber(item.count)
|
|
191
|
+
})]
|
|
192
|
+
}, item.label)) }) : /* @__PURE__ */ jsx("p", {
|
|
193
|
+
className: "text-sm text-muted-foreground",
|
|
194
|
+
children: "No status data."
|
|
195
|
+
})]
|
|
196
|
+
})]
|
|
197
|
+
})
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
export { UsagePage as default };
|