@lukoweb/apitogo-plugin-dev-portal-billing 0.2.53 → 0.2.56

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.
Files changed (26) hide show
  1. package/dist/{AccountPlansPage-DvRFe3di.mjs → AccountPlansPage-BuM7fe82.mjs} +14 -10
  2. package/dist/{CheckoutPage-CwSElFUk.mjs → CheckoutPage-BSiADcI6.mjs} +32 -15
  3. package/dist/{ContactSalesPage-q7Dxxtx6.mjs → ContactSalesPage-BqqDmorF.mjs} +4 -11
  4. package/dist/{CredentialsPage-B2jPo6Go.mjs → CredentialsPage-DjNvBBLb.mjs} +28 -19
  5. package/dist/{PricingCard-CKdq1oWn.mjs → PricingCard-DFuQEw7Y.mjs} +10 -4
  6. package/dist/{PricingPage-DnQQfy9L.mjs → PricingPage-SpL81vpd.mjs} +14 -13
  7. package/dist/SubscriptionsPage-CwsBtNld.mjs +274 -0
  8. package/dist/{UsagePage-BF4QgrTc.mjs → UsagePage-Dw5Uepg3.mjs} +61 -19
  9. package/dist/{account-links-D_vS0GJn.mjs → account-links-Drofs-f-.mjs} +1 -1
  10. package/dist/{billing-state-DbU5Lpoz.mjs → billing-state-DBQR2tad.mjs} +1 -1
  11. package/dist/index.d.mts +3 -2
  12. package/dist/index.mjs +25 -23
  13. package/dist/{key-plan-note-DTWeFhn2.mjs → page-state-BLL2JJBc.mjs} +22 -2
  14. package/dist/paths-DaVyTIDJ.mjs +14 -0
  15. package/dist/pending-checkout-DpVaNnTu.mjs +45 -0
  16. package/dist/problem-details-n20-dy3w.mjs +18 -0
  17. package/dist/{useApiKeys-CVgXfUop.mjs → useApiKeys-BOY3vIOr.mjs} +3 -9
  18. package/dist/{useDevPortalApiBaseUrl-BstWzITX.mjs → useDevPortalApiBaseUrl-BiI2jFjU.mjs} +14 -14
  19. package/dist/{useMySubscriptions-BtxAXA1m.mjs → useMySubscriptions-BYVIIWbe.mjs} +2 -2
  20. package/dist/{usePlans-DPGFWcVo.mjs → usePlans-HyhIIuJZ.mjs} +1 -1
  21. package/dist/{useUsage--5sZub3M.mjs → useUsage-B5x9YuKa.mjs} +1 -1
  22. package/package.json +3 -3
  23. package/dist/SubscriptionsPage-msO31jv-.mjs +0 -223
  24. package/dist/paths-ujy9wjVK.mjs +0 -12
  25. /package/dist/{demo-session-rN2Gj4lM.mjs → demo-session-yK9qEqV-.mjs} +0 -0
  26. /package/dist/{pricing-config-02m4dyFf.mjs → pricing-config-Bc6eWFUO.mjs} +0 -0
@@ -1,12 +1,14 @@
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";
1
+ import { f as parseManifestPlans, n as useDevPortalPreviewMode, r as useIsDemoSession, s as isActiveSubscription, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
2
+ import { t as PricingCard } from "./PricingCard-DFuQEw7Y.mjs";
3
3
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
- import { t as useMySubscriptions } from "./useMySubscriptions-BtxAXA1m.mjs";
5
- import { t as usePlans } from "./usePlans-DPGFWcVo.mjs";
4
+ import { n as readProblemDetail } from "./problem-details-n20-dy3w.mjs";
5
+ import { t as useMySubscriptions } from "./useMySubscriptions-BYVIIWbe.mjs";
6
+ import { t as usePlans } from "./usePlans-HyhIIuJZ.mjs";
7
+ import { n as readPendingCheckout, t as clearPendingCheckout } from "./pending-checkout-DpVaNnTu.mjs";
6
8
  import { Head } from "@lukoweb/apitogo/components";
7
9
  import { CreditCardIcon } from "@lukoweb/apitogo/icons";
8
10
  import { useSearchParams } from "@lukoweb/apitogo/router";
9
- import { useState } from "react";
11
+ import { useEffect, useState } from "react";
10
12
  import { jsx, jsxs } from "react/jsx-runtime";
11
13
  import { useMutation, useQueryClient } from "@lukoweb/apitogo/react-query";
12
14
 
@@ -43,10 +45,7 @@ function useCancelSubscription() {
43
45
  credentials: "include",
44
46
  body: JSON.stringify({ planSku })
45
47
  });
46
- if (!response.ok) {
47
- const detail = await response.text();
48
- throw new Error(detail || "Failed to cancel subscription.");
49
- }
48
+ if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Failed to cancel subscription.");
50
49
  return await response.json();
