@lukoweb/apitogo-plugin-dev-portal-billing 0.2.51 → 0.2.52

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.
@@ -1,8 +1,8 @@
1
- import { c as isActiveSubscription, n as useDevPortalPreviewMode, p as parseManifestPlans, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-Dk00H8xy.mjs";
2
- import { t as PricingCard } from "./PricingCard-D-owYlf3.mjs";
1
+ import { f as parseManifestPlans, n as useDevPortalPreviewMode, r as useIsDemoSession, s as isActiveSubscription, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
+ import { t as PricingCard } from "./PricingCard-CKdq1oWn.mjs";
3
3
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
- import { t as useMySubscriptions } from "./useMySubscriptions-B4iV7zFs.mjs";
5
- import { t as usePlans } from "./usePlans-DVGt2Myv.mjs";
4
+ import { t as useMySubscriptions } from "./useMySubscriptions-BtxAXA1m.mjs";
5
+ import { t as usePlans } from "./usePlans-DPGFWcVo.mjs";
6
6
  import { Head } from "@lukoweb/apitogo/components";
7
7
  import { CreditCardIcon } from "@lukoweb/apitogo/icons";
8
8
  import { useSearchParams } from "@lukoweb/apitogo/router";
@@ -1,6 +1,6 @@
1
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-Dk00H8xy.mjs";
3
- import { t as FREE_CHECKOUT_MARKER } from "./billing-state-DjUXQYSr.mjs";
2
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
3
+ import { t as FREE_CHECKOUT_MARKER } from "./billing-state-DbU5Lpoz.mjs";
4
4
  import { ShieldIcon } from "@lukoweb/apitogo/icons";
5
5
  import { Link, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
6
6
  import { useEffect } from "react";
@@ -1,7 +1,7 @@
1
1
  import { o as PRICING_PATH } from "./paths-ujy9wjVK.mjs";
2
- import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl, u as isPlanCustomPriced } from "./useDevPortalApiBaseUrl-Dk00H8xy.mjs";
2
+ import { l as isPlanCustomPriced, n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
3
3
  import "./DevPortalBillingContext-CR83s4LO.mjs";
4
- import { t as usePlans } from "./usePlans-DVGt2Myv.mjs";
4
+ import { t as usePlans } from "./usePlans-DPGFWcVo.mjs";
5
5
  import { Link, useSearchParams } from "@lukoweb/apitogo/router";
6
6
  import { useState } from "react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -1,9 +1,10 @@
1
1
  import { t as ACCOUNT_PLANS_PATH } from "./paths-ujy9wjVK.mjs";
2
- import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-Dk00H8xy.mjs";
2
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
3
3
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
4
  import { r as createDemoCredentials, t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-rN2Gj4lM.mjs";
5
- import { a as useApiKeys, n as apiReferencePath, r as ApiKeysPanel } from "./account-links-Dg4ugnGW.mjs";
6
- import { t as ChoosePlanNotice } from "./ChoosePlanNotice-DITJNrh1.mjs";
5
+ import { n as useApiKeys } from "./useApiKeys-CVgXfUop.mjs";
6
+ import { n as apiReferencePath, r as ApiKeysPanel } from "./account-links-D_vS0GJn.mjs";
7
+ import { n as ChoosePlanNotice, t as keyPlanNote } from "./key-plan-note-DTWeFhn2.mjs";
7
8
  import { Head } from "@lukoweb/apitogo/components";
8
9
  import { KeyRoundIcon } from "@lukoweb/apitogo/icons";
9
10
  import { Link as Link$1 } from "@lukoweb/apitogo/router";
@@ -63,6 +64,7 @@ const CredentialsPage = () => {
63
64
  const gatewayBaseUrl = credentials?.gatewayBaseUrl?.trim() || apiKeys?.gatewayBaseUrl?.trim() || configuredGatewayBaseUrl?.trim() || zudokuConfig.__meta?.gatewayBaseUrl?.trim() || null;
64
65
  const header = configuredKeyHeader?.trim() || apiKeys?.header || DEMO_GATEWAY_KEY_HEADER;
65
66
  const plans = apiKeys?.plans ?? [];
67
+ const planNote = isDemoSession ? null : keyPlanNote(plans);
66
68
  return /* @__PURE__ */ jsxs("div", {
67
69
  className: "mx-auto max-w-screen-lg px-4 pb-16 pt-(--padding-content-top) lg:px-8",
68
70
  children: [
@@ -128,29 +130,37 @@ const CredentialsPage = () => {
128
130
  }) : null,
129
131
  (isDemoSession || !isPreview) && auth.isAuthenticated && !error && !keysError && !isLoading && !keysLoading ? /* @__PURE__ */ jsxs("div", {
130
132
  className: "space-y-6",
131
- children: [plans.length === 0 ? /* @__PURE__ */ jsx(ChoosePlanNotice, {}) : plans.map((plan) => /* @__PURE__ */ jsx(ApiKeysPanel, {
132
- plan,
133
- header,
134
- readOnly: isDemoSession
135
- }, plan.planSku)), /* @__PURE__ */ jsxs("article", {
136
- className: "rounded-xl border bg-card/60 p-6 space-y-4",
137
- children: [/* @__PURE__ */ jsxs("div", {
138
- className: "space-y-1",
139
- children: [/* @__PURE__ */ jsx("h2", {
140
- className: "text-lg font-medium",
141
- children: "Gateway URL"
142
- }), /* @__PURE__ */ jsx("p", {
133
+ children: [
134
+ planNote ? /* @__PURE__ */ jsx("div", {
135
+ role: "status",
136
+ className: "rounded-xl border border-primary/30 bg-primary/5 p-4 text-sm",
137
+ children: planNote.text
138
+ }) : null,
139
+ plans.length === 0 ? /* @__PURE__ */ jsx(ChoosePlanNotice, {}) : plans.map((plan) => /* @__PURE__ */ jsx(ApiKeysPanel, {
140
+ plan,
141
+ header,
142
+ readOnly: isDemoSession
143
+ }, plan.planSku)),
144
+ /* @__PURE__ */ jsxs("article", {
145
+ className: "rounded-xl border bg-card/60 p-6 space-y-4",
146
+ children: [/* @__PURE__ */ jsxs("div", {
147
+ className: "space-y-1",
148
+ children: [/* @__PURE__ */ jsx("h2", {
149
+ className: "text-lg font-medium",
150
+ children: "Gateway URL"
151
+ }), /* @__PURE__ */ jsx("p", {
152
+ className: "text-sm text-muted-foreground",
153
+ children: "Base URL for API requests through the gateway."
154
+ })]
155
+ }), gatewayBaseUrl ? /* @__PURE__ */ jsx(Value, {
156
+ className: "max-w-full w-full",
157
+ value: gatewayBaseUrl
158
+ }) : /* @__PURE__ */ jsx("p", {
143
159
  className: "text-sm text-muted-foreground",
144
- children: "Base URL for API requests through the gateway."
160
+ children: "Available after your API is deployed to the gateway."
145
161
  })]
146
- }), gatewayBaseUrl ? /* @__PURE__ */ jsx(Value, {
147
- className: "max-w-full w-full",
148
- value: gatewayBaseUrl
149
- }) : /* @__PURE__ */ jsx("p", {
150
- className: "text-sm text-muted-foreground",
151
- children: "Available after your API is deployed to the gateway."
152
- })]
153
- })]
162
+ })
163
+ ]
154
164
  }) : null,
155
165
  /* @__PURE__ */ jsxs("div", {
156
166
  className: "mt-8 flex flex-wrap gap-3",
@@ -1,5 +1,5 @@
1
1
  import { n as CHECKOUT_PATH, r as CONTACT_SALES_PATH } from "./paths-ujy9wjVK.mjs";
2
- import { a as formatPlanDisplayPrice, c as isActiveSubscription, d as isPlanFree, f as listPlanFeatureLines, s as formatPlanPriceUnit, u as isPlanCustomPriced } from "./useDevPortalApiBaseUrl-Dk00H8xy.mjs";
2
+ import { d as listPlanFeatureLines, i as formatPlanDisplayPrice, l as isPlanCustomPriced, o as formatPlanPriceUnit, s as isActiveSubscription, u as isPlanFree } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
3
3
  import { Button } from "@lukoweb/apitogo/components";
4
4
  import { Link as Link$1 } from "@lukoweb/apitogo/router";
5
5
  import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@lukoweb/apitogo/ui/AlertDialog.js";
@@ -1,9 +1,9 @@
1
- import { g as pickFeaturedPlanSku, n as useDevPortalPreviewMode, p as parseManifestPlans } from "./useDevPortalApiBaseUrl-Dk00H8xy.mjs";
2
- import { t as PricingCard } from "./PricingCard-D-owYlf3.mjs";
1
+ import { f as parseManifestPlans, h as pickFeaturedPlanSku, n as useDevPortalPreviewMode } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
+ import { t as PricingCard } from "./PricingCard-CKdq1oWn.mjs";
3
3
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
- import { t as usePlans } from "./usePlans-DVGt2Myv.mjs";
5
- import { t as isPreviewPricingEnabled } from "./pricing-config-CfhRX9tk.mjs";
6
- import { a as isPricingLoading, n as buildCompareRows } from "./billing-state-DjUXQYSr.mjs";
4
+ import { t as usePlans } from "./usePlans-DPGFWcVo.mjs";
5
+ import { t as isPreviewPricingEnabled } from "./pricing-config-02m4dyFf.mjs";
6
+ import { a as isPricingLoading, n as buildCompareRows } from "./billing-state-DbU5Lpoz.mjs";
7
7
  import { Head, Heading } from "@lukoweb/apitogo/components";
8
8
  import { Navigate } from "@lukoweb/apitogo/router";
9
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -1,15 +1,16 @@
1
1
  import { i as CREDENTIALS_PATH, t as ACCOUNT_PLANS_PATH } from "./paths-ujy9wjVK.mjs";
2
- import { c as isActiveSubscription, i as formatBillingPeriod, m as parseMonthlyQuotaCalls, o as formatPlanPrice, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-Dk00H8xy.mjs";
2
+ import { a as formatPlanPrice, p as parseMonthlyQuotaCalls, s as isActiveSubscription, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
3
3
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
- import { t as useMySubscriptions } from "./useMySubscriptions-B4iV7zFs.mjs";
5
- import { t as usePlans } from "./usePlans-DVGt2Myv.mjs";
6
- import { t as useUsage } from "./useUsage-DOUibue4.mjs";
7
- import { i as hasPaymentProvider, r as checkoutConfirmingText } from "./billing-state-DjUXQYSr.mjs";
8
- import { t as ChoosePlanNotice } from "./ChoosePlanNotice-DITJNrh1.mjs";
4
+ import { t as useMySubscriptions } from "./useMySubscriptions-BtxAXA1m.mjs";
5
+ import { t as usePlans } from "./usePlans-DPGFWcVo.mjs";
6
+ import { n as useApiKeys } from "./useApiKeys-CVgXfUop.mjs";
7
+ import { t as useUsage } from "./useUsage--5sZub3M.mjs";
8
+ import { i as hasPaymentProvider, r as checkoutConfirmingText } from "./billing-state-DbU5Lpoz.mjs";
9
+ import { n as ChoosePlanNotice, t as keyPlanNote } from "./key-plan-note-DTWeFhn2.mjs";
9
10
  import { Head, Spinner } from "@lukoweb/apitogo/components";
10
11
  import { Link as Link$1, useSearchParams } from "@lukoweb/apitogo/router";
11
12
  import { useEffect } from "react";
12
- import { jsx, jsxs } from "react/jsx-runtime";
13
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
14
  import { useMutation } from "@lukoweb/apitogo/react-query";
14
15
  import { useAuth } from "@lukoweb/apitogo/hooks";
15
16
  import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
@@ -47,6 +48,8 @@ const SubscriptionsPage = () => {
47
48
  const { data: catalog } = usePlans();
48
49
  const billingPortal = useBillingPortal();
49
50
  const { data: usage } = useUsage("30d");
51
+ const { data: apiKeys } = useApiKeys();
52
+ const planNote = keyPlanNote(apiKeys?.plans ?? []);
50
53
  const planBySku = new Map((catalog?.plans ?? []).map((plan) => [plan.sku, plan]));
51
54
  const subscriptions = me?.subscriptions ?? [];
52
55
  const activeSubscription = subscriptions.find((sub) => isActiveSubscription(sub.status));
@@ -81,9 +84,18 @@ const SubscriptionsPage = () => {
81
84
  className: "mb-6 rounded-xl border border-border bg-card p-4 text-sm",
82
85
  children: checkoutConfirmingText(checkoutPaid)
83
86
  }) : null,
84
- checkoutSuccess && activeSubscription ? /* @__PURE__ */ jsxs("div", {
87
+ checkoutSuccess && activeSubscription ? /* @__PURE__ */ jsx("div", {
85
88
  className: "mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-sm",
86
- children: [
89
+ children: planNote ? /* @__PURE__ */ jsxs(Fragment, { children: [
90
+ planNote.text,
91
+ " ",
92
+ /* @__PURE__ */ jsx(Link$1, {
93
+ to: CREDENTIALS_PATH,
94
+ className: "font-medium underline",
95
+ children: "Create the key"
96
+ }),
97
+ "."
98
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
87
99
  "Your subscription is active.",
88
100
  " ",
89
101
  /* @__PURE__ */ jsx(Link$1, {
@@ -92,7 +104,7 @@ const SubscriptionsPage = () => {
92
104
  children: "View your API credentials"
93
105
  }),
94
106
  "."
95
- ]
107
+ ] })
96
108
  }) : null,
97
109
  isLoading ? /* @__PURE__ */ jsx("p", {
98
110
  className: "text-muted-foreground",
@@ -109,14 +121,9 @@ const SubscriptionsPage = () => {
109
121
  className: "mt-2 text-2xl font-bold",
110
122
  children: activePlan?.displayName ?? activeSubscription.planSku
111
123
  }),
112
- activePlan ? /* @__PURE__ */ jsxs("p", {
124
+ activePlan ? /* @__PURE__ */ jsx("p", {
113
125
  className: "mt-1 text-sm text-muted-foreground",
114
- children: [
115
- formatPlanPrice(activePlan),
116
- " ·",
117
- " ",
118
- formatBillingPeriod(activePlan.billingPeriod)
119
- ]
126
+ children: formatPlanPrice(activePlan)
120
127
  }) : /* @__PURE__ */ jsx("p", {
121
128
  className: "mt-1 text-sm text-muted-foreground",
122
129
  children: "This plan is no longer offered. Your access continues until you cancel or change plan."
@@ -164,14 +171,9 @@ const SubscriptionsPage = () => {
164
171
  children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
165
172
  className: "font-semibold",
166
173
  children: plan?.displayName ?? subscription.planSku
167
- }), plan ? /* @__PURE__ */ jsxs("p", {
174
+ }), plan ? /* @__PURE__ */ jsx("p", {
168
175
  className: "mt-1 text-sm text-muted-foreground",
169
- children: [
170
- formatPlanPrice(plan),
171
- " ·",
172
- " ",
173
- formatBillingPeriod(plan.billingPeriod)
174
- ]
176
+ children: formatPlanPrice(plan)
175
177
  }) : null] }), /* @__PURE__ */ jsx("span", {
176
178
  className: "rounded-full bg-muted px-3 py-1 text-xs font-medium text-muted-foreground",
177
179
  children: subscription.status
@@ -1,8 +1,8 @@
1
- import { c as isActiveSubscription, h as parseQuotaPeriodDays, m as parseMonthlyQuotaCalls } from "./useDevPortalApiBaseUrl-Dk00H8xy.mjs";
1
+ import { m as parseQuotaPeriodDays, p as parseMonthlyQuotaCalls, s as isActiveSubscription } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
2
  import "./DevPortalBillingContext-CR83s4LO.mjs";
3
- import { t as useMySubscriptions } from "./useMySubscriptions-B4iV7zFs.mjs";
4
- import { t as usePlans } from "./usePlans-DVGt2Myv.mjs";
5
- import { t as useUsage } from "./useUsage-DOUibue4.mjs";
3
+ import { t as useMySubscriptions } from "./useMySubscriptions-BtxAXA1m.mjs";
4
+ import { t as usePlans } from "./usePlans-DPGFWcVo.mjs";
5
+ import { t as useUsage } from "./useUsage--5sZub3M.mjs";
6
6
  import { Head } from "@lukoweb/apitogo/components";
7
7
  import { Link as Link$1 } from "@lukoweb/apitogo/router";
8
8
  import { useState } from "react";
@@ -1,177 +1,14 @@
1
- import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-Dk00H8xy.mjs";
2
- import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
3
- import { n as createDemoApiKeys, t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-rN2Gj4lM.mjs";
1
+ import { i as useRevokeApiKey, r as useCreateApiKey, t as API_KEY_CAP_REACHED } from "./useApiKeys-CVgXfUop.mjs";
4
2
  import { Spinner } from "@lukoweb/apitogo/components";
5
3
  import { KeyRoundIcon, TrashIcon } from "@lukoweb/apitogo/icons";
6
4
  import { useState } from "react";
7
5
  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
6
  import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
11
7
  import { Alert, AlertTitle } from "@lukoweb/apitogo/ui/Alert.js";
12
8
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@lukoweb/apitogo/ui/Dialog.js";
13
9
  import { Input } from "@lukoweb/apitogo/ui/Input.js";
14
10
  import { Secret } from "@lukoweb/apitogo/ui/Secret.js";
15
11
 
16
- //#region src/utils/playground-key.ts
17
- const STORAGE_KEY = "apitogo-playground-api-key";
18
- const PLAYGROUND_KEY_IDENTITY_ID = "api-key";
19
- const storage = () => {
20
- try {
21
- return typeof localStorage === "undefined" ? void 0 : localStorage;
22
- } catch {
23
- return;
24
- }
25
- };
26
- /**
27
- * Remembers a key the moment it is created: the plans backend shows a key's secret only then, so this is the one
28
- * chance to offer it to the playground later. Only the last key is kept, in this browser only.
29
- */
30
- function rememberPlaygroundKey(entry) {
31
- if (!entry.key) return;
32
- const value = {
33
- id: entry.id,
34
- key: entry.key,
35
- header: entry.header || DEMO_GATEWAY_KEY_HEADER,
36
- keyPrefix: entry.keyPrefix
37
- };
38
- try {
39
- storage()?.setItem(STORAGE_KEY, JSON.stringify(value));
40
- } catch {}
41
- }
42
- function readPlaygroundKey() {
43
- try {
44
- const raw = storage()?.getItem(STORAGE_KEY);
45
- if (!raw) return void 0;
46
- const parsed = JSON.parse(raw);
47
- return parsed.id && parsed.key && parsed.header && parsed.keyPrefix ? parsed : void 0;
48
- } catch {
49
- return;
50
- }
51
- }
52
- /** A revoked key stops working, so it is no longer offered. */
53
- function forgetPlaygroundKey(keyId) {
54
- if (readPlaygroundKey()?.id !== keyId) return;
55
- try {
56
- storage()?.removeItem(STORAGE_KEY);
57
- } catch {}
58
- }
59
- /** The "Try it" identity for the remembered key, sent in the gateway's key header. */
60
- function playgroundKeyIdentities() {
61
- const remembered = readPlaygroundKey();
62
- if (!remembered) return [];
63
- return [{
64
- id: PLAYGROUND_KEY_IDENTITY_ID,
65
- label: `Your API key (${remembered.keyPrefix}…)`,
66
- authorizationFields: { headers: [remembered.header] },
67
- authorizeRequest: (request) => {
68
- request.headers.set(remembered.header, remembered.key);
69
- return request;
70
- }
71
- }];
72
- }
73
-
74
- //#endregion
75
- //#region src/hooks/useApiKeys.ts
76
- /** Thrown when the developer has hit their plan's per-developer key cap. */
77
- const API_KEY_CAP_REACHED = "API_KEY_CAP_REACHED";
78
- function apiKeysQueryKey(apiBaseUrl) {
79
- return ["dev-portal-api-keys", apiBaseUrl];
80
- }
81
- function useApiKeys() {
82
- const auth = useAuth();
83
- const apiBaseUrl = useDevPortalApiBaseUrl();
84
- const isPreview = useDevPortalPreviewMode();
85
- const isDemoSession = useIsDemoSession();
86
- const { previewPlans } = useDevPortalBillingConfig();
87
- const liveQuery = useQuery({
88
- queryKey: apiKeysQueryKey(apiBaseUrl),
89
- enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
90
- queryFn: async () => {
91
- const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
92
- method: "GET",
93
- headers: { Accept: "application/json" },
94
- credentials: "include"
95
- });
96
- if (response.status === 404) throw new Error("NO_ENTITLEMENT");
97
- if (!response.ok) throw new Error("Failed to load API keys.");
98
- return await response.json();
99
- },
100
- retry: (failureCount, error) => error instanceof Error && error.message === "NO_ENTITLEMENT" ? false : failureCount < 2
101
- });
102
- if (isDemoSession && auth.isAuthenticated) return {
103
- ...liveQuery,
104
- data: createDemoApiKeys(previewPlans),
105
- error: null,
106
- isError: false,
107
- isFetching: false,
108
- isLoading: false,
109
- isPending: false,
110
- isSuccess: true,
111
- status: "success"
112
- };
113
- return liveQuery;
114
- }
115
- function useCreateApiKey() {
116
- const apiBaseUrl = useDevPortalApiBaseUrl();
117
- const queryClient = useQueryClient();
118
- const { keyHeader } = useDevPortalBillingConfig();
119
- return useMutation({
120
- mutationFn: async ({ name, planSku }) => {
121
- if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
122
- const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
123
- method: "POST",
124
- headers: {
125
- Accept: "application/json",
126
- "Content-Type": "application/json"
127
- },
128
- credentials: "include",
129
- body: JSON.stringify({
130
- name,
131
- planSku
132
- })
133
- });
134
- if (response.status === 409) throw new Error(API_KEY_CAP_REACHED);
135
- if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Failed to create API key.");
136
- return await response.json();
137
- },
138
- onSuccess: async (created) => {
139
- rememberPlaygroundKey({
140
- ...created,
141
- header: keyHeader?.trim() || queryClient.getQueryData(apiKeysQueryKey(apiBaseUrl))?.header
142
- });
143
- await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
144
- }
145
- });
146
- }
147
- function useRevokeApiKey() {
148
- const apiBaseUrl = useDevPortalApiBaseUrl();
149
- const queryClient = useQueryClient();
150
- return useMutation({
151
- mutationFn: async (keyId) => {
152
- if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
153
- const response = await fetch(`${apiBaseUrl}/api/v1/api-keys/${keyId}`, {
154
- method: "DELETE",
155
- headers: { Accept: "application/json" },
156
- credentials: "include"
157
- });
158
- if (!response.ok && response.status !== 204) throw new Error(await readProblemDetail(response) ?? "Failed to revoke API key.");
159
- },
160
- onSuccess: async (_result, keyId) => {
161
- forgetPlaygroundKey(keyId);
162
- await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
163
- }
164
- });
165
- }
166
- async function readProblemDetail(response) {
167
- try {
168
- return (await response.json()).detail?.trim() || null;
169
- } catch {
170
- return null;
171
- }
172
- }
173
-
174
- //#endregion
175
12
  //#region src/components/ApiKeysPanel.tsx
176
13
  /**
177
14
  * Lists gateway API keys for one plan with create and revoke. The plaintext key is returned by the
@@ -369,4 +206,4 @@ function apiReferencePath(links) {
369
206
  }
370
207
 
371
208
  //#endregion
372
- export { useApiKeys as a, playgroundKeyIdentities as c, API_KEY_CAP_REACHED as i, apiReferencePath as n, useCreateApiKey as o, ApiKeysPanel as r, useRevokeApiKey as s, accountLayoutProps as t };
209
+ export { apiReferencePath as n, ApiKeysPanel as r, accountLayoutProps as t };
@@ -1,4 +1,4 @@
1
- import { _ as quotaCell, v as rateLimitCell } from "./useDevPortalApiBaseUrl-Dk00H8xy.mjs";
1
+ import { _ as rateLimitCell, g as quotaCell } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
2
 
3
3
  //#region src/utils/billing-state.ts
4
4
  /** Whether the pricing page has no plans to show yet because the answer is still on its way. */
package/dist/index.mjs CHANGED
@@ -1,14 +1,15 @@
1
- import { t as AccountPlansPage } from "./AccountPlansPage-BEDZbyAS.mjs";
1
+ import { t as AccountPlansPage } from "./AccountPlansPage-CeUizdWk.mjs";
2
2
  import { a as LOGIN_NAV_PATH, c as USAGE_PATH, i as CREDENTIALS_PATH, n as CHECKOUT_PATH, o as PRICING_PATH, r as CONTACT_SALES_PATH, s as SUBSCRIPTIONS_PATH, t as ACCOUNT_PLANS_PATH } from "./paths-ujy9wjVK.mjs";
3
- import { a as formatPlanDisplayPrice, c as isActiveSubscription, f as listPlanFeatureLines, g as pickFeaturedPlanSku, p as parseManifestPlans, s as formatPlanPriceUnit, y as resolveDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-Dk00H8xy.mjs";
4
- import "./PricingCard-D-owYlf3.mjs";
3
+ import { d as listPlanFeatureLines, f as parseManifestPlans, h as pickFeaturedPlanSku, i as formatPlanDisplayPrice, o as formatPlanPriceUnit, s as isActiveSubscription, v as resolveDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
4
+ import "./PricingCard-CKdq1oWn.mjs";
5
5
  import { n as useDevPortalBillingConfig, t as DevPortalBillingProvider } from "./DevPortalBillingContext-CR83s4LO.mjs";
6
6
  import { t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-rN2Gj4lM.mjs";
7
- import { t as useMySubscriptions } from "./useMySubscriptions-B4iV7zFs.mjs";
8
- import { n as startPlansPrefetch, t as usePlans } from "./usePlans-DVGt2Myv.mjs";
9
- import { a as useApiKeys, c as playgroundKeyIdentities, i as API_KEY_CAP_REACHED, o as useCreateApiKey, r as ApiKeysPanel, s as useRevokeApiKey, t as accountLayoutProps } from "./account-links-Dg4ugnGW.mjs";
10
- import { n as useUsageRecent, t as useUsage } from "./useUsage-DOUibue4.mjs";
11
- import { t as isPreviewPricingEnabled } from "./pricing-config-CfhRX9tk.mjs";
7
+ import { t as useMySubscriptions } from "./useMySubscriptions-BtxAXA1m.mjs";
8
+ import { n as startPlansPrefetch, t as usePlans } from "./usePlans-DPGFWcVo.mjs";
9
+ import { a as playgroundKeyIdentities, i as useRevokeApiKey, n as useApiKeys, r as useCreateApiKey, t as API_KEY_CAP_REACHED } from "./useApiKeys-CVgXfUop.mjs";
10
+ import { n as useUsageRecent, t as useUsage } from "./useUsage--5sZub3M.mjs";
11
+ import { r as ApiKeysPanel, t as accountLayoutProps } from "./account-links-D_vS0GJn.mjs";
12
+ import { t as isPreviewPricingEnabled } from "./pricing-config-02m4dyFf.mjs";
12
13
  import { Head, Link } from "@lukoweb/apitogo/components";
13
14
  import { ArrowRightIcon, KeyRoundIcon, StarsIcon } from "@lukoweb/apitogo/icons";
14
15
  import { Outlet } from "@lukoweb/apitogo/router";
@@ -365,13 +366,13 @@ async function getSignedInIdentities(apiBaseUrl, fetchImpl = fetch) {
365
366
 
366
367
  //#endregion
367
368
  //#region src/DevPortalBillingPlugin.tsx
368
- const AccountPlansPage$1 = lazy(() => import("./AccountPlansPage-BEDZbyAS.mjs").then((n) => n.n));
369
- const CheckoutPage = lazy(() => import("./CheckoutPage-6nFq9GKO.mjs"));
370
- const ContactSalesPage = lazy(() => import("./ContactSalesPage-Imh39E8z.mjs"));
371
- const CredentialsPage = lazy(() => import("./CredentialsPage-D9voniJR.mjs"));
372
- const PricingPage = lazy(() => import("./PricingPage-BTPQ-ntp.mjs"));
373
- const SubscriptionsPage = lazy(() => import("./SubscriptionsPage-Bo5D3K1x.mjs"));
374
- const UsagePage = lazy(() => import("./UsagePage-CK6p01Ut.mjs"));
369
+ const AccountPlansPage$1 = lazy(() => import("./AccountPlansPage-CeUizdWk.mjs").then((n) => n.n));
370
+ const CheckoutPage = lazy(() => import("./CheckoutPage-CwSElFUk.mjs"));
371
+ const ContactSalesPage = lazy(() => import("./ContactSalesPage-q7Dxxtx6.mjs"));
372
+ const CredentialsPage = lazy(() => import("./CredentialsPage-B2jPo6Go.mjs"));
373
+ const PricingPage = lazy(() => import("./PricingPage-DnQQfy9L.mjs"));
374
+ const SubscriptionsPage = lazy(() => import("./SubscriptionsPage-msO31jv-.mjs"));
375
+ const UsagePage = lazy(() => import("./UsagePage-BF4QgrTc.mjs"));
375
376
  function resolvePricingEnabled(config) {
376
377
  if (config.__meta?.pricingEnabled !== void 0) return config.__meta.pricingEnabled;
377
378
  return isPreviewPricingEnabled();
@@ -31,4 +31,23 @@ const ChoosePlanNotice = ({ className = "rounded-xl border border-dashed p-8 tex
31
31
  });
32
32
 
33
33
  //#endregion
34
- export { ChoosePlanNotice as t };
34
+ //#region src/utils/key-plan-note.ts
35
+ /**
36
+ * An API user may hold several plans at once, each with its own keys and limits (API-121). After buying a plan, their
37
+ * app still calls with the key of the plan they had, so it keeps that plan's limits. When a plan has no key yet while
38
+ * another plan they hold has one, this says so and what to do; otherwise null.
39
+ */
40
+ function keyPlanNote(plans) {
41
+ const withKeys = plans.find((plan) => plan.activeCount > 0);
42
+ const withoutKey = plans.find((plan) => plan.activeCount === 0 && plan.canCreate);
43
+ if (!withKeys || !withoutKey) return null;
44
+ const fresh = withoutKey.displayName || withoutKey.planSku;
45
+ const old = withKeys.displayName || withKeys.planSku;
46
+ return {
47
+ planSku: withoutKey.planSku,
48
+ text: `You're on ${fresh} now. Your ${old} key still has ${old} limits. Create a ${fresh} key and use it in your app to get ${fresh} limits.`
49
+ };
50
+ }
51
+
52
+ //#endregion
53
+ export { ChoosePlanNotice as n, keyPlanNote as t };
@@ -0,0 +1,166 @@
1
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
+ import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
3
+ import { n as createDemoApiKeys, t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-rN2Gj4lM.mjs";
4
+ import { useMutation, useQuery, useQueryClient } from "@lukoweb/apitogo/react-query";
5
+ import { useAuth } from "@lukoweb/apitogo/hooks";
6
+
7
+ //#region src/utils/playground-key.ts
8
+ const STORAGE_KEY = "apitogo-playground-api-key";
9
+ const PLAYGROUND_KEY_IDENTITY_ID = "api-key";
10
+ const storage = () => {
11
+ try {
12
+ return typeof localStorage === "undefined" ? void 0 : localStorage;
13
+ } catch {
14
+ return;
15
+ }
16
+ };
17
+ /**
18
+ * Remembers a key the moment it is created: the plans backend shows a key's secret only then, so this is the one
19
+ * chance to offer it to the playground later. Only the last key is kept, in this browser only.
20
+ */
21
+ function rememberPlaygroundKey(entry) {
22
+ if (!entry.key) return;
23
+ const value = {
24
+ id: entry.id,
25
+ key: entry.key,
26
+ header: entry.header || DEMO_GATEWAY_KEY_HEADER,
27
+ keyPrefix: entry.keyPrefix
28
+ };
29
+ try {
30
+ storage()?.setItem(STORAGE_KEY, JSON.stringify(value));
31
+ } catch {}
32
+ }
33
+ function readPlaygroundKey() {
34
+ try {
35
+ const raw = storage()?.getItem(STORAGE_KEY);
36
+ if (!raw) return void 0;
37
+ const parsed = JSON.parse(raw);
38
+ return parsed.id && parsed.key && parsed.header && parsed.keyPrefix ? parsed : void 0;
39
+ } catch {
40
+ return;
41
+ }
42
+ }
43
+ /** A revoked key stops working, so it is no longer offered. */
44
+ function forgetPlaygroundKey(keyId) {
45
+ if (readPlaygroundKey()?.id !== keyId) return;
46
+ try {
47
+ storage()?.removeItem(STORAGE_KEY);
48
+ } catch {}
49
+ }
50
+ /** The "Try it" identity for the remembered key, sent in the gateway's key header. */
51
+ function playgroundKeyIdentities() {
52
+ const remembered = readPlaygroundKey();
53
+ if (!remembered) return [];
54
+ return [{
55
+ id: PLAYGROUND_KEY_IDENTITY_ID,
56
+ label: `Your API key (${remembered.keyPrefix}…)`,
57
+ authorizationFields: { headers: [remembered.header] },
58
+ authorizeRequest: (request) => {
59
+ request.headers.set(remembered.header, remembered.key);
60
+ return request;
61
+ }
62
+ }];
63
+ }
64
+
65
+ //#endregion
66
+ //#region src/hooks/useApiKeys.ts
67
+ /** Thrown when the developer has hit their plan's per-developer key cap. */
68
+ const API_KEY_CAP_REACHED = "API_KEY_CAP_REACHED";
69
+ function apiKeysQueryKey(apiBaseUrl) {
70
+ return ["dev-portal-api-keys", apiBaseUrl];
71
+ }
72
+ function useApiKeys() {
73
+ const auth = useAuth();
74
+ const apiBaseUrl = useDevPortalApiBaseUrl();
75
+ const isPreview = useDevPortalPreviewMode();
76
+ const isDemoSession = useIsDemoSession();
77
+ const { previewPlans } = useDevPortalBillingConfig();
78
+ const liveQuery = useQuery({
79
+ queryKey: apiKeysQueryKey(apiBaseUrl),
80
+ enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
81
+ queryFn: async () => {
82
+ const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
83
+ method: "GET",
84
+ headers: { Accept: "application/json" },
85
+ credentials: "include"
86
+ });
87
+ if (response.status === 404) throw new Error("NO_ENTITLEMENT");
88
+ if (!response.ok) throw new Error("Failed to load API keys.");
89
+ return await response.json();
90
+ },
91
+ retry: (failureCount, error) => error instanceof Error && error.message === "NO_ENTITLEMENT" ? false : failureCount < 2
92
+ });
93
+ if (isDemoSession && auth.isAuthenticated) return {
94
+ ...liveQuery,
95
+ data: createDemoApiKeys(previewPlans),
96
+ error: null,
97
+ isError: false,
98
+ isFetching: false,
99
+ isLoading: false,
100
+ isPending: false,
101
+ isSuccess: true,
102
+ status: "success"
103
+ };
104
+ return liveQuery;
105
+ }
106
+ function useCreateApiKey() {
107
+ const apiBaseUrl = useDevPortalApiBaseUrl();
108
+ const queryClient = useQueryClient();
109
+ const { keyHeader } = useDevPortalBillingConfig();
110
+ return useMutation({
111
+ mutationFn: async ({ name, planSku }) => {
112
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
113
+ const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
114
+ method: "POST",
115
+ headers: {
116
+ Accept: "application/json",
117
+ "Content-Type": "application/json"
118
+ },
119
+ credentials: "include",
120
+ body: JSON.stringify({
121
+ name,
122
+ planSku
123
+ })
124
+ });
125
+ if (response.status === 409) throw new Error(API_KEY_CAP_REACHED);
126
+ if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Failed to create API key.");
127
+ return await response.json();
128
+ },
129
+ onSuccess: async (created) => {
130
+ rememberPlaygroundKey({
131
+ ...created,
132
+ header: keyHeader?.trim() || queryClient.getQueryData(apiKeysQueryKey(apiBaseUrl))?.header
133
+ });
134
+ await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
135
+ }
136
+ });
137
+ }
138
+ function useRevokeApiKey() {
139
+ const apiBaseUrl = useDevPortalApiBaseUrl();
140
+ const queryClient = useQueryClient();
141
+ return useMutation({
142
+ mutationFn: async (keyId) => {
143
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
144
+ const response = await fetch(`${apiBaseUrl}/api/v1/api-keys/${keyId}`, {
145
+ method: "DELETE",
146
+ headers: { Accept: "application/json" },
147
+ credentials: "include"
148
+ });
149
+ if (!response.ok && response.status !== 204) throw new Error(await readProblemDetail(response) ?? "Failed to revoke API key.");
150
+ },
151
+ onSuccess: async (_result, keyId) => {
152
+ forgetPlaygroundKey(keyId);
153
+ await queryClient.invalidateQueries({ queryKey: apiKeysQueryKey(apiBaseUrl) });
154
+ }
155
+ });
156
+ }
157
+ async function readProblemDetail(response) {
158
+ try {
159
+ return (await response.json()).detail?.trim() || null;
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ //#endregion
166
+ export { playgroundKeyIdentities as a, useRevokeApiKey as i, useApiKeys as n, useCreateApiKey as r, API_KEY_CAP_REACHED as t };
@@ -250,4 +250,4 @@ function useDevPortalPreviewMode() {
250
250
  }
251
251
 
252
252
  //#endregion
253
- export { quotaCell as _, formatPlanDisplayPrice as a, isActiveSubscription as c, isPlanFree as d, listPlanFeatureLines as f, pickFeaturedPlanSku as g, parseQuotaPeriodDays as h, formatBillingPeriod as i, isPlaceholderDevPortalApiUrl as l, parseMonthlyQuotaCalls as m, useDevPortalPreviewMode as n, formatPlanPrice as o, parseManifestPlans as p, useIsDemoSession as r, formatPlanPriceUnit as s, useDevPortalApiBaseUrl as t, isPlanCustomPriced as u, rateLimitCell as v, resolveDevPortalApiBaseUrl as y };
253
+ export { rateLimitCell as _, formatPlanPrice as a, isPlaceholderDevPortalApiUrl as c, listPlanFeatureLines as d, parseManifestPlans as f, quotaCell as g, pickFeaturedPlanSku as h, formatPlanDisplayPrice as i, isPlanCustomPriced as l, parseQuotaPeriodDays as m, useDevPortalPreviewMode as n, formatPlanPriceUnit as o, parseMonthlyQuotaCalls as p, useIsDemoSession as r, isActiveSubscription as s, useDevPortalApiBaseUrl as t, isPlanFree as u, resolveDevPortalApiBaseUrl as v };
@@ -1,4 +1,4 @@
1
- import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-Dk00H8xy.mjs";
1
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
2
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
3
3
  import { i as createDemoMeResponse } from "./demo-session-rN2Gj4lM.mjs";
4
4
  import { useQuery } from "@lukoweb/apitogo/react-query";
@@ -1,4 +1,4 @@
1
- import { l as isPlaceholderDevPortalApiUrl, n as useDevPortalPreviewMode, p as parseManifestPlans, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-Dk00H8xy.mjs";
1
+ import { c as isPlaceholderDevPortalApiUrl, f as parseManifestPlans, n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
2
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
3
3
  import { useQuery } from "@lukoweb/apitogo/react-query";
4
4
 
@@ -1,4 +1,4 @@
1
- import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-Dk00H8xy.mjs";
1
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
2
  import { useQuery } from "@lukoweb/apitogo/react-query";
3
3
  import { useAuth } from "@lukoweb/apitogo/hooks";
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo-plugin-dev-portal-billing",
3
- "version": "0.2.51",
3
+ "version": "0.2.52",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
@@ -46,8 +46,8 @@
46
46
  "react": "19.2.4",
47
47
  "react-dom": "19.2.4",
48
48
  "tsdown": "0.20.3",
49
- "@lukoweb/apitogo": "0.2.51",
50
- "@lukoweb/apitogo-module-user-panel": "0.2.51"
49
+ "@lukoweb/apitogo": "0.2.52",
50
+ "@lukoweb/apitogo-module-user-panel": "0.2.52"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": ">=19.2.0",