@lukoweb/apitogo-plugin-dev-portal-billing 0.1.72 → 0.1.74

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 CHANGED
@@ -5,17 +5,6 @@ import * as _tanstack_react_query0 from "@tanstack/react-query";
5
5
  //#region src/pages/AccountPlansPage.d.ts
6
6
  declare const AccountPlansPage: () => react_jsx_runtime0.JSX.Element;
7
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
19
8
  //#region src/types.d.ts
20
9
  type DevPortalApiKey = {
21
10
  id: string;
@@ -26,11 +15,16 @@ type DevPortalApiKey = {
26
15
  revokedAtUtc: string | null; /** Present only in the response to a successful create; never returned again. */
27
16
  key: string | null;
28
17
  };
29
- type DevPortalApiKeyListResponse = {
18
+ type DevPortalApiKeyPlan = {
19
+ planSku: string;
20
+ displayName: string;
30
21
  keys: DevPortalApiKey[];
31
22
  activeCount: number; /** Per-developer cap from the project's platform plan; null means unlimited. */
32
23
  maxKeys: number | null;
33
- canCreate: boolean; /** Header the key must be sent in when calling the gateway. */
24
+ canCreate: boolean;
25
+ };
26
+ type DevPortalApiKeyListResponse = {
27
+ plans: DevPortalApiKeyPlan[]; /** Header the key must be sent in when calling the gateway. */
34
28
  header: string;
35
29
  gatewayBaseUrl: string | null;
36
30
  };
@@ -50,6 +44,21 @@ type DevPortalBillingConfig = {
50
44
  }>;
51
45
  };
52
46
  //#endregion
47
+ //#region src/components/ApiKeysPanel.d.ts
48
+ /**
49
+ * Lists gateway API keys for one plan with create and revoke. The plaintext key is returned by the
50
+ * platform exactly once, so it is held in local state until dismissed and never re-fetched.
51
+ */
52
+ declare const ApiKeysPanel: ({
53
+ plan,
54
+ header,
55
+ readOnly
56
+ }: {
57
+ plan: DevPortalApiKeyPlan;
58
+ header: string;
59
+ readOnly?: boolean;
60
+ }) => react_jsx_runtime0.JSX.Element;
61
+ //#endregion
53
62
  //#region src/paths.d.ts
54
63
  declare const PRICING_PATH = "/pricing";
55
64
  declare const CHECKOUT_PATH = "/checkout";
@@ -171,7 +180,10 @@ declare function useApiKeys(): _tanstack_react_query0.QueryObserverRefetchErrorR
171
180
  fetchStatus: _tanstack_react_query0.FetchStatus;
172
181
  promise: Promise<DevPortalApiKeyListResponse>;
173
182
  };
174
- declare function useCreateApiKey(): _tanstack_react_query0.UseMutationResult<DevPortalApiKey, Error, string, unknown>;
183
+ declare function useCreateApiKey(): _tanstack_react_query0.UseMutationResult<DevPortalApiKey, Error, {
184
+ name: string;
185
+ planSku: string;
186
+ }, unknown>;
175
187
  declare function useRevokeApiKey(): _tanstack_react_query0.UseMutationResult<void, Error, string, unknown>;
176
188
  //#endregion
177
189
  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,4 +1,4 @@
1
- import { Button, Head, Heading } from "@lukoweb/apitogo/components";
1
+ import { Button, Head, Heading, Spinner } from "@lukoweb/apitogo/components";
2
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";
@@ -11,7 +11,7 @@ import { useMutation, useQuery, useQueryClient } from "@lukoweb/apitogo/react-qu
11
11
  import { useAuth } from "@lukoweb/apitogo/hooks";
12
12
  import { Alert, AlertTitle } from "@lukoweb/apitogo/ui/Alert.js";
13
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";
14
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@lukoweb/apitogo/ui/Dialog.js";
15
15
  import { Input } from "@lukoweb/apitogo/ui/Input.js";