51
50
  },
52
51
  onSuccess: async () => {
@@ -72,6 +71,10 @@ const AccountPlansPage = () => {
72
71
  const cancelMutation = useCancelSubscription();
73
72
  const [cancelingSku, setCancelingSku] = useState(null);
74
73
  const subscriptionBySku = new Map((me?.subscriptions ?? []).map((sub) => [sub.planSku, sub]));
74
+ useEffect(() => {
75
+ if (checkoutCancelled) clearPendingCheckout();
76
+ }, [checkoutCancelled]);
77
+ const pendingSku = checkoutCancelled ? void 0 : readPendingCheckout();
75
78
  const handleUnsubscribe = async (planSku) => {
76
79
  setCancelingSku(planSku);
77
80
  try {
@@ -135,7 +138,8 @@ const AccountPlansPage = () => {
135
138
  isSubscribed,
136
139
  onUnsubscribe: isPreview || isDemoSession ? void 0 : handleUnsubscribe,
137
140
  isCanceling: cancelingSku === plan.sku,
138
- cancelDisabled: isPreview || isDemoSession
141
+ cancelDisabled: isPreview || isDemoSession,
142
+ isConfirming: !isSubscribed && pendingSku === plan.sku
139
143
  }, plan.sku);
140
144
  })
141
145
  }) : !isLoading && !isError ? /* @__PURE__ */ jsx("div", {
@@ -1,6 +1,8 @@
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-BstWzITX.mjs";
3
- import { t as FREE_CHECKOUT_MARKER } from "./billing-state-DbU5Lpoz.mjs";
1
+ import { n as ALREADY_SUBSCRIBED_PARAM, s as PRICING_PATH, t as ACCOUNT_PLANS_PATH } from "./paths-DaVyTIDJ.mjs";
2
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
3
+ import { n as readProblemDetail, t as isAlreadySubscribedMessage } from "./problem-details-n20-dy3w.mjs";
4
+ import { r as rememberPendingCheckout } from "./pending-checkout-DpVaNnTu.mjs";
5
+ import { t as FREE_CHECKOUT_MARKER } from "./billing-state-DBQR2tad.mjs";
4
6
  import { ShieldIcon } from "@lukoweb/apitogo/icons";
5
7
  import { Link, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
6
8
  import { useEffect } from "react";
@@ -81,14 +83,23 @@ const CheckoutPage = () => {
81
83
  credentials: "include",
82
84
  body: JSON.stringify({ planSku })
83
85
  });
84
- if (!response.ok) {
85
- const detail = await response.text();
86
- throw new Error(detail || "Checkout failed.");
87
- }
86
+ if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Checkout failed.");
88
87
  return await response.json();
89
88
  }
90
89
  });
91
90
  const freeProvisioned = checkoutQuery.data?.checkoutUrl == null && checkoutQuery.data?.entitlementProvisioned === true;
91
+ const checkoutUrl = checkoutQuery.data?.checkoutUrl;
92
+ useEffect(() => {
93
+ if (checkoutUrl) rememberPendingCheckout(planSku);
94
+ }, [checkoutUrl, planSku]);
95
+ const alreadySubscribed = checkoutQuery.isError && isAlreadySubscribedMessage(checkoutQuery.error.message);
96
+ useEffect(() => {
97
+ if (alreadySubscribed) navigate(`/account/subscriptions?${ALREADY_SUBSCRIBED_PARAM}=${encodeURIComponent(planSku)}`, { replace: true });
98
+ }, [
99
+ alreadySubscribed,
100
+ navigate,
101
+ planSku
102
+ ]);
92
103
  useEffect(() => {
93
104
  if (freeProvisioned) navigate(`/account/subscriptions?checkout=success&${FREE_CHECKOUT_MARKER}`, { replace: true });
94
105
  }, [freeProvisioned, navigate]);
@@ -146,13 +157,13 @@ const CheckoutPage = () => {
146
157
  })
147
158
  ]
148
159
  });
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, {
160
+ if (freeProvisioned || alreadySubscribed) return null;
161
+ if (checkoutQuery.isError) return /* @__PURE__ */ jsxs("div", {
162
+ className: "mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center",
163
+ children: [/* @__PURE__ */ jsx("h1", {
164
+ className: "text-2xl font-semibold tracking-tight",
165
+ children: "Checkout could not start"
166
+ }), /* @__PURE__ */ jsxs(Alert, {
156
167
  variant: "destructive",
157
168
  children: [/* @__PURE__ */ jsx(AlertDescription, {
158
169
  className: "first-letter:uppercase",
@@ -166,7 +177,13 @@ const CheckoutPage = () => {
166
177
  children: "Back to plans"
167
178
  })
168
179
  }) })]
169
- }) : null
180
+ })]
181
+ });
182
+ return /* @__PURE__ */ jsx(RedirectPage, {
183
+ icon: ShieldIcon,
184
+ title: "Establishing secure checkout…",
185
+ description: "Redirecting you to complete your subscription.",
186
+ url: checkoutQuery.data?.checkoutUrl ?? void 0
170
187
  });
