@lukoweb/apitogo-plugin-dev-portal-billing 0.2.31 → 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 -2402
- 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,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 };
|