16
16
  import { Secret } from "@lukoweb/apitogo/ui/Secret.js";
17
17
  import { createPlugin } from "@lukoweb/apitogo/plugins";
@@ -177,7 +177,7 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
177
177
  variant: "secondary",
178
178
  className: "w-full",
179
179
  disabled: true,
180
- children: "Current plan"
180
+ children: "Subscribed"
181
181
  }),
182
182
  onUnsubscribe ? /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
183
183
  asChild: true,
@@ -292,48 +292,57 @@ function useCancelSubscription() {
292
292
  const DEMO_SUBSCRIPTION_KEY = "demo-subscription-key";
293
293
  const DEMO_GATEWAY_KEY_HEADER = "Ocp-Apim-Subscription-Key";
294
294
  const DEMO_MAX_KEYS = 5;
295
- function resolveDemoPlanSku(plans) {
296
- return plans?.[0]?.sku ?? "free";
295
+ function resolveDemoPlans(plans) {
296
+ if (plans && plans.length > 0) return plans.map((plan) => ({
297
+ sku: plan.sku,
298
+ displayName: plan.displayName
299
+ }));
300
+ return [{
301
+ sku: "free",
302
+ displayName: "Free"
303
+ }];
297
304
  }
298
305
  function createDemoMeResponse(plans) {
299
- const planSku = resolveDemoPlanSku(plans);
300
306
  const periodEnd = /* @__PURE__ */ new Date();
301
307
  periodEnd.setMonth(periodEnd.getMonth() + 1);
302
308
  return {
303
309
  userId: "00000000-0000-0000-0000-000000000001",
304
310
  displayName: "Demo User",
305
- subscriptions: [{
306
- planSku,
311
+ subscriptions: resolveDemoPlans(plans).map((plan) => ({
312
+ planSku: plan.sku,
307
313
  status: "Active",
308
314
  entitlementState: "active",
309
315
  currentPeriodEnd: periodEnd.toISOString()
310
- }]
316
+ }))
311
317
  };
312
318
  }
313
319
  function createDemoCredentials(plans) {
314
320
  return {
315
321
  idToken: "demo-id-token",
316
322
  accessToken: null,
317
- subscriptionKey: DEMO_SUBSCRIPTION_KEY,
318
323
  gatewayBaseUrl: null,
319
- planSku: resolveDemoPlanSku(plans),
324
+ planSkus: resolveDemoPlans(plans).map((plan) => plan.sku),
320
325
  expiresAt: null
321
326
  };
322
327
  }
323
- function createDemoApiKeys() {
328
+ function createDemoApiKeys(plans) {
324
329
  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,
330
+ plans: resolveDemoPlans(plans).map((plan, index) => ({
331
+ planSku: plan.sku,
332
+ displayName: plan.displayName,
333
+ keys: index === 0 ? [{
334
+ id: "00000000-0000-0000-0000-0000000000a1",
335
+ name: "default",
336
+ keyPrefix: DEMO_SUBSCRIPTION_KEY.slice(0, 8),
337
+ status: "active",
338
+ createdAtUtc: (/* @__PURE__ */ new Date()).toISOString(),
339
+ revokedAtUtc: null,
340
+ key: null
341
+ }] : [],
342
+ activeCount: index === 0 ? 1 : 0,
343
+ maxKeys: DEMO_MAX_KEYS,
344
+ canCreate: true
345
+ })),
337
346
  header: DEMO_GATEWAY_KEY_HEADER,
338
347
  gatewayBaseUrl: null
339
348
  };
@@ -503,6 +512,7 @@ function useApiKeys() {
503
512
  const apiBaseUrl = useDevPortalApiBaseUrl();
504
513
  const isPreview = useDevPortalPreviewMode();
505
514
  const isDemoSession = useIsDemoSession();
515
+ const { previewPlans } = useDevPortalBillingConfig();
506
516
  const liveQuery = useQuery({
507
517
  queryKey: apiKeysQueryKey(apiBaseUrl),
508
518
  enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
@@ -520,7 +530,7 @@ function useApiKeys() {
520
530
  });
521
531
  if (isDemoSession && auth.isAuthenticated) return {
522
532
  ...liveQuery,
523
- data: createDemoApiKeys(),
533
+ data: createDemoApiKeys(previewPlans),
524
534
  error: null,
525
535
  isError: false,
526
536
  isFetching: false,
@@ -535,7 +545,7 @@ function useCreateApiKey() {
535
545
  const apiBaseUrl = useDevPortalApiBaseUrl();
536
546
  const queryClient = useQueryClient();
537
547
  return useMutation({
538
- mutationFn: async (name) => {
548
+ mutationFn: async ({ name, planSku }) => {
539
549
  if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
540
550
  const response = await fetch(`${apiBaseUrl}/api/v1/api-keys`, {
541
551
  method: "POST",
@@ -544,7 +554,10 @@ function useCreateApiKey() {
544
554
  "Content-Type": "application/json"
545
555
  },
546
556
  credentials: "include",
547
- body: JSON.stringify({ name })
557
+ body: JSON.stringify({
558
+ name,
559
+ planSku
560
+ })
548
561
  });
549
562
  if (response.status === 409) throw new Error(API_KEY_CAP_REACHED);
550
563
  if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Failed to create API key.");
@@ -584,26 +597,38 @@ async function readProblemDetail(response) {
584
597
  //#endregion
585
598
  //#region src/components/ApiKeysPanel.tsx
586
599
  /**
587
- * Lists the developer's gateway API keys with create and revoke. The plaintext key is returned by the
600
+ * Lists gateway API keys for one plan with create and revoke. The plaintext key is returned by the
588
601
  * platform exactly once, so it is held in local state until dismissed and never re-fetched.
589
602
  */
590
- const ApiKeysPanel = ({ readOnly = false }) => {
591
- const { data, isLoading, error } = useApiKeys();
603
+ const ApiKeysPanel = ({ plan, header, readOnly = false }) => {
592
604
  const createKey = useCreateApiKey();
593
605
  const revokeKey = useRevokeApiKey();
594
606
  const [name, setName] = useState("");
595
607
  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);
608
+ const [openRevokeId, setOpenRevokeId] = useState(null);
609
+ const [pendingRevokeId, setPendingRevokeId] = useState(null);
610
+ const activeKeys = plan.keys.filter((key) => key.status === "active");
611
+ const planActiveCount = activeKeys.length;
612
+ const capLabel = plan.maxKeys == null ? `${plan.activeCount} active (all plans)` : `${plan.activeCount} of ${plan.maxKeys} used (all plans)`;
613
+ const canCreate = !readOnly && plan.canCreate;
599
614
  const handleCreate = () => {
600
615
  const trimmed = name.trim();
601
616
  if (!trimmed) return;
602
- createKey.mutate(trimmed, { onSuccess: (key) => {
617
+ createKey.mutate({
618
+ name: trimmed,
619
+ planSku: plan.planSku
620
+ }, { onSuccess: (key) => {
603
621
  setNewKey(key);
604
622
  setName("");
605
623
  } });
606
624
  };
625
+ const handleRevoke = (keyId) => {
626
+ setPendingRevokeId(keyId);
627
+ revokeKey.mutate(keyId, { onSettled: () => {
628
+ setPendingRevokeId(null);
629
+ setOpenRevokeId(null);
630
+ } });
631
+ };
607
632
  return /* @__PURE__ */ jsxs("article", {
608
633
  className: "rounded-xl border bg-card/60 p-6 space-y-5",
609
634
  children: [
@@ -611,9 +636,9 @@ const ApiKeysPanel = ({ readOnly = false }) => {
611
636
  className: "flex flex-wrap items-start justify-between gap-3",
612
637
  children: [/* @__PURE__ */ jsxs("div", {
613
638
  className: "space-y-1",
614
- children: [/* @__PURE__ */ jsx("h2", {
639
+ children: [/* @__PURE__ */ jsxs("h2", {
615
640
  className: "text-lg font-medium",
616
- children: "API keys"
641
+ children: [plan.displayName, " API keys"]
617
642
  }), /* @__PURE__ */ jsxs("p", {
618
643
  className: "text-sm text-muted-foreground",
619
644
  children: [
@@ -621,16 +646,16 @@ const ApiKeysPanel = ({ readOnly = false }) => {
621
646
  " ",
622
647
  /* @__PURE__ */ jsx("code", {
623
648
  className: "rounded bg-muted px-1 py-0.5 text-xs",
624
- children: data?.header ?? "Ocp-Apim-Subscription-Key"
649
+ children: header
625
650
  }),
626
651
  " ",
627
- "header when calling the gateway. Keys share your plan's rate limit and quota."
652
+ "header when calling this plan's endpoints. Keys share your developer key cap across all plans."
628
653
  ]
629
654
  })]
630
- }), data ? /* @__PURE__ */ jsx("span", {
655
+ }), /* @__PURE__ */ jsx("span", {
631
656
  className: "shrink-0 rounded-full border px-3 py-1 text-xs text-muted-foreground",
632
657
  children: capLabel
633
- }) : null]
658
+ })]
634
659
  }),
635
660
  newKey?.key ? /* @__PURE__ */ jsxs(Alert, { children: [
636
661
  /* @__PURE__ */ jsx(KeyRoundIcon, { size: 16 }),
@@ -650,72 +675,71 @@ const ApiKeysPanel = ({ readOnly = false }) => {
650
675
  ] }) : null,
651
676
  createKey.isError ? /* @__PURE__ */ jsx(Alert, {
652
677
  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 })
678
+ 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 })
654
679
  }) : null,
655
680
  revokeKey.isError ? /* @__PURE__ */ jsx(Alert, {
656
681
  variant: "destructive",
657
682
  children: /* @__PURE__ */ jsx(AlertTitle, { children: revokeKey.error.message })
658
683
  }) : 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", {
684
+ planActiveCount === 0 ? /* @__PURE__ */ jsx("p", {
668
685
  className: "text-sm text-muted-foreground",
669
- children: "You have no active API keys yet."
686
+ children: "You have no active API keys for this plan yet."
670
687
  }) : null,
671
688
  activeKeys.length > 0 ? /* @__PURE__ */ jsx("ul", {
672
689
  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"
690
+ children: activeKeys.map((key) => {
691
+ const isRevoking = pendingRevokeId === key.id;
692
+ return /* @__PURE__ */ jsxs("li", {
693
+ className: "flex flex-wrap items-center justify-between gap-3 p-4",
694
+ children: [/* @__PURE__ */ jsxs("div", {
695
+ className: "min-w-0 space-y-1",
696
+ children: [/* @__PURE__ */ jsx("p", {
697
+ className: "truncate font-medium",
698
+ children: key.name
699
+ }), /* @__PURE__ */ jsxs("p", {
700
+ className: "text-xs text-muted-foreground",
701
+ children: [
702
+ /* @__PURE__ */ jsxs("code", { children: [key.keyPrefix, "…"] }),
703
+ " · created",
704
+ " ",
705
+ new Date(key.createdAtUtc).toLocaleDateString()
706
+ ]
698
707
  })]
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))
708
+ }), readOnly ? null : /* @__PURE__ */ jsxs(Dialog, {
709
+ open: openRevokeId === key.id,
710
+ onOpenChange: (open) => {
711
+ if (isRevoking) return;
712
+ setOpenRevokeId(open ? key.id : null);
713
+ },
714
+ children: [/* @__PURE__ */ jsx(DialogTrigger, {
715
+ asChild: true,
716
+ children: /* @__PURE__ */ jsxs(Button$1, {
717
+ variant: "ghost",
718
+ className: "flex items-center gap-2",
719
+ disabled: isRevoking,
720
+ children: [isRevoking ? /* @__PURE__ */ jsx(Spinner, { size: 14 }) : /* @__PURE__ */ jsx(TrashIcon, { size: 16 }), /* @__PURE__ */ jsx("span", {
721
+ className: "hidden md:block",
722
+ children: isRevoking ? "Revoking…" : "Revoke"
723
+ })]
724
+ })
725
+ }), /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, { children: [
726
+ "Revoke “",
727
+ key.name,
728
+ "”?"
729
+ ] }), /* @__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, {
730
+ variant: "outline",
731
+ disabled: isRevoking,
732
+ onClick: () => setOpenRevokeId(null),
733
+ children: "Cancel"
734
+ }), /* @__PURE__ */ jsx(Button$1, {
735
+ variant: "destructive",
736
+ disabled: isRevoking,
737
+ onClick: () => handleRevoke(key.id),
738
+ children: isRevoking ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { size: 14 }), "Revoking…"] }) : "Revoke key"
739
+ })] })] })]
740
+ })]
741
+ }, key.id);
742
+ })
719
743
  }) : null,
720
744
  readOnly ? null : /* @__PURE__ */ jsxs("div", {
721
745
  className: "flex flex-wrap items-center gap-2",
@@ -723,7 +747,7 @@ const ApiKeysPanel = ({ readOnly = false }) => {
723
747
  className: "max-w-xs",
724
748
  maxLength: 64,
725
749
  placeholder: "Key name (e.g. production)",
726
- "aria-label": "New API key name",
750
+ "aria-label": `New API key name for ${plan.displayName}`,
727
751
  value: name,
728
752
  disabled: !canCreate || createKey.isPending,
729
753
  onChange: (event) => setName(event.target.value),
@@ -733,12 +757,13 @@ const ApiKeysPanel = ({ readOnly = false }) => {
733
757
  }), /* @__PURE__ */ jsx(Button$1, {
734
758
  onClick: handleCreate,
735
759
  disabled: !canCreate || createKey.isPending || !name.trim(),
736
- children: createKey.isPending ? "Creating…" : "Create API key"
760
+ className: "flex items-center gap-2",
761
+ children: createKey.isPending ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Spinner, { size: 14 }), "Creating…"] }) : "Create API key"
737
762
  })]