171
188
  };
172
189
 
@@ -1,7 +1,8 @@
1
- import { o as PRICING_PATH } from "./paths-ujy9wjVK.mjs";
2
- import { l as isPlanCustomPriced, n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
1
+ import { s as PRICING_PATH } from "./paths-DaVyTIDJ.mjs";
2
+ import { l as isPlanCustomPriced, n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
3
3
  import "./DevPortalBillingContext-CR83s4LO.mjs";
4
- import { t as usePlans } from "./usePlans-DPGFWcVo.mjs";
4
+ import { n as readProblemDetail } from "./problem-details-n20-dy3w.mjs";
5
+ import { t as usePlans } from "./usePlans-HyhIIuJZ.mjs";
5
6
  import { Link, useSearchParams } from "@lukoweb/apitogo/router";
6
7
  import { useState } from "react";
7
8
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -32,14 +33,6 @@ function useSubmitPlanInquiry() {
32
33
  if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Your message could not be sent. Please try again later.");
33
34
  } });
34
35
  }
35
- async function readProblemDetail(response) {
36
- try {
37
- const body = await response.json();
38
- return (body.errors ? Object.values(body.errors).flat()[0] : void 0) ?? body.detail ?? body.title ?? null;
39
- } catch {
40
- return null;
41
- }
42
- }
43
36
 
44
37
  //#endregion
45
38
  //#region src/pages/ContactSalesPage.tsx
