@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.
@@ -0,0 +1,208 @@
1
+ import { c as isActiveSubscription, n as useDevPortalPreviewMode, p as parseManifestPlans, r as useIsDemoSession, t as useDevPortalApiBaseUrl } 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 useMySubscriptions } from "./useMySubscriptions-CLh3eFY4.mjs";
5
+ import { t as usePlans } from "./usePlans-DUex2tzT.mjs";
6
+ import { Head } from "@lukoweb/apitogo/components";
7
+ import { CreditCardIcon } from "@lukoweb/apitogo/icons";
8
+ import { useSearchParams } from "@lukoweb/apitogo/router";
9
+ import { useState } from "react";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { useMutation, useQueryClient } from "@lukoweb/apitogo/react-query";
12
+
13
+ //#region \0rolldown/runtime.js
14
+ var __defProp = Object.defineProperty;
15
+ var __exportAll = (all, no_symbols) => {
16
+ let target = {};
17
+ for (var name in all) {
18
+ __defProp(target, name, {
19
+ get: all[name],
20
+ enumerable: true
21
+ });
22
+ }
23
+ if (!no_symbols) {
24
+ __defProp(target, Symbol.toStringTag, { value: "Module" });
25
+ }
26
+ return target;
27
+ };
28
+
29
+ //#endregion
30
+ //#region src/hooks/useCancelSubscription.ts
31
+ function useCancelSubscription() {
32
+ const apiBaseUrl = useDevPortalApiBaseUrl();
33
+ const queryClient = useQueryClient();
34
+ return useMutation({
35
+ mutationFn: async (planSku) => {
36
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
37
+ const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/cancel`, {
38
+ method: "POST",
39
+ headers: {
40
+ Accept: "application/json",
41
+ "Content-Type": "application/json"
42
+ },
43
+ credentials: "include",
44
+ body: JSON.stringify({ planSku })
45
+ });
46
+ if (!response.ok) {
47
+ const detail = await response.text();
48
+ throw new Error(detail || "Failed to cancel subscription.");
49
+ }
50
+ return await response.json();
51
+ },
52
+ onSuccess: async () => {
53
+ await queryClient.invalidateQueries({ queryKey: ["dev-portal-me", apiBaseUrl] });
54
+ }
55
+ });
56
+ }
57
+
58
+ //#endregion
59
+ //#region src/hooks/useChangePlan.ts
60
+ function useChangePlan() {
61
+ const apiBaseUrl = useDevPortalApiBaseUrl();
62
+ const queryClient = useQueryClient();
63
+ return useMutation({
64
+ mutationFn: async ({ fromPlanSku, toPlanSku }) => {
65
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
66
+ const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/change`, {
67
+ method: "POST",
68
+ headers: {
69
+ Accept: "application/json",
70
+ "Content-Type": "application/json"
71
+ },
72
+ credentials: "include",
73
+ body: JSON.stringify({
74
+ fromPlanSku,
75
+ toPlanSku
76
+ })
77
+ });
78
+ if (!response.ok) {
79
+ const detail = await response.text();
80
+ throw new Error(detail || "Failed to change plan.");
81
+ }
82
+ return await response.json();
83
+ },
84
+ onSuccess: async (result) => {
85
+ if (result.checkoutUrl) {
86
+ window.location.assign(result.checkoutUrl);
87
+ return;
88
+ }
89
+ await queryClient.invalidateQueries({ queryKey: ["dev-portal-me", apiBaseUrl] });
90
+ }
91
+ });
92
+ }
93
+
94
+ //#endregion
95
+ //#region src/pages/AccountPlansPage.tsx
96
+ var AccountPlansPage_exports = /* @__PURE__ */ __exportAll({ default: () => AccountPlansPage });
97
+ const AccountPlansPage = () => {
98
+ const [searchParams] = useSearchParams();
99
+ const checkoutCancelled = searchParams.get("checkout") === "cancelled";
100
+ const { previewPlans } = useDevPortalBillingConfig();
101
+ const isPreview = useDevPortalPreviewMode();
102
+ const isDemoSession = useIsDemoSession();
103
+ const livePlans = usePlans();
104
+ const catalog = isPreview ? parseManifestPlans(previewPlans) : livePlans.data;
105
+ const isLoading = isPreview ? false : livePlans.isLoading;
106
+ const isError = isPreview ? false : livePlans.isError;
107
+ const { data: me } = useMySubscriptions();
108
+ const cancelMutation = useCancelSubscription();
109
+ const changeMutation = useChangePlan();
110
+ const [cancelingSku, setCancelingSku] = useState(null);
111
+ const [switchingSku, setSwitchingSku] = useState(null);
112
+ const subscriptionBySku = new Map((me?.subscriptions ?? []).map((sub) => [sub.planSku, sub]));
113
+ const activePlanSkus = (me?.subscriptions ?? []).filter((sub) => isActiveSubscription(sub.status)).map((sub) => sub.planSku);
114
+ const currentPlanSku = activePlanSkus.length === 1 ? activePlanSkus[0] : null;
115
+ const handleSwitch = async (toPlanSku) => {
116
+ if (!currentPlanSku) return;
117
+ setSwitchingSku(toPlanSku);
118
+ try {
119
+ await changeMutation.mutateAsync({
120
+ fromPlanSku: currentPlanSku,
121
+ toPlanSku
122
+ });
123
+ } finally {
124
+ setSwitchingSku(null);
125
+ }
126
+ };
127
+ const handleUnsubscribe = async (planSku) => {
128
+ setCancelingSku(planSku);
129
+ try {
130
+ await cancelMutation.mutateAsync(planSku);
131
+ } finally {
132
+ setCancelingSku(null);
133
+ }
134
+ };
135
+ return /* @__PURE__ */ jsxs("div", {
136
+ className: "mx-auto max-w-screen-xl px-4 pb-16 pt-(--padding-content-top) lg:px-8",
137
+ children: [
138
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Plans & billing" }) }),
139
+ /* @__PURE__ */ jsxs("div", {
140
+ className: "mb-8 flex items-start gap-4",
141
+ children: [/* @__PURE__ */ jsx("div", {
142
+ className: "flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary",
143
+ children: /* @__PURE__ */ jsx(CreditCardIcon, { size: 24 })
144
+ }), /* @__PURE__ */ jsxs("div", {
145
+ className: "space-y-1",
146
+ children: [
147
+ /* @__PURE__ */ jsx("h1", {
148
+ className: "text-3xl font-semibold tracking-tight",
149
+ children: "Plans & billing"
150
+ }),
151
+ /* @__PURE__ */ jsx("p", {
152
+ className: "max-w-2xl text-muted-foreground",
153
+ children: "Compare plans, manage your subscriptions, and change tiers."
154
+ }),
155
+ checkoutCancelled ? /* @__PURE__ */ jsx("p", {
156
+ className: "max-w-2xl text-sm text-muted-foreground",
157
+ children: "Checkout was cancelled and you were not charged."
158
+ }) : null
159
+ ]
160
+ })]
161
+ }),
162
+ isDemoSession ? /* @__PURE__ */ jsx("div", {
163
+ className: "mb-8 rounded-xl border border-dashed bg-muted/20 p-6 text-center text-sm text-muted-foreground",
164
+ children: "Local demo mode shows plans from your manifest with a sample active subscription. Billing actions activate after publish."
165
+ }) : null,
166
+ cancelMutation.isError ? /* @__PURE__ */ jsx("div", {
167
+ className: "mb-6 rounded-xl border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive",
168
+ children: cancelMutation.error instanceof Error ? cancelMutation.error.message : "Failed to cancel subscription."
169
+ }) : null,
170
+ changeMutation.isError ? /* @__PURE__ */ jsx("div", {
171
+ className: "mb-6 rounded-xl border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive",
172
+ children: changeMutation.error instanceof Error ? changeMutation.error.message : "Failed to change plan."
173
+ }) : null,
174
+ isLoading ? /* @__PURE__ */ jsx("p", {
175
+ className: "text-muted-foreground",
176
+ children: "Loading plans…"
177
+ }) : null,
178
+ isError ? /* @__PURE__ */ jsx("p", {
179
+ className: "text-destructive",
180
+ children: "Unable to load plans."
181
+ }) : null,
182
+ catalog && catalog.plans.length > 0 ? /* @__PURE__ */ jsx("div", {
183
+ className: "grid grid-cols-1 justify-center gap-6 sm:grid-cols-[repeat(auto-fit,minmax(280px,max-content))]",
184
+ children: catalog.plans.map((plan) => {
185
+ const subscription = subscriptionBySku.get(plan.sku);
186
+ const isSubscribed = isActiveSubscription(subscription?.status ?? "");
187
+ return /* @__PURE__ */ jsx(PricingCard, {
188
+ plan,
189
+ mode: "account",
190
+ subscriptionStatus: subscription?.status,
191
+ isSubscribed,
192
+ onUnsubscribe: isPreview || isDemoSession ? void 0 : handleUnsubscribe,
193
+ onSwitch: currentPlanSku && !isPreview && !isDemoSession ? handleSwitch : void 0,
194
+ isSwitching: switchingSku === plan.sku,
195
+ isCanceling: cancelingSku === plan.sku,
196
+ cancelDisabled: isPreview || isDemoSession
197
+ }, plan.sku);
198
+ })
199
+ }) : !isLoading && !isError ? /* @__PURE__ */ jsx("div", {
200
+ className: "rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
201
+ children: "No plans are available yet. Sync plans from your API ToGo manifest or platform gateway."
202
+ }) : null
203
+ ]
204
+ });
205
+ };
206
+
207
+ //#endregion
208
+ export { AccountPlansPage_exports as n, AccountPlansPage as t };
@@ -0,0 +1,285 @@
1
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
2
+ import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
3
+ import { t as createDemoApiKeys } from "./demo-session-Cez740bi.mjs";
4
+ import { Spinner } from "@lukoweb/apitogo/components";
5
+ import { KeyRoundIcon, TrashIcon } from "@lukoweb/apitogo/icons";
6
+ import { useState } from "react";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { useMutation, useQuery, useQueryClient } from "@lukoweb/apitogo/react-query";
9
+ import { useAuth } from "@lukoweb/apitogo/hooks";
10
+ import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
11
+ import { Alert, AlertTitle } from "@lukoweb/apitogo/ui/Alert.js";
12
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@lukoweb/apitogo/ui/Dialog.js";
13
+ import { Input } from "@lukoweb/apitogo/ui/Input.js";
14
+ import { Secret } from "@lukoweb/apitogo/ui/Secret.js";
15
+
16
+ //#region src/hooks/useApiKeys.ts
17
+ /** Thrown when the developer has hit their plan's per-developer key cap. */
18
+ const API_KEY_CAP_REACHED = "API_KEY_CAP_REACHED";
19
+ function apiKeysQueryKey(apiBaseUrl) {
20
+ return ["dev-portal-api-keys", apiBaseUrl];
21
+ }
22
+ function useApiKeys() {
23
+ const auth = useAuth();
24
+ const apiBaseUrl = useDevPortalApiBaseUrl();
25
+ const isPreview = useDevPortalPreviewMode();
26
+ const isDemoSession = useIsDemoSession();
27
+ const { previewPlans } = useDevPortalBillingConfig();
28
+ const liveQuery = useQuery({
29
+ queryKey: apiKeysQueryKey(apiBaseUrl),
30
+ enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
31
+ queryFn: async () => {
32
+ const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
33
+ method: "GET",
34
+ headers: { Accept: "application/json" },
35
+ credentials: "include"
36
+ });
37
+ if (response.status === 404) throw new Error("NO_ENTITLEMENT");
38
+ if (!response.ok) throw new Error("Failed to load API keys.");
39
+ return await response.json();
40
+ },
41
+ retry: (failureCount, error) => error instanceof Error && error.message === "NO_ENTITLEMENT" ? false : failureCount < 2
42
+ });
43
+ if (isDemoSession && auth.isAuthenticated) return {
44
+ ...liveQuery,
45
+ data: createDemoApiKeys(previewPlans),
46
+ error: null,
47
+ isError: false,
48
+ isFetching: false,
49
+ isLoading: false,
50
+ isPending: false,
51
+ isSuccess: true,
52
+ status: "success"
53
+ };
54
+ return liveQuery;
55
+ }
56
+ function useCreateApiKey() {
57
+ const apiBaseUrl = useDevPortalApiBaseUrl();
58
+ const queryClient = useQueryClient();
59
+ return useMutation({
60
+ mutationFn: async ({ name, planSku }) => {
61
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
62
+ const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
63
+ method: "POST",
64
+ headers: {
65
+ Accept: "application/json",
66
+ "Content-Type": "application/json"
67
+ },
68
+ credentials: "include",
69
+ body: JSON.stringify({
70
+ name,
71
+ planSku
72
+ })
73
+ });
74
+ if (response.status === 409) throw new Error(API_KEY_CAP_REACHED);
75
+ if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Failed to create API key.");
76
+ return await response.json();
77
+ },
78
+ onSuccess: async () => {
79
+ await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
80
+ }
81
+ });
82
+ }
83
+ function useRevokeApiKey() {
84
+ const apiBaseUrl = useDevPortalApiBaseUrl();
85
+ const queryClient = useQueryClient();
86
+ return useMutation({
87
+ mutationFn: async (keyId) => {
88
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
89
+ const response = await fetch(`${apiBaseUrl}/api/v1/api-keys/${keyId}`, {
90
+ method: "DELETE",
91
+ headers: { Accept: "application/json" },
92
+ credentials: "include"
93
+ });
94
+ if (!response.ok && response.status !== 204) throw new Error(await readProblemDetail(response) ?? "Failed to revoke API key.");
95
+ },
96
+ onSuccess: async () => {
97
+ await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
98
+ }
99
+ });
100
+ }
101
+ async function readProblemDetail(response) {
102
+ try {
103
+ return (await response.json()).detail?.trim() || null;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ //#endregion
110
+ //#region src/components/ApiKeysPanel.tsx
111
+ /**
112
+ * Lists gateway API keys for one plan with create and revoke. The plaintext key is returned by the
113
+ * platform exactly once, so it is held in local state until dismissed and never re-fetched.
114
+ */
115
+ const ApiKeysPanel = ({ plan, header, readOnly = false }) => {
116
+ const createKey = useCreateApiKey();
117
+ const revokeKey = useRevokeApiKey();
118
+ const [name, setName] = useState("");
119
+ const [newKey, setNewKey] = useState(null);
120
+ const [openRevokeId, setOpenRevokeId] = useState(null);
121
+ const [pendingRevokeId, setPendingRevokeId] = useState(null);
122
+ const activeKeys = plan.keys.filter((key) => key.status === "active");
123
+ const planActiveCount = activeKeys.length;
124
+ const capLabel = plan.maxKeys == null ? `${plan.activeCount} active` : `${plan.activeCount} of ${plan.maxKeys} used`;
125
+ const canCreate = !readOnly && plan.canCreate;
126
+ const handleCreate = () => {
127
+ const trimmed = name.trim();
128
+ if (!trimmed) return;
129
+ createKey.mutate({
130
+ name: trimmed,
131
+ planSku: plan.planSku
132
+ }, { onSuccess: (key) => {
133
+ setNewKey(key);
134
+ setName("");
135
+ } });
136
+ };
137
+ const handleRevoke = (keyId) => {
138
+ setPendingRevokeId(keyId);
139
+ revokeKey.mutate(keyId, { onSettled: () => {
140
+ setPendingRevokeId(null);
141
+ setOpenRevokeId(null);
142
+ } });
143
+ };
144
+ return /* @__PURE__ */ jsxs("article", {
145
+ className: "rounded-xl border bg-card/60 p-6 space-y-5",
146
+ children: [
147
+ /* @__PURE__ */ jsxs("div", {
148
+ className: "flex flex-wrap items-start justify-between gap-3",
149
+ children: [/* @__PURE__ */ jsxs("div", {
150
+ className: "space-y-1",
151
+ children: [/* @__PURE__ */ jsxs("h2", {
152
+ className: "text-lg font-medium",
153
+ children: [plan.displayName, " API keys"]
154
+ }), /* @__PURE__ */ jsxs("p", {
155
+ className: "text-sm text-muted-foreground",
156
+ children: [
157
+ "Send a key in the",
158
+ " ",
159
+ /* @__PURE__ */ jsx("code", {
160
+ className: "rounded bg-muted px-1 py-0.5 text-xs",
161
+ children: header
162
+ }),
163
+ " ",
164
+ "header when calling this plan's endpoints. Keys share this plan's rate limit and key cap."
165
+ ]
166
+ })]
167
+ }), /* @__PURE__ */ jsx("span", {
168
+ className: "shrink-0 rounded-full border px-3 py-1 text-xs text-muted-foreground",
169
+ children: capLabel
170
+ })]
171
+ }),
172
+ newKey?.key ? /* @__PURE__ */ jsxs(Alert, { children: [
173
+ /* @__PURE__ */ jsx(KeyRoundIcon, { size: 16 }),
174
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Copy your new key now — it is shown only once." }),
175
+ /* @__PURE__ */ jsxs("div", {
176
+ className: "mt-3 space-y-3",
177
+ children: [/* @__PURE__ */ jsx(Secret, {
178
+ className: "max-w-full w-full",
179
+ secret: newKey.key,
180
+ status: "active"
181
+ }), /* @__PURE__ */ jsx(Button$1, {
182
+ variant: "outline",
183
+ onClick: () => setNewKey(null),
184
+ children: "Done"
185
+ })]
186
+ })
187
+ ] }) : null,
188
+ createKey.isError ? /* @__PURE__ */ jsx(Alert, {
189
+ variant: "destructive",
190
+ children: /* @__PURE__ */ jsx(AlertTitle, { children: createKey.error.message === API_KEY_CAP_REACHED ? plan.maxKeys == null ? "You cannot create more API keys right now." : `You already have the maximum of ${plan.maxKeys} active API keys. Revoke one to create another.` : createKey.error.message })
191
+ }) : null,
192
+ revokeKey.isError ? /* @__PURE__ */ jsx(Alert, {
193
+ variant: "destructive",
194
+ children: /* @__PURE__ */ jsx(AlertTitle, { children: revokeKey.error.message })
195
+ }) : null,
196
+ planActiveCount === 0 ? /* @__PURE__ */ jsx("p", {
197
+ className: "text-sm text-muted-foreground",
198
+ children: "You have no active API keys for this plan yet."
199
+ }) : null,
200
+ activeKeys.length > 0 ? /* @__PURE__ */ jsx("ul", {
201
+ className: "divide-y rounded-lg border",
202
+ children: activeKeys.map((key) => {
203
+ const isRevoking = pendingRevokeId === key.id;
204
+ return /* @__PURE__ */ jsxs("li", {
205
+ className: "flex flex-wrap items-center justify-between gap-3 p-4",
206
+ children: [/* @__PURE__ */ jsxs("div", {
207
+ className: "min-w-0 space-y-1",
208
+ children: [/* @__PURE__ */ jsx("p", {
209
+ className: "truncate font-medium",
210
+ children: key.name
211
+ }), /* @__PURE__ */ jsxs("p", {
212
+ className: "text-xs text-muted-foreground",
213
+ children: [
214
+ /* @__PURE__ */ jsxs("code", { children: [key.keyPrefix, "…"] }),
215
+ " · created",
216
+ " ",
217
+ new Date(key.createdAtUtc).toLocaleDateString()
218
+ ]
219
+ })]
220
+ }), readOnly ? null : /* @__PURE__ */ jsxs(Dialog, {
221
+ open: openRevokeId === key.id,
222
+ onOpenChange: (open) => {
223
+ if (isRevoking) return;
224
+ setOpenRevokeId(open ? key.id : null);
225
+ },
226
+ children: [/* @__PURE__ */ jsx(DialogTrigger, {
227
+ asChild: true,
228
+ children: /* @__PURE__ */ jsxs(Button$1, {
229
+ variant: "ghost",
230
+ className: "flex items-center gap-2",
231
+ disabled: isRevoking,
232
+ children: [isRevoking ? /* @__PURE__ */ jsx(Spinner, { size: 14 }) : /* @__PURE__ */ jsx(TrashIcon, { size: 16 }), /* @__PURE__ */ jsx("span", {
233
+ className: "hidden md:block",
234
+ children: isRevoking ? "Revoking…" : "Revoke"
235
+ })]
236
+ })
237
+ }), /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, { children: [
238
+ "Revoke “",
239
+ key.name,
240
+ "”?"
241
+ ] }), /* @__PURE__ */ jsx(DialogDescription, { children: "Any application using this key will immediately start getting 401 responses from the gateway. This cannot be undone." })] }), /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button$1, {
242
+ variant: "outline",
243
+ disabled: isRevoking,
244
+ onClick: () => setOpenRevokeId(null),
245
+ children: "Cancel"
246
+ }), /* @__PURE__ */ jsx(Button$1, {
247
+ variant: "destructive",
248
+ disabled: isRevoking,
249
+ onClick: () => handleRevoke(key.id),
250
+ children: isRevoking ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { size: 14 }), "Revoking…"] }) : "Revoke key"
251
+ })] })] })]
252
+ })]
253
+ }, key.id);
254
+ })
255
+ }) : null,
256
+ readOnly ? null : /* @__PURE__ */ jsxs("div", {
257
+ className: "flex flex-wrap items-center gap-2",
258
+ children: [/* @__PURE__ */ jsx(Input, {
259
+ className: "max-w-xs",
260
+ maxLength: 64,
261
+ placeholder: "Key name (e.g. production)",
262
+ "aria-label": `New API key name for ${plan.displayName}`,
263
+ value: name,
264
+ disabled: !canCreate || createKey.isPending,
265
+ onChange: (event) => setName(event.target.value),
266
+ onKeyDown: (event) => {
267
+ if (event.key === "Enter") handleCreate();
268
+ }
269
+ }), /* @__PURE__ */ jsx(Button$1, {
270
+ onClick: handleCreate,
271
+ disabled: !canCreate || createKey.isPending || !name.trim(),
272
+ className: "flex items-center gap-2",
273
+ children: createKey.isPending ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { size: 14 }), "Creating…"] }) : "Create API key"
274
+ })]
275
+ }),
276
+ plan && !plan.canCreate && !readOnly ? /* @__PURE__ */ jsx("p", {
277
+ className: "text-sm text-muted-foreground",
278
+ children: plan.maxKeys == null ? "API key creation is currently disabled for this API." : `You have reached this plan's limit of ${plan.maxKeys} API keys. Revoke a key to create a new one.`
279
+ }) : null
280
+ ]
281
+ });
282
+ };
283
+
284
+ //#endregion
285
+ export { useRevokeApiKey as a, useCreateApiKey as i, API_KEY_CAP_REACHED as n, useApiKeys as r, ApiKeysPanel as t };
@@ -0,0 +1,174 @@
1
+ import { o as PRICING_PATH, t as ACCOUNT_PLANS_PATH } from "./paths-ujy9wjVK.mjs";
2
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
3
+ import { t as FREE_CHECKOUT_MARKER } from "./billing-state-pnT0H1Lm.mjs";
4
+ import { ShieldIcon } from "@lukoweb/apitogo/icons";
5
+ import { Link, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
6
+ import { useEffect } from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { useQuery } from "@lukoweb/apitogo/react-query";
9
+ import { useAuth } from "@lukoweb/apitogo/hooks";
10
+ import { Alert, AlertAction, AlertDescription } from "@lukoweb/apitogo/ui/Alert";
11
+ import { Button } from "@lukoweb/apitogo/ui/Button";
12
+
13
+ //#region src/components/RedirectPage.tsx
14
+ const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
15
+ useEffect(() => {
16
+ if (url) window.location.assign(url);
17
+ }, [url]);
18
+ return /* @__PURE__ */ jsx("div", {
19
+ className: "flex min-h-screen items-center justify-center bg-muted",
20
+ children: /* @__PURE__ */ jsxs("div", {
21
+ className: "flex max-w-md flex-col items-center space-y-6 text-center",
22
+ children: [
23
+ /* @__PURE__ */ jsx("div", {
24
+ className: "relative",
25
+ children: /* @__PURE__ */ jsx("div", {
26
+ className: "flex h-24 w-24 items-center justify-center rounded-full bg-foreground/10",
27
+ children: /* @__PURE__ */ jsx(Icon, { className: "h-12 w-12 text-foreground" })
28
+ })
29
+ }),
30
+ children,
31
+ /* @__PURE__ */ jsxs("div", {
32
+ className: "space-y-2",
33
+ children: [/* @__PURE__ */ jsx("h2", {
34
+ className: "text-2xl font-bold text-card-foreground",
35
+ children: title
36
+ }), /* @__PURE__ */ jsx("p", {
37
+ className: "text-muted-foreground",
38
+ children: description
39
+ })]
40
+ }),
41
+ /* @__PURE__ */ jsxs("div", {
42
+ className: "flex space-x-2",
43
+ children: [
44
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.3s]" }),
45
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.15s]" }),
46
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary" })
47
+ ]
48
+ })
49
+ ]
50
+ })
51
+ });
52
+ };
53
+
54
+ //#endregion
55
+ //#region src/pages/CheckoutPage.tsx
56
+ const CheckoutPage = () => {
57
+ const [searchParams] = useSearchParams();
58
+ const planSku = searchParams.get("planSku") ?? "";
59
+ const navigate = useNavigate();
60
+ const auth = useAuth();
61
+ const apiBaseUrl = useDevPortalApiBaseUrl();
62
+ const isPreview = useDevPortalPreviewMode();
63
+ const isDemoSession = useIsDemoSession();
64
+ const plansPath = auth.isAuthenticated ? ACCOUNT_PLANS_PATH : PRICING_PATH;
65
+ const checkoutQuery = useQuery({
66
+ queryKey: [
67
+ "dev-portal-checkout",
68
+ apiBaseUrl,
69
+ planSku,
70
+ auth.profile?.sub
71
+ ],
72
+ enabled: Boolean(planSku) && !isPreview && !isDemoSession && auth.isAuthenticated && Boolean(apiBaseUrl),
73
+ retry: false,
74
+ queryFn: async () => {
75
+ const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/checkout`, {
76
+ method: "POST",
77
+ headers: {
78
+ Accept: "application/json",
79
+ "Content-Type": "application/json"
80
+ },
81
+ credentials: "include",
82
+ body: JSON.stringify({ planSku })
83
+ });
84
+ if (!response.ok) {
85
+ const detail = await response.text();
86
+ throw new Error(detail || "Checkout failed.");
87
+ }
88
+ return await response.json();
89
+ }
90
+ });
91
+ const freeProvisioned = checkoutQuery.data?.checkoutUrl == null && checkoutQuery.data?.entitlementProvisioned === true;
92
+ useEffect(() => {
93
+ if (freeProvisioned) navigate(`/account/subscriptions?checkout=success&${FREE_CHECKOUT_MARKER}`, { replace: true });
94
+ }, [freeProvisioned, navigate]);
95
+ if (!planSku) return /* @__PURE__ */ jsxs("div", {
96
+ className: "mx-auto max-w-lg px-4 py-16 text-center text-muted-foreground",
97
+ children: [
98
+ "Missing plan SKU.",
99
+ " ",
100
+ /* @__PURE__ */ jsx(Link, {
101
+ to: plansPath,
102
+ className: "text-primary underline",
103
+ children: "Back to plans"
104
+ })
105
+ ]
106
+ });
107
+ if (isDemoSession) return /* @__PURE__ */ jsxs("div", {
108
+ className: "mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center",
109
+ children: [
110
+ /* @__PURE__ */ jsx("h1", {
111
+ className: "text-2xl font-semibold tracking-tight",
112
+ children: "Demo checkout"
113
+ }),
114
+ /* @__PURE__ */ jsx("p", {
115
+ className: "text-muted-foreground",
116
+ children: "Checkout is simulated in local demo mode. Publish your portal to API ToGo to complete real subscriptions and billing."
117
+ }),
118
+ /* @__PURE__ */ jsx(Button, {
119
+ asChild: true,
120
+ variant: "outline",
121
+ children: /* @__PURE__ */ jsx(Link, {
122
+ to: "/account/subscriptions",
123
+ children: "View demo subscriptions"
124
+ })
125
+ })
126
+ ]
127
+ });
128
+ if (isPreview) return /* @__PURE__ */ jsxs("div", {
129
+ className: "mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center",
130
+ children: [
131
+ /* @__PURE__ */ jsx("h1", {
132
+ className: "text-2xl font-semibold tracking-tight",
133
+ children: "Checkout is temporarily unavailable"
134
+ }),
135
+ /* @__PURE__ */ jsx("p", {
136
+ className: "text-muted-foreground",
137
+ children: "We couldn’t reach the developer portal API. Try again shortly, or contact the site owner if this continues."
138
+ }),
139
+ /* @__PURE__ */ jsx(Button, {
140
+ asChild: true,
141
+ variant: "outline",
142
+ children: /* @__PURE__ */ jsx(Link, {
143
+ to: plansPath,
144
+ children: "Back to plans"
145
+ })
146
+ })
147
+ ]
148
+ });
149
+ if (freeProvisioned) return null;
150
+ return /* @__PURE__ */ jsx(RedirectPage, {
151
+ icon: ShieldIcon,
152
+ title: "Establishing secure checkout…",
153
+ description: "Redirecting you to complete your subscription.",
154
+ url: checkoutQuery.data?.checkoutUrl ?? void 0,
155
+ children: checkoutQuery.isError ? /* @__PURE__ */ jsxs(Alert, {
156
+ variant: "destructive",
157
+ children: [/* @__PURE__ */ jsx(AlertDescription, {
158
+ className: "first-letter:uppercase",
159
+ children: checkoutQuery.error.message
160
+ }), /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(Button, {
161
+ variant: "outline",
162
+ size: "xs",
163
+ asChild: true,
164
+ children: /* @__PURE__ */ jsx(Link, {
165
+ to: plansPath,
166
+ children: "Back to plans"
167
+ })
168
+ }) })]
169
+ }) : null
170
+ });
171
+ };
172
+
173
+ //#endregion
174
+ export { CheckoutPage as default };