738
763
  }),
739
- data && !data.canCreate && !readOnly ? /* @__PURE__ */ jsx("p", {
764
+ plan && !plan.canCreate && !readOnly ? /* @__PURE__ */ jsx("p", {
740
765
  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.`
766
+ children: plan.maxKeys == null ? "API key creation is currently disabled for this API." : `You have reached your limit of ${plan.maxKeys} API keys across all plans. Revoke a key to create a new one.`
742
767
  }) : null
743
768
  ]
744
769
  });
@@ -965,9 +990,10 @@ const CredentialsPage = () => {
965
990
  const isPreview = useDevPortalPreviewMode();
966
991
  const isDemoSession = useIsDemoSession();
967
992
  const { data: credentials, isLoading, error } = useCredentials();
968
- const { data: catalog } = usePlans();
969
- const [revealed, setRevealed] = useState(false);
970
- const plan = catalog?.plans.find((item) => item.sku === credentials?.planSku);
993
+ const { data: apiKeys, isLoading: keysLoading, error: keysError } = useApiKeys();
994
+ const gatewayBaseUrl = credentials?.gatewayBaseUrl ?? apiKeys?.gatewayBaseUrl ?? null;
995
+ const header = apiKeys?.header ?? "Ocp-Apim-Subscription-Key";
996
+ const plans = apiKeys?.plans ?? [];
971
997
  return /* @__PURE__ */ jsxs("div", {
972
998
  className: "mx-auto max-w-screen-lg px-4 pb-16 pt-(--padding-content-top) lg:px-8",
973
999
  children: [
@@ -984,7 +1010,7 @@ const CredentialsPage = () => {
984
1010
  children: "API credentials"
985
1011
  }), /* @__PURE__ */ jsx("p", {
986
1012
  className: "text-muted-foreground",
987
- children: "Create API keys and use them to authenticate requests to the API gateway."
1013
+ children: "Create API keys for each subscribed plan and use them to authenticate requests to the API gateway."
988
1014
  })]
989
1015
  })]
990
1016
  }),
@@ -1003,7 +1029,7 @@ const CredentialsPage = () => {
1003
1029
  className: "rounded-xl border border-dashed p-8 text-center",
1004
1030
  children: [/* @__PURE__ */ jsx("p", {
1005
1031
  className: "text-muted-foreground",
1006
- children: "Sign in to view your subscription key."
1032
+ children: "Sign in to view your subscription keys."
1007
1033
  }), /* @__PURE__ */ jsx(Button$2, {
1008
1034
  className: "mt-4",
1009
1035
  asChild: true,
@@ -1013,15 +1039,15 @@ const CredentialsPage = () => {
1013
1039
  })
1014
1040
  })]
1015
1041
  }) : null,
1016
- !isPreview && !isDemoSession && auth.isAuthenticated && isLoading ? /* @__PURE__ */ jsx("p", {
1042
+ !isPreview && !isDemoSession && auth.isAuthenticated && (isLoading || keysLoading) ? /* @__PURE__ */ jsx("p", {
1017
1043
  className: "text-muted-foreground",
1018
1044
  children: "Loading credentials…"
1019
1045
  }) : null,
1020
- !isPreview && !isDemoSession && auth.isAuthenticated && error ? /* @__PURE__ */ jsxs("div", {
1046
+ !isPreview && !isDemoSession && auth.isAuthenticated && (error || keysError) ? /* @__PURE__ */ jsxs("div", {
1021
1047
  className: "rounded-xl border border-dashed p-8 text-center",
1022
1048
  children: [/* @__PURE__ */ jsx("p", {
1023
1049
  className: "text-muted-foreground",
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."
1050
+ children: error instanceof Error && error.message === "NO_ENTITLEMENT" || keysError instanceof Error && keysError.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."
1025
1051
  }), /* @__PURE__ */ jsx(Button$2, {
1026
1052
  className: "mt-4",
1027
1053
  asChild: true,
@@ -1031,66 +1057,44 @@ const CredentialsPage = () => {
1031
1057
  })
1032
1058
  })]
1033
1059
  }) : null,
1034
- (isDemoSession || !isPreview) && credentials ? /* @__PURE__ */ jsxs("div", {
1060
+ (isDemoSession || !isPreview) && auth.isAuthenticated && !error && !keysError && !isLoading && !keysLoading ? /* @__PURE__ */ jsxs("div", {
1035
1061
  className: "space-y-6",
1036
- children: [
1037
- /* @__PURE__ */ jsx("article", {
1038
- className: "rounded-xl border bg-card/60 p-6",
1039
- children: /* @__PURE__ */ jsxs("div", {
1040
- className: "space-y-1",
1041
- children: [/* @__PURE__ */ jsx("h2", {
1042
- className: "text-lg font-medium",
1043
- children: "Active plan"
1044
- }), /* @__PURE__ */ jsx("p", {
1045
- className: "text-sm text-muted-foreground",
1046
- children: plan?.displayName ?? credentials.planSku
1047
- })]
1062
+ children: [plans.length === 0 ? /* @__PURE__ */ jsxs("div", {
1063
+ className: "rounded-xl border border-dashed p-8 text-center",
1064
+ children: [/* @__PURE__ */ jsx("p", {
1065
+ className: "text-muted-foreground",
1066
+ children: "Choose a plan to get your first API key."
1067
+ }), /* @__PURE__ */ jsx(Button$2, {
1068
+ className: "mt-4",
1069
+ asChild: true,
1070
+ children: /* @__PURE__ */ jsx(Link, {
1071
+ to: "/account/plans",
1072
+ children: "View plans"
1048
1073
  })
1049
- }),
1050
- /* @__PURE__ */ jsx(ApiKeysPanel, { readOnly: isDemoSession }),
1051
- credentials.subscriptionKey ? /* @__PURE__ */ jsxs("article", {
1052
- className: "rounded-xl border bg-card/60 p-6 space-y-4",
1053
- children: [/* @__PURE__ */ jsxs("div", {
1054
- className: "space-y-1",
1055
- children: [/* @__PURE__ */ jsx("h2", {
1056
- className: "text-lg font-medium",
1057
- children: "Default key value"
1058
- }), /* @__PURE__ */ jsxs("p", {
1059
- className: "text-sm text-muted-foreground",
1060
- children: [
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."
1064
- ]
1065
- })]
1066
- }), /* @__PURE__ */ jsx(Secret, {
1067
- className: "max-w-full w-full",
1068
- secret: credentials.subscriptionKey,
1069
- status: "active",
1070
- revealed,
1071
- onReveal: setRevealed
1072
- })]
1073
- }) : null,
1074
- /* @__PURE__ */ jsxs("article", {
1075
- className: "rounded-xl border bg-card/60 p-6 space-y-4",
1076
- children: [/* @__PURE__ */ jsxs("div", {
1077
- className: "space-y-1",
1078
- children: [/* @__PURE__ */ jsx("h2", {
1079
- className: "text-lg font-medium",
1080
- children: "Gateway URL"
1081
- }), /* @__PURE__ */ jsx("p", {
1082
- className: "text-sm text-muted-foreground",
1083
- children: "Base URL for API requests through the gateway."
1084
- })]
1085
- }), credentials.gatewayBaseUrl ? /* @__PURE__ */ jsx(Value, {
1086
- className: "max-w-full w-full",
1087
- value: credentials.gatewayBaseUrl
1088
- }) : /* @__PURE__ */ jsx("p", {
1074
+ })]
1075
+ }) : plans.map((plan) => /* @__PURE__ */ jsx(ApiKeysPanel, {
1076
+ plan,
1077
+ header,
1078
+ readOnly: isDemoSession
1079
+ }, plan.planSku)), /* @__PURE__ */ jsxs("article", {
1080
+ className: "rounded-xl border bg-card/60 p-6 space-y-4",
1081
+ children: [/* @__PURE__ */ jsxs("div", {
1082
+ className: "space-y-1",
1083
+ children: [/* @__PURE__ */ jsx("h2", {
1084
+ className: "text-lg font-medium",
1085
+ children: "Gateway URL"
1086
+ }), /* @__PURE__ */ jsx("p", {
1089
1087
  className: "text-sm text-muted-foreground",
1090
- children: "Available after your API is deployed to the gateway."
1088
+ children: "Base URL for API requests through the gateway."
1091
1089
  })]
1092
- })
1093
- ]
1090
+ }), gatewayBaseUrl ? /* @__PURE__ */ jsx(Value, {
1091
+ className: "max-w-full w-full",
1092
+ value: gatewayBaseUrl
1093
+ }) : /* @__PURE__ */ jsx("p", {
1094
+ className: "text-sm text-muted-foreground",
1095
+ children: "Available after your API is deployed to the gateway."
1096
+ })]
1097
+ })]
1094
1098
  }) : null,
1095
1099
  /* @__PURE__ */ jsxs("div", {
1096
1100
  className: "mt-8 flex flex-wrap gap-3",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo-plugin-dev-portal-billing",
3
- "version": "0.1.72",
3
+ "version": "0.1.74",
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.72"
49
+ "@lukoweb/apitogo": "0.1.74"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "react": ">=19.2.0",