@@ -1,10 +1,10 @@
1
- import { t as ACCOUNT_PLANS_PATH } from "./paths-ujy9wjVK.mjs";
2
- import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
1
+ import { t as ACCOUNT_PLANS_PATH } from "./paths-DaVyTIDJ.mjs";
2
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
3
3
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
- import { r as createDemoCredentials, t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-rN2Gj4lM.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";
4
+ import { r as createDemoCredentials, t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-yK9qEqV-.mjs";
5
+ import { n as useApiKeys } from "./useApiKeys-BOY3vIOr.mjs";
6
+ import { n as apiReferencePath, r as ApiKeysPanel } from "./account-links-Drofs-f-.mjs";
7
+ import { i as ChoosePlanNotice, r as keyPlanNote, t as apiKeysPageState } from "./page-state-BLL2JJBc.mjs";
8
8
  import { Head } from "@lukoweb/apitogo/components";
9
9
  import { KeyRoundIcon } from "@lukoweb/apitogo/icons";
10
10
  import { Link as Link$1 } from "@lukoweb/apitogo/router";
@@ -57,18 +57,23 @@ const CredentialsPage = () => {
57
57
  const auth = useAuth();
58
58
  const isPreview = useDevPortalPreviewMode();
59
59
  const isDemoSession = useIsDemoSession();
60
- const { data: credentials, isLoading, error } = useCredentials();
61
- const { data: apiKeys, isLoading: keysLoading, error: keysError } = useApiKeys();
60
+ const { data: credentials } = useCredentials();
61
+ const { data: apiKeys, error: keysError } = useApiKeys();
62
62
  const { gatewayBaseUrl: configuredGatewayBaseUrl, links, keyHeader: configuredKeyHeader } = useDevPortalBillingConfig();
63
63
  const apiReferenceLink = apiReferencePath(links);
64
64
  const gatewayBaseUrl = credentials?.gatewayBaseUrl?.trim() || apiKeys?.gatewayBaseUrl?.trim() || configuredGatewayBaseUrl?.trim() || zudokuConfig.__meta?.gatewayBaseUrl?.trim() || null;
65
65
  const header = configuredKeyHeader?.trim() || apiKeys?.header || DEMO_GATEWAY_KEY_HEADER;
66
66
  const plans = apiKeys?.plans ?? [];
67
67
  const planNote = isDemoSession ? null : keyPlanNote(plans);
68
+ const pageState = apiKeysPageState({
69
+ data: apiKeys,
70
+ error: keysError
71
+ });
72
+ const showsLiveData = auth.isAuthenticated && (isDemoSession || !isPreview);
68
73
  return /* @__PURE__ */ jsxs("div", {
69
74
  className: "mx-auto max-w-screen-lg px-4 pb-16 pt-(--padding-content-top) lg:px-8",
70
75
  children: [
71
- /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "API credentials" }) }),
76
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "API keys" }) }),
72
77
  /* @__PURE__ */ jsxs("div", {
73
78
  className: "mb-8 flex items-start gap-4",
74
79
  children: [/* @__PURE__ */ jsx("div", {
@@ -78,7 +83,7 @@ const CredentialsPage = () => {
78
83
  className: "space-y-1",
79
84
  children: [/* @__PURE__ */ jsx("h1", {
80
85
  className: "text-3xl font-semibold tracking-tight",
81
- children: "API credentials"
86
+ children: "API keys"
82
87
  }), /* @__PURE__ */ jsx("p", {
83
88
  className: "text-muted-foreground",
84
89
  children: "Create API keys for each subscribed plan and use them to authenticate requests to the API gateway."
@@ -93,7 +98,7 @@ const CredentialsPage = () => {
93
98
  className: "rounded-xl border border-dashed p-8 text-center",
94
99
  children: /* @__PURE__ */ jsx("p", {
95
100
  className: "text-muted-foreground",
96
- children: "API credentials are available after the developer portal is published."
101
+ children: "API keys are available after the developer portal is published."
97
102
  })
98
103
  }) : null,
99
104
  !isPreview && !isDemoSession && !auth.isAuthenticated ? /* @__PURE__ */ jsxs("div", {
@@ -110,15 +115,18 @@ const CredentialsPage = () => {
110
115
  })
111
116
  })]
112
117
  }) : null,
113
- !isPreview && !isDemoSession && auth.isAuthenticated && (isLoading || keysLoading) ? /* @__PURE__ */ jsx("p", {
114
- className: "text-muted-foreground",
115
- children: "Loading credentials…"
118
+ showsLiveData && pageState === "loading" ? /* @__PURE__ */ jsx("div", {
119
+ className: "space-y-6",
120
+ role: "status",
121
+ "aria-busy": "true",
122
+ "aria-label": "Loading API keys",
123
+ children: [0, 1].map((i) => /* @__PURE__ */ jsx("div", { className: "h-40 animate-pulse rounded-xl border bg-muted/40" }, i))
116
124
  }) : null,
117
- !isPreview && !isDemoSession && auth.isAuthenticated && (error || keysError) ? error instanceof Error && error.message === "NO_ENTITLEMENT" || keysError instanceof Error && keysError.message === "NO_ENTITLEMENT" ? /* @__PURE__ */ jsx(ChoosePlanNotice, {}) : /* @__PURE__ */ jsxs("div", {
125
+ showsLiveData && pageState === "failed" ? /* @__PURE__ */ jsxs("div", {
118
126
  className: "rounded-xl border border-dashed p-8 text-center",
119
127
  children: [/* @__PURE__ */ jsx("p", {
120
128
  className: "text-muted-foreground",
121
- children: "Unable to load API credentials. Try again in a moment."
129
+ children: "Unable to load your API keys. Try again in a moment."
122
130
  }), /* @__PURE__ */ jsx(Button$1, {
123
131
  className: "mt-4",
124
132
  asChild: true,
@@ -128,7 +136,8 @@ const CredentialsPage = () => {
128
136
  })
129
137
  })]
130
138
  }) : null,
131
- (isDemoSession || !isPreview) && auth.isAuthenticated && !error && !keysError && !isLoading && !keysLoading ? /* @__PURE__ */ jsxs("div", {
139
+ showsLiveData && pageState === "empty" ? /* @__PURE__ */ jsx(ChoosePlanNotice, {}) : null,
140
+ showsLiveData && pageState === "ready" ? /* @__PURE__ */ jsxs("div", {
132
141
  className: "space-y-6",
133
142
  children: [
134
143
  planNote ? /* @__PURE__ */ jsx("div", {
@@ -136,7 +145,7 @@ const CredentialsPage = () => {
136
145
  className: "rounded-xl border border-primary/30 bg-primary/5 p-4 text-sm",
137
146
  children: planNote.text
138
147
  }) : null,
139
- plans.length === 0 ? /* @__PURE__ */ jsx(ChoosePlanNotice, {}) : plans.map((plan) => /* @__PURE__ */ jsx(ApiKeysPanel, {
148
+ plans.map((plan) => /* @__PURE__ */ jsx(ApiKeysPanel, {
140
149
  plan,
141
150
  header,
142
151
  readOnly: isDemoSession
@@ -169,7 +178,7 @@ const CredentialsPage = () => {
169
178
  asChild: true,
170
179
  children: /* @__PURE__ */ jsx(Link$1, {
171
180
  to: "/account/subscriptions",
172
- children: "My subscriptions"
181
+ children: "Billing"
173
182
  })
174
183
  }), apiReferenceLink ? /* @__PURE__ */ jsx(Button$1, {
175
184
  variant: "outline",
@@ -1,5 +1,5 @@
1
- import { n as CHECKOUT_PATH, r as CONTACT_SALES_PATH } from "./paths-ujy9wjVK.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";
1
+ import { i as CONTACT_SALES_PATH, r as CHECKOUT_PATH } from "./paths-DaVyTIDJ.mjs";
2
+ import { d as listPlanFeatureLines, i as formatPlanDisplayPrice, l as isPlanCustomPriced, o as formatPlanPriceUnit, s as isActiveSubscription, u as isPlanFree } from "./useDevPortalApiBaseUrl-BiI2jFjU.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";
@@ -18,7 +18,7 @@ const accountCtaLabel = (plan) => {
18
18
  };
19
19
  /** Custom-priced plans skip checkout and open the contact-sales form. */
20
20
  const planCtaPath = (plan) => `${isPlanCustomPriced(plan) ? CONTACT_SALES_PATH : CHECKOUT_PATH}?planSku=${encodeURIComponent(plan.sku)}`;
21
- const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, onSwitch, isSwitching = false, isCanceling = false, cancelDisabled = false, featured = false }) => {
21
+ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, onSwitch, isSwitching = false, isCanceling = false, cancelDisabled = false, featured = false, isConfirming = false }) => {
22
22
  const isActive = isSubscribed || isActiveSubscription(subscriptionStatus ?? "");
23
23
  const features = listPlanFeatureLines(plan);
24
24
  const displayPrice = formatPlanDisplayPrice(plan);
@@ -83,7 +83,13 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
83
83
  className: "text-center text-xs text-muted-foreground",
84
84
  children: "Unsubscribe is available after the portal is published."
85
85
  }) : null
86
- ] }) : onSwitch && !isPlanCustomPriced(plan) ? /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
86
+ ] }) : isConfirming ? /* @__PURE__ */ jsx(Button, {
87
+ variant: "secondary",
88
+ className: "w-full",
89
+ disabled: true,
90
+ "data-testid": `confirming-${plan.sku}`,
91
+ children: "Confirming payment…"
92
+ }) : onSwitch && !isPlanCustomPriced(plan) ? /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
87
93
  asChild: true,
88
94
  children: /* @__PURE__ */ jsx(Button, {
89
95
  variant: featured ? "default" : "outline",
@@ -1,9 +1,9 @@
1
- import { f as parseManifestPlans, h as pickFeaturedPlanSku, n as useDevPortalPreviewMode } from "./useDevPortalApiBaseUrl-BstWzITX.mjs";
2
- import { t as PricingCard } from "./PricingCard-CKdq1oWn.mjs";
1
+ import { f as parseManifestPlans, h as pickFeaturedPlanSku, n as useDevPortalPreviewMode } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
2
+ import { t as PricingCard } from "./PricingCard-DFuQEw7Y.mjs";
3
3
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.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";
4
+ import { t as usePlans } from "./usePlans-HyhIIuJZ.mjs";
5
+ import { t as isPreviewPricingEnabled } from "./pricing-config-Bc6eWFUO.mjs";
6
+ import { a as isPricingLoading, n as buildCompareRows } from "./billing-state-DBQR2tad.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";
@@ -66,15 +66,16 @@ const DEFAULT_COMPARE_ROWS = [
66
66
  ]
67
67
  }
68
68
  ];
69
+ /** Only what the platform itself does, so the defaults never promise anything for the tenant (API-124). */
69
70
  const DEFAULT_FAQS = [{
70
- question: "What happens if I exceed my quota?",
71
- answer: "Requests over your monthly quota return a 429 response so you can test rate-limit handling instead of being cut off unexpectedly."
71
+ question: "What happens if I go over my plan's limits?",
72
+ answer: "Calls over the rate limit or the quota are refused with a 429 response, and its Retry-After header says when to try again."
72
73
  }, {
73
- question: "Can I change plans later?",
74
- answer: "Yes, upgrade or downgrade any time from your billing page. Changes apply on your next billing cycle."
74
+ question: "Can I have more than one plan?",
75
+ answer: "Yes. Each plan has its own API keys and limits: create a key for a plan on the API keys page and use it in your app."
75
76
  }];
76
77
  const PricingPage = () => {
77
- const { pricing, previewPlans } = useDevPortalBillingConfig();
78
+ const { pricing, previewPlans, featuredPlanSku } = useDevPortalBillingConfig();
78
79
  const isPreview = useDevPortalPreviewMode();
79
80
  const pricingEnabled = isPreviewPricingEnabled() || isPreview;
80
81
  const livePlans = usePlans();
@@ -92,10 +93,10 @@ const PricingPage = () => {
92
93
  to: "/",
93
94
  replace: true
94
95
  });
95
- const title = pricing?.title ?? "Pay for what you call";
96
- const subtitle = pricing?.subtitle ?? "Every plan includes the full endpoint catalog. Higher tiers raise your request quota, rate limit, and support level.";
96
+ const title = pricing?.title ?? "Plans and pricing";
97
+ const subtitle = pricing?.subtitle ?? "Choose the plan that fits how you use the API. Each plan lists its limits and what it includes.";
97
98
  const plans = catalog?.plans ?? [];
98
- const featuredSku = pickFeaturedPlanSku(plans);
99
+ const featuredSku = pickFeaturedPlanSku(plans, featuredPlanSku);
99
100
  const compareRows = buildCompareRows({
100
101
  plans,
101
102
  configuredRows: pricing?.compareRows,
@@ -0,0 +1,274 @@
1
+ import { a as CREDENTIALS_PATH, n as ALREADY_SUBSCRIBED_PARAM, t as ACCOUNT_PLANS_PATH } from "./paths-DaVyTIDJ.mjs";
2
+ import { a as formatPlanPrice, p as parseMonthlyQuotaCalls, s as isActiveSubscription, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
3
+ import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
+ import { t as useMySubscriptions } from "./useMySubscriptions-BYVIIWbe.mjs";
5
+ import { t as usePlans } from "./usePlans-HyhIIuJZ.mjs";
6
+ import { n as readPendingCheckout, t as clearPendingCheckout } from "./pending-checkout-DpVaNnTu.mjs";
7
+ import { n as useApiKeys } from "./useApiKeys-BOY3vIOr.mjs";
8
+ import { t as useUsage } from "./useUsage-B5x9YuKa.mjs";
9
+ import { i as hasPaymentProvider, r as checkoutConfirmingText } from "./billing-state-DBQR2tad.mjs";
10
+ import { i as ChoosePlanNotice, n as billingPageState, r as keyPlanNote } from "./page-state-BLL2JJBc.mjs";
11
+ import { Head, Spinner } from "@lukoweb/apitogo/components";
12
+ import { Link as Link$1, useSearchParams } from "@lukoweb/apitogo/router";
13
+ import { useEffect } from "react";
14
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
+ import { useMutation } from "@lukoweb/apitogo/react-query";
16
+ import { useAuth } from "@lukoweb/apitogo/hooks";
17
+ import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
18
+
19
+ //#region src/hooks/useBillingPortal.ts
20
+ function useBillingPortal() {
21
+ const apiBaseUrl = useDevPortalApiBaseUrl();
22
+ return useMutation({ mutationFn: async (returnUrl) => {
23
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
24
+ const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/billing-portal`, {
25
+ method: "POST",
26
+ headers: {
27
+ Accept: "application/json",
28
+ "Content-Type": "application/json"
29
+ },
30
+ credentials: "include",
31
+ body: JSON.stringify({ returnUrl: returnUrl ?? `${window.location.origin}/account/subscriptions` })
32
+ });
33
+ if (!response.ok) throw new Error("Unable to open billing portal.");
34
+ const body = await response.json();
35
+ if (!body.url) throw new Error("Billing portal URL was not returned.");
36
+ window.location.assign(body.url);
37
+ } });
38
+ }
39
+
40
+ //#endregion
41
+ //#region src/pages/SubscriptionsPage.tsx
42
+ /** How an earlier plan ended, in words a customer reads. */
43
+ function subscriptionStatusLabel(status) {
44
+ switch (status.toLowerCase().replace(/_/g, "")) {
45
+ case "canceled": return "Ended";
46
+ case "pastdue": return "Payment due";
47
+ case "incomplete": return "Not started";
48
+ default: return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
49
+ }
50
+ }
51
+ const SubscriptionsPage = () => {
52
+ const { billing } = useDevPortalBillingConfig();
53
+ const auth = useAuth();
54
+ const [searchParams] = useSearchParams();
55
+ const checkoutSuccess = searchParams.get("checkout") === "success";
56
+ const alreadySubscribedSku = searchParams.get(ALREADY_SUBSCRIBED_PARAM);
57
+ const checkoutPaid = searchParams.get("paid") !== "0";
58
+ const { data: me, error: meError, refetch } = useMySubscriptions();
59
+ const billingState = billingPageState({
60
+ data: me,
61
+ error: meError
62
+ });
63
+ const isLoading = billingState === "loading";
64
+ const { data: catalog } = usePlans();
65
+ const billingPortal = useBillingPortal();
66
+ const { data: usage } = useUsage("30d");
67
+ const { data: apiKeys } = useApiKeys();
68
+ const planNote = keyPlanNote(apiKeys?.plans ?? []);
69
+ const planBySku = new Map((catalog?.plans ?? []).map((plan) => [plan.sku, plan]));
70
+ const subscriptions = me?.subscriptions ?? [];
71
+ const activeSubscriptions = subscriptions.filter((sub) => isActiveSubscription(sub.status));
72
+ const earlierSubscriptions = subscriptions.filter((sub) => !isActiveSubscription(sub.status));
73
+ const hasActivePlan = activeSubscriptions.length > 0;
74
+ const singlePlan = activeSubscriptions.length === 1 ? planBySku.get(activeSubscriptions[0].planSku) : void 0;
75
+ const pendingSku = checkoutSuccess ? readPendingCheckout() : void 0;
76
+ const pendingActive = pendingSku !== void 0 && activeSubscriptions.some((sub) => sub.planSku === pendingSku);
77
+ const confirmingPayment = checkoutSuccess && !isLoading && (pendingSku !== void 0 ? !pendingActive : !hasActivePlan);
78
+ useEffect(() => {
79
+ if (pendingActive) clearPendingCheckout();
80
+ }, [pendingActive]);
81
+ useEffect(() => {
82
+ if (!confirmingPayment) return;
83
+ let attempts = 0;
84
+ const timer = setInterval(() => {
85
+ attempts += 1;
86
+ refetch();
87
+ if (attempts >= 30) clearInterval(timer);
88
+ }, 2e3);
89
+ return () => clearInterval(timer);
90
+ }, [confirmingPayment, refetch]);
91
+ const monthlyQuota = singlePlan ? parseMonthlyQuotaCalls(singlePlan.limitsJson) : null;
92
+ const usedRequests = usage?.requestCount ?? 0;
93
+ const quotaPct = monthlyQuota && monthlyQuota > 0 ? Math.min(100, Math.round(usedRequests / monthlyQuota * 100)) : null;
94
+ return /* @__PURE__ */ jsxs("div", {
95
+ className: "px-8 py-7",
96
+ children: [
97
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Billing" }) }),
98
+ /* @__PURE__ */ jsx("h1", {
99
+ className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
100
+ children: "Billing"
101
+ }),
102
+ /* @__PURE__ */ jsx("p", {
103
+ className: "mb-8 text-muted-foreground",
104
+ children: "Manage your plans, payment method, and invoices."
105
+ }),
106
+ alreadySubscribedSku ? /* @__PURE__ */ jsxs("div", {
107
+ role: "status",
108
+ className: "mb-6 rounded-xl border border-primary/30 bg-primary/5 p-4 text-sm",
109
+ children: [
110
+ "You're already on the",
111
+ " ",
112
+ planBySku.get(alreadySubscribedSku)?.displayName ?? alreadySubscribedSku,
113
+ " ",
114
+ "plan. Go to",
115
+ " ",
116
+ /* @__PURE__ */ jsx(Link$1, {
117
+ to: CREDENTIALS_PATH,
118
+ className: "font-medium underline",
119
+ children: "API keys"
120
+ }),
121
+ " ",
122
+ "to create a key."
123
+ ]
124
+ }) : null,
125
+ confirmingPayment ? /* @__PURE__ */ jsx("div", {
126
+ className: "mb-6 rounded-xl border border-border bg-card p-4 text-sm",
127
+ children: checkoutConfirmingText(checkoutPaid)
128
+ }) : null,
129
+ checkoutSuccess && hasActivePlan && !confirmingPayment ? /* @__PURE__ */ jsx("div", {
130
+ className: "mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-sm",
131
+ children: planNote ? /* @__PURE__ */ jsxs(Fragment, { children: [
132
+ planNote.text,
133
+ " ",
134
+ /* @__PURE__ */ jsx(Link$1, {
135
+ to: CREDENTIALS_PATH,
136
+ className: "font-medium underline",
137
+ children: "Create the key"
138
+ }),
139
+ "."
140
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
141
+ "Your subscription is active.",
142
+ " ",
143
+ /* @__PURE__ */ jsx(Link$1, {
144
+ to: CREDENTIALS_PATH,
145
+ className: "font-medium underline",
146
+ children: "View your API keys"
147
+ }),
148
+ "."
149
+ ] })
150
+ }) : null,
151
+ isLoading ? /* @__PURE__ */ jsx("div", {
152
+ className: "mb-8 h-40 animate-pulse rounded-xl border bg-muted/40",
153
+ role: "status",
154
+ "aria-busy": "true",
155
+ "aria-label": "Loading billing details"
156
+ }) : null,
157
+ billingState === "failed" ? /* @__PURE__ */ jsx("p", {
158
+ className: "mb-8 text-muted-foreground",
159
+ children: "Unable to load your plans. Try again in a moment."
160
+ }) : null,
161
+ !isLoading && hasActivePlan ? /* @__PURE__ */ jsxs("section", {
162
+ className: "mb-8",
163
+ children: [/* @__PURE__ */ jsx("p", {
164
+ className: "mb-3 text-xs font-medium uppercase tracking-[0.05em] text-muted-foreground",
165
+ children: activeSubscriptions.length > 1 ? "Your plans" : "Current plan"
166
+ }), /* @__PURE__ */ jsx("div", {
167
+ className: "space-y-4",
168
+ children: activeSubscriptions.map((subscription) => {
169
+ const plan = planBySku.get(subscription.planSku);
170
+ return /* @__PURE__ */ jsxs("article", {
171
+ className: "rounded-xl border border-border bg-card p-6",
172
+ "data-testid": `active-plan-${subscription.planSku}`,
173
+ children: [
174
+ /* @__PURE__ */ jsx("h2", {
175
+ className: "text-2xl font-bold",
176
+ children: plan?.displayName ?? subscription.planSku
177
+ }),
178
+ plan ? /* @__PURE__ */ jsx("p", {
179
+ className: "mt-1 text-sm text-muted-foreground",
180
+ children: formatPlanPrice(plan)
181
+ }) : /* @__PURE__ */ jsx("p", {
182
+ className: "mt-1 text-sm text-muted-foreground",
183
+ children: "This plan is no longer offered. Your access continues until you cancel it."
184
+ }),
185
+ subscription.currentPeriodEnd ? /* @__PURE__ */ jsxs("p", {
186
+ className: "mt-3 text-sm text-muted-foreground",
187
+ children: [
188
+ "Renews",
189
+ " ",
190
+ new Date(subscription.currentPeriodEnd).toLocaleDateString()
191
+ ]
192
+ }) : null,
193
+ singlePlan && quotaPct != null && monthlyQuota != null ? /* @__PURE__ */ jsxs("div", {
194
+ className: "mt-5",
195
+ children: [/* @__PURE__ */ jsxs("div", {
196
+ className: "mb-1.5 flex justify-between text-xs text-muted-foreground",
197
+ children: [/* @__PURE__ */ jsx("span", { children: "Monthly requests" }), /* @__PURE__ */ jsxs("span", { children: [
198
+ usedRequests.toLocaleString(),
199
+ " /",
200
+ " ",
201
+ monthlyQuota.toLocaleString()
202
+ ] })]
203
+ }), /* @__PURE__ */ jsx("div", {
204
+ className: "h-2 rounded-full bg-muted",
205
+ children: /* @__PURE__ */ jsx("div", {
206
+ className: "h-full rounded-full bg-primary transition-[width]",
207
+ style: { width: `${quotaPct}%` }
208
+ })
209
+ })]
210
+ }) : null
211
+ ]
212
+ }, subscription.planSku);
213
+ })
214
+ })]
215
+ }) : null,
216
+ billingState === "empty" && !confirmingPayment ? /* @__PURE__ */ jsx(ChoosePlanNotice, {
217
+ className: "mb-8 rounded-xl border border-dashed p-8 text-center",
218
+ body: "You do not have a plan yet. Choose one to start using the API; your billing details appear here afterwards."
219
+ }) : null,
220
+ !isLoading && earlierSubscriptions.length > 0 ? /* @__PURE__ */ jsxs("section", {
221
+ className: "mb-8",
222
+ children: [/* @__PURE__ */ jsx("p", {
223
+ className: "mb-2 text-xs font-medium uppercase tracking-[0.05em] text-muted-foreground",
224
+ children: "Earlier plans"
225
+ }), /* @__PURE__ */ jsx("ul", {
226
+ className: "divide-y rounded-xl border bg-card text-sm",
227
+ children: earlierSubscriptions.map((subscription) => /* @__PURE__ */ jsxs("li", {
228
+ className: "flex items-center justify-between gap-4 px-5 py-3",
229
+ children: [/* @__PURE__ */ jsx("span", { children: planBySku.get(subscription.planSku)?.displayName ?? subscription.planSku }), /* @__PURE__ */ jsx("span", {
230
+ className: "text-xs text-muted-foreground",
231
+ children: subscriptionStatusLabel(subscription.status)
232
+ })]
233
+ }, `${subscription.planSku}-${subscription.status}`))
234
+ })]
235
+ }) : null,
236
+ /* @__PURE__ */ jsxs("div", {
237
+ className: "flex flex-wrap gap-3",
238
+ children: [
239
+ /* @__PURE__ */ jsx(Button$1, {
240
+ variant: "outline",
241
+ asChild: true,
242
+ children: /* @__PURE__ */ jsx(Link$1, {
243
+ to: ACCOUNT_PLANS_PATH,
244
+ children: "Manage plans"
245
+ })
246
+ }),
247
+ auth.isAuthenticated && hasPaymentProvider(billing) ? /* @__PURE__ */ jsx(Button$1, {
248
+ variant: "default",
249
+ disabled: billingPortal.isPending,
250
+ onClick: () => {
251
+ billingPortal.mutate();
252
+ },
253
+ children: billingPortal.isPending ? /* @__PURE__ */ jsx(Spinner, {}) : "Manage payment & invoices"
254
+ }) : null,
255
+ auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
256
+ variant: "secondary",
257
+ asChild: true,
258
+ children: /* @__PURE__ */ jsx(Link$1, {
259
+ to: CREDENTIALS_PATH,
260
+ children: "API keys"
261
+ })
262
+ }) : null
263
+ ]
264
+ }),
265
+ billingPortal.isError ? /* @__PURE__ */ jsx("p", {
266
+ className: "mt-4 text-sm text-destructive",
267
+ children: billingPortal.error instanceof Error ? billingPortal.error.message : "Unable to open billing portal."
268
+ }) : null
269
+ ]
270
+ });
271
+ };
272
+
273
+ //#endregion
274
+ export { SubscriptionsPage as default, subscriptionStatusLabel };