@lukoweb/apitogo-plugin-dev-portal-billing 0.1.66 → 0.1.67
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 +145 -1
- package/dist/index.mjs +300 -27
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
2
|
import * as _lukoweb_apitogo_plugins0 from "@lukoweb/apitogo/plugins";
|
|
3
|
+
import * as _tanstack_react_query0 from "@tanstack/react-query";
|
|
3
4
|
|
|
4
5
|
//#region src/pages/AccountPlansPage.d.ts
|
|
5
6
|
declare const AccountPlansPage: () => react_jsx_runtime0.JSX.Element;
|
|
6
7
|
//#endregion
|
|
8
|
+
//#region src/components/ApiKeysPanel.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* Lists the developer's gateway API keys with create and revoke. The plaintext key is returned by the
|
|
11
|
+
* platform exactly once, so it is held in local state until dismissed and never re-fetched.
|
|
12
|
+
*/
|
|
13
|
+
declare const ApiKeysPanel: ({
|
|
14
|
+
readOnly
|
|
15
|
+
}: {
|
|
16
|
+
readOnly?: boolean;
|
|
17
|
+
}) => react_jsx_runtime0.JSX.Element;
|
|
18
|
+
//#endregion
|
|
7
19
|
//#region src/types.d.ts
|
|
20
|
+
type DevPortalApiKey = {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
keyPrefix: string;
|
|
24
|
+
status: string;
|
|
25
|
+
createdAtUtc: string;
|
|
26
|
+
revokedAtUtc: string | null; /** Present only in the response to a successful create; never returned again. */
|
|
27
|
+
key: string | null;
|
|
28
|
+
};
|
|
29
|
+
type DevPortalApiKeyListResponse = {
|
|
30
|
+
keys: DevPortalApiKey[];
|
|
31
|
+
activeCount: number; /** Per-developer cap from the project's platform plan; null means unlimited. */
|
|
32
|
+
maxKeys: number | null;
|
|
33
|
+
canCreate: boolean; /** Header the key must be sent in when calling the gateway. */
|
|
34
|
+
header: string;
|
|
35
|
+
gatewayBaseUrl: string | null;
|
|
36
|
+
};
|
|
8
37
|
type DevPortalBillingConfig = {
|
|
9
38
|
pricing?: {
|
|
10
39
|
title?: string;
|
|
@@ -30,4 +59,119 @@ declare const CREDENTIALS_PATH = "/account/credentials";
|
|
|
30
59
|
//#region src/DevPortalBillingPlugin.d.ts
|
|
31
60
|
declare const devPortalBillingPlugin: (options?: DevPortalBillingConfig | undefined) => _lukoweb_apitogo_plugins0.ApitogoPlugin;
|
|
32
61
|
//#endregion
|
|
33
|
-
|
|
62
|
+
//#region src/hooks/useApiKeys.d.ts
|
|
63
|
+
/** Thrown when the developer has hit their plan's per-developer key cap. */
|
|
64
|
+
declare const API_KEY_CAP_REACHED = "API_KEY_CAP_REACHED";
|
|
65
|
+
declare function useApiKeys(): _tanstack_react_query0.QueryObserverRefetchErrorResult<DevPortalApiKeyListResponse, Error> | _tanstack_react_query0.QueryObserverLoadingErrorResult<DevPortalApiKeyListResponse, Error> | _tanstack_react_query0.QueryObserverPendingResult<DevPortalApiKeyListResponse, Error> | {
|
|
66
|
+
data: DevPortalApiKeyListResponse;
|
|
67
|
+
error: null;
|
|
68
|
+
isError: boolean;
|
|
69
|
+
isFetching: boolean;
|
|
70
|
+
isLoading: boolean;
|
|
71
|
+
isPending: boolean;
|
|
72
|
+
isSuccess: boolean;
|
|
73
|
+
status: string;
|
|
74
|
+
isLoadingError: false;
|
|
75
|
+
isRefetchError: true;
|
|
76
|
+
isPlaceholderData: false;
|
|
77
|
+
dataUpdatedAt: number;
|
|
78
|
+
errorUpdatedAt: number;
|
|
79
|
+
failureCount: number;
|
|
80
|
+
failureReason: Error | null;
|
|
81
|
+
errorUpdateCount: number;
|
|
82
|
+
isFetched: boolean;
|
|
83
|
+
isFetchedAfterMount: boolean;
|
|
84
|
+
isInitialLoading: boolean;
|
|
85
|
+
isPaused: boolean;
|
|
86
|
+
isRefetching: boolean;
|
|
87
|
+
isStale: boolean;
|
|
88
|
+
isEnabled: boolean;
|
|
89
|
+
refetch: (options?: _tanstack_react_query0.RefetchOptions) => Promise<_tanstack_react_query0.QueryObserverResult<DevPortalApiKeyListResponse, Error>>;
|
|
90
|
+
fetchStatus: _tanstack_react_query0.FetchStatus;
|
|
91
|
+
promise: Promise<DevPortalApiKeyListResponse>;
|
|
92
|
+
} | {
|
|
93
|
+
data: DevPortalApiKeyListResponse;
|
|
94
|
+
error: null;
|
|
95
|
+
isError: boolean;
|
|
96
|
+
isFetching: boolean;
|
|
97
|
+
isLoading: boolean;
|
|
98
|
+
isPending: boolean;
|
|
99
|
+
isSuccess: boolean;
|
|
100
|
+
status: string;
|
|
101
|
+
isLoadingError: false;
|
|
102
|
+
isRefetchError: false;
|
|
103
|
+
isPlaceholderData: false;
|
|
104
|
+
dataUpdatedAt: number;
|
|
105
|
+
errorUpdatedAt: number;
|
|
106
|
+
failureCount: number;
|
|
107
|
+
failureReason: Error | null;
|
|
108
|
+
errorUpdateCount: number;
|
|
109
|
+
isFetched: boolean;
|
|
110
|
+
isFetchedAfterMount: boolean;
|
|
111
|
+
isInitialLoading: boolean;
|
|
112
|
+
isPaused: boolean;
|
|
113
|
+
isRefetching: boolean;
|
|
114
|
+
isStale: boolean;
|
|
115
|
+
isEnabled: boolean;
|
|
116
|
+
refetch: (options?: _tanstack_react_query0.RefetchOptions) => Promise<_tanstack_react_query0.QueryObserverResult<DevPortalApiKeyListResponse, Error>>;
|
|
117
|
+
fetchStatus: _tanstack_react_query0.FetchStatus;
|
|
118
|
+
promise: Promise<DevPortalApiKeyListResponse>;
|
|
119
|
+
} | {
|
|
120
|
+
data: DevPortalApiKeyListResponse;
|
|
121
|
+
error: null;
|
|
122
|
+
isError: boolean;
|
|
123
|
+
isFetching: boolean;
|
|
124
|
+
isLoading: boolean;
|
|
125
|
+
isPending: boolean;
|
|
126
|
+
isSuccess: boolean;
|
|
127
|
+
status: string;
|
|
128
|
+
isLoadingError: true;
|
|
129
|
+
isRefetchError: false;
|
|
130
|
+
isPlaceholderData: false;
|
|
131
|
+
dataUpdatedAt: number;
|
|
132
|
+
errorUpdatedAt: number;
|
|
133
|
+
failureCount: number;
|
|
134
|
+
failureReason: Error | null;
|
|
135
|
+
errorUpdateCount: number;
|
|
136
|
+
isFetched: boolean;
|
|
137
|
+
isFetchedAfterMount: boolean;
|
|
138
|
+
isInitialLoading: boolean;
|
|
139
|
+
isPaused: boolean;
|
|
140
|
+
isRefetching: boolean;
|
|
141
|
+
isStale: boolean;
|
|
142
|
+
isEnabled: boolean;
|
|
143
|
+
refetch: (options?: _tanstack_react_query0.RefetchOptions) => Promise<_tanstack_react_query0.QueryObserverResult<DevPortalApiKeyListResponse, Error>>;
|
|
144
|
+
fetchStatus: _tanstack_react_query0.FetchStatus;
|
|
145
|
+
promise: Promise<DevPortalApiKeyListResponse>;
|
|
146
|
+
} | {
|
|
147
|
+
data: DevPortalApiKeyListResponse;
|
|
148
|
+
error: null;
|
|
149
|
+
isError: boolean;
|
|
150
|
+
isFetching: boolean;
|
|
151
|
+
isLoading: boolean;
|
|
152
|
+
isPending: boolean;
|
|
153
|
+
isSuccess: boolean;
|
|
154
|
+
status: string;
|
|
155
|
+
isLoadingError: false;
|
|
156
|
+
isRefetchError: false;
|
|
157
|
+
isPlaceholderData: true;
|
|
158
|
+
dataUpdatedAt: number;
|
|
159
|
+
errorUpdatedAt: number;
|
|
160
|
+
failureCount: number;
|
|
161
|
+
failureReason: Error | null;
|
|
162
|
+
errorUpdateCount: number;
|
|
163
|
+
isFetched: boolean;
|
|
164
|
+
isFetchedAfterMount: boolean;
|
|
165
|
+
isInitialLoading: boolean;
|
|
166
|
+
isPaused: boolean;
|
|
167
|
+
isRefetching: boolean;
|
|
168
|
+
isStale: boolean;
|
|
169
|
+
isEnabled: boolean;
|
|
170
|
+
refetch: (options?: _tanstack_react_query0.RefetchOptions) => Promise<_tanstack_react_query0.QueryObserverResult<DevPortalApiKeyListResponse, Error>>;
|
|
171
|
+
fetchStatus: _tanstack_react_query0.FetchStatus;
|
|
172
|
+
promise: Promise<DevPortalApiKeyListResponse>;
|
|
173
|
+
};
|
|
174
|
+
declare function useCreateApiKey(): _tanstack_react_query0.UseMutationResult<DevPortalApiKey, Error, string, unknown>;
|
|
175
|
+
declare function useRevokeApiKey(): _tanstack_react_query0.UseMutationResult<void, Error, string, unknown>;
|
|
176
|
+
//#endregion
|
|
177
|
+
export { API_KEY_CAP_REACHED, AccountPlansPage, ApiKeysPanel, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin, useApiKeys, useCreateApiKey, useRevokeApiKey };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Button, Head, Heading } from "@lukoweb/apitogo/components";
|
|
2
|
-
import { CreditCardIcon, KeyRoundIcon, ShieldIcon, StarsIcon } from "@lukoweb/apitogo/icons";
|
|
2
|
+
import { CreditCardIcon, KeyRoundIcon, ShieldIcon, StarsIcon, TrashIcon } from "@lukoweb/apitogo/icons";
|
|
3
3
|
import { createContext, useContext, useEffect, useState } from "react";
|
|
4
4
|
import { Link, 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";
|
|
@@ -9,10 +9,14 @@ 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 {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { Alert, AlertTitle } from "@lukoweb/apitogo/ui/Alert.js";
|
|
13
|
+
import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
|
|
14
|
+
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@lukoweb/apitogo/ui/Dialog.js";
|
|
15
|
+
import { Input } from "@lukoweb/apitogo/ui/Input.js";
|
|
15
16
|
import { Secret } from "@lukoweb/apitogo/ui/Secret.js";
|
|
17
|
+
import { createPlugin } from "@lukoweb/apitogo/plugins";
|
|
18
|
+
import { Alert as Alert$1, AlertAction, AlertDescription } from "@lukoweb/apitogo/ui/Alert";
|
|
19
|
+
import { Button as Button$2 } from "@lukoweb/apitogo/ui/Button";
|
|
16
20
|
import { Value } from "@lukoweb/apitogo/ui/Value.js";
|
|
17
21
|
|
|
18
22
|
//#region src/utils/dev-portal-api.ts
|
|
@@ -286,6 +290,8 @@ function useCancelSubscription() {
|
|
|
286
290
|
//#endregion
|
|
287
291
|
//#region src/utils/demo-session.ts
|
|
288
292
|
const DEMO_SUBSCRIPTION_KEY = "demo-subscription-key";
|
|
293
|
+
const DEMO_GATEWAY_KEY_HEADER = "Ocp-Apim-Subscription-Key";
|
|
294
|
+
const DEMO_MAX_KEYS = 5;
|
|
289
295
|
function resolveDemoPlanSku(plans) {
|
|
290
296
|
return plans?.[0]?.sku ?? "free";
|
|
291
297
|
}
|
|
@@ -314,6 +320,24 @@ function createDemoCredentials(plans) {
|
|
|
314
320
|
expiresAt: null
|
|
315
321
|
};
|
|
316
322
|
}
|
|
323
|
+
function createDemoApiKeys() {
|
|
324
|
+
return {
|
|
325
|
+
keys: [{
|
|
326
|
+
id: "00000000-0000-0000-0000-0000000000a1",
|
|
327
|
+
name: "default",
|
|
328
|
+
keyPrefix: DEMO_SUBSCRIPTION_KEY.slice(0, 8),
|
|
329
|
+
status: "active",
|
|
330
|
+
createdAtUtc: (/* @__PURE__ */ new Date()).toISOString(),
|
|
331
|
+
revokedAtUtc: null,
|
|
332
|
+
key: null
|
|
333
|
+
}],
|
|
334
|
+
activeCount: 1,
|
|
335
|
+
maxKeys: DEMO_MAX_KEYS,
|
|
336
|
+
canCreate: true,
|
|
337
|
+
header: DEMO_GATEWAY_KEY_HEADER,
|
|
338
|
+
gatewayBaseUrl: null
|
|
339
|
+
};
|
|
340
|
+
}
|
|
317
341
|
|
|
318
342
|
//#endregion
|
|
319
343
|
//#region src/hooks/useMySubscriptions.ts
|
|
@@ -467,6 +491,259 @@ const AccountPlansPage = () => {
|
|
|
467
491
|
});
|
|
468
492
|
};
|
|
469
493
|
|
|
494
|
+
//#endregion
|
|
495
|
+
//#region src/hooks/useApiKeys.ts
|
|
496
|
+
/** Thrown when the developer has hit their plan's per-developer key cap. */
|
|
497
|
+
const API_KEY_CAP_REACHED = "API_KEY_CAP_REACHED";
|
|
498
|
+
function apiKeysQueryKey(apiBaseUrl) {
|
|
499
|
+
return ["dev-portal-api-keys", apiBaseUrl];
|
|
500
|
+
}
|
|
501
|
+
function useApiKeys() {
|
|
502
|
+
const auth = useAuth();
|
|
503
|
+
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
504
|
+
const isPreview = useDevPortalPreviewMode();
|
|
505
|
+
const isDemoSession = useIsDemoSession();
|
|
506
|
+
const liveQuery = useQuery({
|
|
507
|
+
queryKey: apiKeysQueryKey(apiBaseUrl),
|
|
508
|
+
enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
|
|
509
|
+
queryFn: async () => {
|
|
510
|
+
const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
|
|
511
|
+
method: "GET",
|
|
512
|
+
headers: { Accept: "application/json" },
|
|
513
|
+
credentials: "include"
|
|
514
|
+
});
|
|
515
|
+
if (response.status === 404) throw new Error("NO_ENTITLEMENT");
|
|
516
|
+
if (!response.ok) throw new Error("Failed to load API keys.");
|
|
517
|
+
return await response.json();
|
|
518
|
+
},
|
|
519
|
+
retry: (failureCount, error) => error instanceof Error && error.message === "NO_ENTITLEMENT" ? false : failureCount < 2
|
|
520
|
+
});
|
|
521
|
+
if (isDemoSession && auth.isAuthenticated) return {
|
|
522
|
+
...liveQuery,
|
|
523
|
+
data: createDemoApiKeys(),
|
|
524
|
+
error: null,
|
|
525
|
+
isError: false,
|
|
526
|
+
isFetching: false,
|
|
527
|
+
isLoading: false,
|
|
528
|
+
isPending: false,
|
|
529
|
+
isSuccess: true,
|
|
530
|
+
status: "success"
|
|
531
|
+
};
|
|
532
|
+
return liveQuery;
|
|
533
|
+
}
|
|
534
|
+
function useCreateApiKey() {
|
|
535
|
+
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
536
|
+
const queryClient = useQueryClient();
|
|
537
|
+
return useMutation({
|
|
538
|
+
mutationFn: async (name) => {
|
|
539
|
+
if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
|
|
540
|
+
const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
|
|
541
|
+
method: "POST",
|
|
542
|
+
headers: {
|
|
543
|
+
Accept: "application/json",
|
|
544
|
+
"Content-Type": "application/json"
|
|
545
|
+
},
|
|
546
|
+
credentials: "include",
|
|
547
|
+
body: JSON.stringify({ name })
|
|
548
|
+
});
|
|
549
|
+
if (response.status === 409) throw new Error(API_KEY_CAP_REACHED);
|
|
550
|
+
if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Failed to create API key.");
|
|
551
|
+
return await response.json();
|
|
552
|
+
},
|
|
553
|
+
onSuccess: async () => {
|
|
554
|
+
await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
function useRevokeApiKey() {
|
|
559
|
+
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
560
|
+
const queryClient = useQueryClient();
|
|
561
|
+
return useMutation({
|
|
562
|
+
mutationFn: async (keyId) => {
|
|
563
|
+
if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
|
|
564
|
+
const response = await fetch(`${apiBaseUrl}/api/v1/api-keys/${keyId}`, {
|
|
565
|
+
method: "DELETE",
|
|
566
|
+
headers: { Accept: "application/json" },
|
|
567
|
+
credentials: "include"
|
|
568
|
+
});
|
|
569
|
+
if (!response.ok && response.status !== 204) throw new Error(await readProblemDetail(response) ?? "Failed to revoke API key.");
|
|
570
|
+
},
|
|
571
|
+
onSuccess: async () => {
|
|
572
|
+
await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
async function readProblemDetail(response) {
|
|
577
|
+
try {
|
|
578
|
+
return (await response.json()).detail?.trim() || null;
|
|
579
|
+
} catch {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
//#endregion
|
|
585
|
+
//#region src/components/ApiKeysPanel.tsx
|
|
586
|
+
/**
|
|
587
|
+
* Lists the developer's gateway API keys with create and revoke. The plaintext key is returned by the
|
|
588
|
+
* platform exactly once, so it is held in local state until dismissed and never re-fetched.
|
|
589
|
+
*/
|
|
590
|
+
const ApiKeysPanel = ({ readOnly = false }) => {
|
|
591
|
+
const { data, isLoading, error } = useApiKeys();
|
|
592
|
+
const createKey = useCreateApiKey();
|
|
593
|
+
const revokeKey = useRevokeApiKey();
|
|
594
|
+
const [name, setName] = useState("");
|
|
595
|
+
const [newKey, setNewKey] = useState(null);
|
|
596
|
+
const activeKeys = data?.keys.filter((key) => key.status === "active") ?? [];
|
|
597
|
+
const capLabel = data?.maxKeys == null ? `${data?.activeCount ?? activeKeys.length} active` : `${data.activeCount} of ${data.maxKeys} used`;
|
|
598
|
+
const canCreate = !readOnly && (data?.canCreate ?? false);
|
|
599
|
+
const handleCreate = () => {
|
|
600
|
+
const trimmed = name.trim();
|
|
601
|
+
if (!trimmed) return;
|
|
602
|
+
createKey.mutate(trimmed, { onSuccess: (key) => {
|
|
603
|
+
setNewKey(key);
|
|
604
|
+
setName("");
|
|
605
|
+
} });
|
|
606
|
+
};
|
|
607
|
+
return /* @__PURE__ */ jsxs("article", {
|
|
608
|
+
className: "rounded-xl border bg-card/60 p-6 space-y-5",
|
|
609
|
+
children: [
|
|
610
|
+
/* @__PURE__ */ jsxs("div", {
|
|
611
|
+
className: "flex flex-wrap items-start justify-between gap-3",
|
|
612
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
613
|
+
className: "space-y-1",
|
|
614
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
615
|
+
className: "text-lg font-medium",
|
|
616
|
+
children: "API keys"
|
|
617
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
618
|
+
className: "text-sm text-muted-foreground",
|
|
619
|
+
children: [
|
|
620
|
+
"Send a key in the",
|
|
621
|
+
" ",
|
|
622
|
+
/* @__PURE__ */ jsx("code", {
|
|
623
|
+
className: "rounded bg-muted px-1 py-0.5 text-xs",
|
|
624
|
+
children: data?.header ?? "Ocp-Apim-Subscription-Key"
|
|
625
|
+
}),
|
|
626
|
+
" ",
|
|
627
|
+
"header when calling the gateway. Keys share your plan's rate limit and quota."
|
|
628
|
+
]
|
|
629
|
+
})]
|
|
630
|
+
}), data ? /* @__PURE__ */ jsx("span", {
|
|
631
|
+
className: "shrink-0 rounded-full border px-3 py-1 text-xs text-muted-foreground",
|
|
632
|
+
children: capLabel
|
|
633
|
+
}) : null]
|
|
634
|
+
}),
|
|
635
|
+
newKey?.key ? /* @__PURE__ */ jsxs(Alert, { children: [
|
|
636
|
+
/* @__PURE__ */ jsx(KeyRoundIcon, { size: 16 }),
|
|
637
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: "Copy your new key now — it is shown only once." }),
|
|
638
|
+
/* @__PURE__ */ jsxs("div", {
|
|
639
|
+
className: "mt-3 space-y-3",
|
|
640
|
+
children: [/* @__PURE__ */ jsx(Secret, {
|
|
641
|
+
className: "max-w-full w-full",
|
|
642
|
+
secret: newKey.key,
|
|
643
|
+
status: "active"
|
|
644
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
645
|
+
variant: "outline",
|
|
646
|
+
onClick: () => setNewKey(null),
|
|
647
|
+
children: "Done"
|
|
648
|
+
})]
|
|
649
|
+
})
|
|
650
|
+
] }) : null,
|
|
651
|
+
createKey.isError ? /* @__PURE__ */ jsx(Alert, {
|
|
652
|
+
variant: "destructive",
|
|
653
|
+
children: /* @__PURE__ */ jsx(AlertTitle, { children: createKey.error.message === API_KEY_CAP_REACHED ? data?.maxKeys == null ? "You cannot create more API keys right now." : `You already have the maximum of ${data.maxKeys} active API keys. Revoke one to create another.` : createKey.error.message })
|
|
654
|
+
}) : null,
|
|
655
|
+
revokeKey.isError ? /* @__PURE__ */ jsx(Alert, {
|
|
656
|
+
variant: "destructive",
|
|
657
|
+
children: /* @__PURE__ */ jsx(AlertTitle, { children: revokeKey.error.message })
|
|
658
|
+
}) : null,
|
|
659
|
+
isLoading ? /* @__PURE__ */ jsx("p", {
|
|
660
|
+
className: "text-sm text-muted-foreground",
|
|
661
|
+
children: "Loading…"
|
|
662
|
+
}) : null,
|
|
663
|
+
error ? /* @__PURE__ */ jsx("p", {
|
|
664
|
+
className: "text-sm text-muted-foreground",
|
|
665
|
+
children: error instanceof Error && error.message === "NO_ENTITLEMENT" ? "Choose a plan to get your first API key." : "Unable to load your API keys. Try again in a moment."
|
|
666
|
+
}) : null,
|
|
667
|
+
data && activeKeys.length === 0 && !isLoading ? /* @__PURE__ */ jsx("p", {
|
|
668
|
+
className: "text-sm text-muted-foreground",
|
|
669
|
+
children: "You have no active API keys yet."
|
|
670
|
+
}) : null,
|
|
671
|
+
activeKeys.length > 0 ? /* @__PURE__ */ jsx("ul", {
|
|
672
|
+
className: "divide-y rounded-lg border",
|
|
673
|
+
children: activeKeys.map((key) => /* @__PURE__ */ jsxs("li", {
|
|
674
|
+
className: "flex flex-wrap items-center justify-between gap-3 p-4",
|
|
675
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
676
|
+
className: "min-w-0 space-y-1",
|
|
677
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
678
|
+
className: "truncate font-medium",
|
|
679
|
+
children: key.name
|
|
680
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
681
|
+
className: "text-xs text-muted-foreground",
|
|
682
|
+
children: [
|
|
683
|
+
/* @__PURE__ */ jsxs("code", { children: [key.keyPrefix, "…"] }),
|
|
684
|
+
" · created",
|
|
685
|
+
" ",
|
|
686
|
+
new Date(key.createdAtUtc).toLocaleDateString()
|
|
687
|
+
]
|
|
688
|
+
})]
|
|
689
|
+
}), readOnly ? null : /* @__PURE__ */ jsxs(Dialog, { children: [/* @__PURE__ */ jsx(DialogTrigger, {
|
|
690
|
+
asChild: true,
|
|
691
|
+
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
692
|
+
variant: "ghost",
|
|
693
|
+
className: "flex items-center gap-2",
|
|
694
|
+
disabled: revokeKey.isPending,
|
|
695
|
+
children: [/* @__PURE__ */ jsx(TrashIcon, { size: 16 }), /* @__PURE__ */ jsx("span", {
|
|
696
|
+
className: "hidden md:block",
|
|
697
|
+
children: "Revoke"
|
|
698
|
+
})]
|
|
699
|
+
})
|
|
700
|
+
}), /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, { children: [
|
|
701
|
+
"Revoke “",
|
|
702
|
+
key.name,
|
|
703
|
+
"”?"
|
|
704
|
+
] }), /* @__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(DialogClose, {
|
|
705
|
+
asChild: true,
|
|
706
|
+
children: /* @__PURE__ */ jsx(Button$1, {
|
|
707
|
+
variant: "outline",
|
|
708
|
+
children: "Cancel"
|
|
709
|
+
})
|
|
710
|
+
}), /* @__PURE__ */ jsx(DialogClose, {
|
|
711
|
+
asChild: true,
|
|
712
|
+
children: /* @__PURE__ */ jsx(Button$1, {
|
|
713
|
+
variant: "destructive",
|
|
714
|
+
onClick: () => revokeKey.mutate(key.id),
|
|
715
|
+
children: "Revoke key"
|
|
716
|
+
})
|
|
717
|
+
})] })] })] })]
|
|
718
|
+
}, key.id))
|
|
719
|
+
}) : null,
|
|
720
|
+
readOnly ? null : /* @__PURE__ */ jsxs("div", {
|
|
721
|
+
className: "flex flex-wrap items-center gap-2",
|
|
722
|
+
children: [/* @__PURE__ */ jsx(Input, {
|
|
723
|
+
className: "max-w-xs",
|
|
724
|
+
maxLength: 64,
|
|
725
|
+
placeholder: "Key name (e.g. production)",
|
|
726
|
+
"aria-label": "New API key name",
|
|
727
|
+
value: name,
|
|
728
|
+
disabled: !canCreate || createKey.isPending,
|
|
729
|
+
onChange: (event) => setName(event.target.value),
|
|
730
|
+
onKeyDown: (event) => {
|
|
731
|
+
if (event.key === "Enter") handleCreate();
|
|
732
|
+
}
|
|
733
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
734
|
+
onClick: handleCreate,
|
|
735
|
+
disabled: !canCreate || createKey.isPending || !name.trim(),
|
|
736
|
+
children: createKey.isPending ? "Creating…" : "Create API key"
|
|
737
|
+
})]
|
|
738
|
+
}),
|
|
739
|
+
data && !data.canCreate && !readOnly ? /* @__PURE__ */ jsx("p", {
|
|
740
|
+
className: "text-sm text-muted-foreground",
|
|
741
|
+
children: data.maxKeys == null ? "API key creation is currently disabled for this API." : `You have reached your plan limit of ${data.maxKeys} API keys. Revoke a key to create a new one.`
|
|
742
|
+
}) : null
|
|
743
|
+
]
|
|
744
|
+
});
|
|
745
|
+
};
|
|
746
|
+
|
|
470
747
|
//#endregion
|
|
471
748
|
//#region src/DevPortalBillingWrapper.tsx
|
|
472
749
|
const DevPortalBillingWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(DevPortalBillingProvider, {
|
|
@@ -589,7 +866,7 @@ const CheckoutPage = () => {
|
|
|
589
866
|
className: "text-muted-foreground",
|
|
590
867
|
children: "Checkout is simulated in local demo mode. Publish your portal to APItoGo to complete real subscriptions and billing."
|
|
591
868
|
}),
|
|
592
|
-
/* @__PURE__ */ jsx(Button$
|
|
869
|
+
/* @__PURE__ */ jsx(Button$2, {
|
|
593
870
|
asChild: true,
|
|
594
871
|
variant: "outline",
|
|
595
872
|
children: /* @__PURE__ */ jsx(Link, {
|
|
@@ -610,7 +887,7 @@ const CheckoutPage = () => {
|
|
|
610
887
|
className: "text-muted-foreground",
|
|
611
888
|
children: "Subscribe and billing flows require a live dev-portal backend. Publish your portal to APItoGo, then return here to complete checkout."
|
|
612
889
|
}),
|
|
613
|
-
/* @__PURE__ */ jsx(Button$
|
|
890
|
+
/* @__PURE__ */ jsx(Button$2, {
|
|
614
891
|
asChild: true,
|
|
615
892
|
variant: "outline",
|
|
616
893
|
children: /* @__PURE__ */ jsx(Link, {
|
|
@@ -626,12 +903,12 @@ const CheckoutPage = () => {
|
|
|
626
903
|
title: "Establishing secure checkout…",
|
|
627
904
|
description: "Redirecting you to complete your subscription.",
|
|
628
905
|
url: checkoutQuery.data?.checkoutUrl ?? void 0,
|
|
629
|
-
children: checkoutQuery.isError ? /* @__PURE__ */ jsxs(Alert, {
|
|
906
|
+
children: checkoutQuery.isError ? /* @__PURE__ */ jsxs(Alert$1, {
|
|
630
907
|
variant: "destructive",
|
|
631
908
|
children: [/* @__PURE__ */ jsx(AlertDescription, {
|
|
632
909
|
className: "first-letter:uppercase",
|
|
633
910
|
children: checkoutQuery.error.message
|
|
634
|
-
}), /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(Button$
|
|
911
|
+
}), /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(Button$2, {
|
|
635
912
|
variant: "outline",
|
|
636
913
|
size: "xs",
|
|
637
914
|
asChild: true,
|
|
@@ -707,7 +984,7 @@ const CredentialsPage = () => {
|
|
|
707
984
|
children: "API credentials"
|
|
708
985
|
}), /* @__PURE__ */ jsx("p", {
|
|
709
986
|
className: "text-muted-foreground",
|
|
710
|
-
children: "
|
|
987
|
+
children: "Create API keys and use them to authenticate requests to the API gateway."
|
|
711
988
|
})]
|
|
712
989
|
})]
|
|
713
990
|
}),
|
|
@@ -727,7 +1004,7 @@ const CredentialsPage = () => {
|
|
|
727
1004
|
children: [/* @__PURE__ */ jsx("p", {
|
|
728
1005
|
className: "text-muted-foreground",
|
|
729
1006
|
children: "Sign in to view your subscription key."
|
|
730
|
-
}), /* @__PURE__ */ jsx(Button$
|
|
1007
|
+
}), /* @__PURE__ */ jsx(Button$2, {
|
|
731
1008
|
className: "mt-4",
|
|
732
1009
|
asChild: true,
|
|
733
1010
|
children: /* @__PURE__ */ jsx(Link, {
|
|
@@ -745,7 +1022,7 @@ const CredentialsPage = () => {
|
|
|
745
1022
|
children: [/* @__PURE__ */ jsx("p", {
|
|
746
1023
|
className: "text-muted-foreground",
|
|
747
1024
|
children: error instanceof Error && error.message === "NO_ENTITLEMENT" ? "No active subscription found. Choose a plan to get your API key." : "Unable to load API credentials. Try again in a moment."
|
|
748
|
-
}), /* @__PURE__ */ jsx(Button$
|
|
1025
|
+
}), /* @__PURE__ */ jsx(Button$2, {
|
|
749
1026
|
className: "mt-4",
|
|
750
1027
|
asChild: true,
|
|
751
1028
|
children: /* @__PURE__ */ jsx(Link, {
|
|
@@ -770,24 +1047,20 @@ const CredentialsPage = () => {
|
|
|
770
1047
|
})]
|
|
771
1048
|
})
|
|
772
1049
|
}),
|
|
1050
|
+
/* @__PURE__ */ jsx(ApiKeysPanel, { readOnly: isDemoSession }),
|
|
773
1051
|
/* @__PURE__ */ jsxs("article", {
|
|
774
1052
|
className: "rounded-xl border bg-card/60 p-6 space-y-4",
|
|
775
1053
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
776
1054
|
className: "space-y-1",
|
|
777
1055
|
children: [/* @__PURE__ */ jsx("h2", {
|
|
778
1056
|
className: "text-lg font-medium",
|
|
779
|
-
children: "
|
|
1057
|
+
children: "Default key value"
|
|
780
1058
|
}), /* @__PURE__ */ jsxs("p", {
|
|
781
1059
|
className: "text-sm text-muted-foreground",
|
|
782
1060
|
children: [
|
|
783
|
-
"
|
|
784
|
-
" ",
|
|
785
|
-
|
|
786
|
-
className: "rounded bg-muted px-1 py-0.5 text-xs",
|
|
787
|
-
children: "Ocp-Apim-Subscription-Key"
|
|
788
|
-
}),
|
|
789
|
-
" ",
|
|
790
|
-
"header when calling the gateway."
|
|
1061
|
+
"The full value of the ",
|
|
1062
|
+
/* @__PURE__ */ jsx("code", { children: "default" }),
|
|
1063
|
+
" key issued with your plan. Keys you create above are shown only once, so store them yourself."
|
|
791
1064
|
]
|
|
792
1065
|
})]
|
|
793
1066
|
}), /* @__PURE__ */ jsx(Secret, {
|
|
@@ -821,14 +1094,14 @@ const CredentialsPage = () => {
|
|
|
821
1094
|
}) : null,
|
|
822
1095
|
/* @__PURE__ */ jsxs("div", {
|
|
823
1096
|
className: "mt-8 flex flex-wrap gap-3",
|
|
824
|
-
children: [/* @__PURE__ */ jsx(Button$
|
|
1097
|
+
children: [/* @__PURE__ */ jsx(Button$2, {
|
|
825
1098
|
variant: "outline",
|
|
826
1099
|
asChild: true,
|
|
827
1100
|
children: /* @__PURE__ */ jsx(Link, {
|
|
828
1101
|
to: "/account/subscriptions",
|
|
829
1102
|
children: "My subscriptions"
|
|
830
1103
|
})
|
|
831
|
-
}), /* @__PURE__ */ jsx(Button$
|
|
1104
|
+
}), /* @__PURE__ */ jsx(Button$2, {
|
|
832
1105
|
variant: "outline",
|
|
833
1106
|
asChild: true,
|
|
834
1107
|
children: /* @__PURE__ */ jsx(Link, {
|
|
@@ -975,7 +1248,7 @@ const SubscriptionsPage = () => {
|
|
|
975
1248
|
children: [/* @__PURE__ */ jsx("p", {
|
|
976
1249
|
className: "text-muted-foreground",
|
|
977
1250
|
children: "You do not have an active subscription yet."
|
|
978
|
-
}), /* @__PURE__ */ jsx(Button$
|
|
1251
|
+
}), /* @__PURE__ */ jsx(Button$2, {
|
|
979
1252
|
className: "mt-4",
|
|
980
1253
|
asChild: true,
|
|
981
1254
|
children: /* @__PURE__ */ jsx(Link, {
|
|
@@ -1021,7 +1294,7 @@ const SubscriptionsPage = () => {
|
|
|
1021
1294
|
/* @__PURE__ */ jsxs("div", {
|
|
1022
1295
|
className: "mt-8 flex flex-wrap gap-3",
|
|
1023
1296
|
children: [
|
|
1024
|
-
/* @__PURE__ */ jsx(Button$
|
|
1297
|
+
/* @__PURE__ */ jsx(Button$2, {
|
|
1025
1298
|
variant: "outline",
|
|
1026
1299
|
asChild: true,
|
|
1027
1300
|
children: /* @__PURE__ */ jsx(Link, {
|
|
@@ -1029,7 +1302,7 @@ const SubscriptionsPage = () => {
|
|
|
1029
1302
|
children: "Change plan"
|
|
1030
1303
|
})
|
|
1031
1304
|
}),
|
|
1032
|
-
/* @__PURE__ */ jsx(Button$
|
|
1305
|
+
/* @__PURE__ */ jsx(Button$2, {
|
|
1033
1306
|
variant: "outline",
|
|
1034
1307
|
asChild: true,
|
|
1035
1308
|
children: /* @__PURE__ */ jsx(Link, {
|
|
@@ -1037,7 +1310,7 @@ const SubscriptionsPage = () => {
|
|
|
1037
1310
|
children: "Billing settings"
|
|
1038
1311
|
})
|
|
1039
1312
|
}),
|
|
1040
|
-
auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$
|
|
1313
|
+
auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$2, {
|
|
1041
1314
|
variant: "secondary",
|
|
1042
1315
|
asChild: true,
|
|
1043
1316
|
children: /* @__PURE__ */ jsx(Link, {
|
|
@@ -1185,4 +1458,4 @@ const devPortalBillingPlugin = createPlugin((options = {}) => ({
|
|
|
1185
1458
|
}));
|
|
1186
1459
|
|
|
1187
1460
|
//#endregion
|
|
1188
|
-
export { AccountPlansPage, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
|
|
1461
|
+
export { API_KEY_CAP_REACHED, AccountPlansPage, ApiKeysPanel, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_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.67",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/zuplo/zudoku",
|
|
@@ -46,7 +46,7 @@
|
|
|
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.67"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": ">=19.2.0",
|