@lukoweb/apitogo-plugin-dev-portal-billing 0.2.36 → 0.2.37

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,6 +1,6 @@
1
1
  import { o as PRICING_PATH, t as ACCOUNT_PLANS_PATH } from "./paths-ujy9wjVK.mjs";
2
2
  import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
3
- import { t as FREE_CHECKOUT_MARKER } from "./billing-state-pnT0H1Lm.mjs";
3
+ import { t as FREE_CHECKOUT_MARKER } from "./billing-state-CIZ0pR37.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";
@@ -0,0 +1,34 @@
1
+ import { t as ACCOUNT_PLANS_PATH } from "./paths-ujy9wjVK.mjs";
2
+ import { Link } from "@lukoweb/apitogo/router";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { Button } from "@lukoweb/apitogo/ui/Button.js";
5
+
6
+ //#region src/components/ChoosePlanNotice.tsx
7
+ /** What a signed-in visitor without a plan reads, and the one thing to do next. Names no plan: a portal can sell only paid ones. */
8
+ const CHOOSE_PLAN_TITLE = "Choose a plan to get started";
9
+ const CHOOSE_PLAN_BODY = "An API key belongs to a plan. Choose a plan first, then come back here to create your key and call the API.";
10
+ const ChoosePlanNotice = ({ className = "rounded-xl border border-dashed p-8 text-center", body = CHOOSE_PLAN_BODY }) => /* @__PURE__ */ jsxs("div", {
11
+ className,
12
+ "data-testid": "choose-plan-notice",
13
+ children: [
14
+ /* @__PURE__ */ jsx("h2", {
15
+ className: "text-lg font-semibold",
16
+ children: CHOOSE_PLAN_TITLE
17
+ }),
18
+ /* @__PURE__ */ jsx("p", {
19
+ className: "mt-2 text-muted-foreground",
20
+ children: body
21
+ }),
22
+ /* @__PURE__ */ jsx(Button, {
23
+ className: "mt-4",
24
+ asChild: true,
25
+ children: /* @__PURE__ */ jsx(Link, {
26
+ to: ACCOUNT_PLANS_PATH,
27
+ children: "Choose a plan"
28
+ })
29
+ })
30
+ ]
31
+ });
32
+
33
+ //#endregion
34
+ export { ChoosePlanNotice as t };
@@ -3,6 +3,7 @@ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalA
3
3
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
4
  import { n as createDemoCredentials } from "./demo-session-Cez740bi.mjs";
5
5
  import { r as useApiKeys, t as ApiKeysPanel } from "./ApiKeysPanel-DrArkHXf.mjs";
6
+ import { t as ChoosePlanNotice } from "./ChoosePlanNotice-DITJNrh1.mjs";
6
7
  import { Head } from "@lukoweb/apitogo/components";
7
8
  import { KeyRoundIcon } from "@lukoweb/apitogo/icons";
8
9
  import { Link as Link$1 } from "@lukoweb/apitogo/router";
@@ -110,11 +111,11 @@ const CredentialsPage = () => {
110
111
  className: "text-muted-foreground",
111
112
  children: "Loading credentials…"
112
113
  }) : null,
113
- !isPreview && !isDemoSession && auth.isAuthenticated && (error || keysError) ? /* @__PURE__ */ jsxs("div", {
114
+ !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", {
114
115
  className: "rounded-xl border border-dashed p-8 text-center",
115
116
  children: [/* @__PURE__ */ jsx("p", {
116
117
  className: "text-muted-foreground",
117
- 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."
118
+ children: "Unable to load API credentials. Try again in a moment."
118
119
  }), /* @__PURE__ */ jsx(Button$1, {
119
120
  className: "mt-4",
120
121
  asChild: true,
@@ -126,20 +127,7 @@ const CredentialsPage = () => {
126
127
  }) : null,
127
128
  (isDemoSession || !isPreview) && auth.isAuthenticated && !error && !keysError && !isLoading && !keysLoading ? /* @__PURE__ */ jsxs("div", {
128
129
  className: "space-y-6",
129
- children: [plans.length === 0 ? /* @__PURE__ */ jsxs("div", {
130
- className: "rounded-xl border border-dashed p-8 text-center",
131
- children: [/* @__PURE__ */ jsx("p", {
132
- className: "text-muted-foreground",
133
- children: "Choose a plan to get your first API key."
134
- }), /* @__PURE__ */ jsx(Button$1, {
135
- className: "mt-4",
136
- asChild: true,
137
- children: /* @__PURE__ */ jsx(Link$1, {
138
- to: ACCOUNT_PLANS_PATH,
139
- children: "View plans"
140
- })
141
- })]
142
- }) : plans.map((plan) => /* @__PURE__ */ jsx(ApiKeysPanel, {
130
+ children: [plans.length === 0 ? /* @__PURE__ */ jsx(ChoosePlanNotice, {}) : plans.map((plan) => /* @__PURE__ */ jsx(ApiKeysPanel, {
143
131
  plan,
144
132
  header,
145
133
  readOnly: isDemoSession
@@ -3,7 +3,7 @@ import { t as PricingCard } from "./PricingCard-Wra2FUX9.mjs";
3
3
  import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
4
  import { t as usePlans } from "./usePlans-DUex2tzT.mjs";
5
5
  import { t as isPreviewPricingEnabled } from "./pricing-config-CfhRX9tk.mjs";
6
- import { i as isPricingLoading, n as buildCompareRows } from "./billing-state-pnT0H1Lm.mjs";
6
+ import { a as isPricingLoading, n as buildCompareRows } from "./billing-state-CIZ0pR37.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,10 +1,11 @@
1
1
  import { i as CREDENTIALS_PATH, t as ACCOUNT_PLANS_PATH } from "./paths-ujy9wjVK.mjs";
2
2
  import { c as isActiveSubscription, i as formatBillingPeriod, m as parseMonthlyQuotaCalls, o as formatPlanPrice, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
3
- import "./DevPortalBillingContext-CR83s4LO.mjs";
3
+ import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
4
  import { t as useMySubscriptions } from "./useMySubscriptions-CLh3eFY4.mjs";
5
5
  import { t as usePlans } from "./usePlans-DUex2tzT.mjs";
6
6
  import { t as useUsage } from "./useUsage-Cx2GB3wg.mjs";
7
- import { r as checkoutConfirmingText } from "./billing-state-pnT0H1Lm.mjs";
7
+ import { i as hasPaymentProvider, r as checkoutConfirmingText } from "./billing-state-CIZ0pR37.mjs";
8
+ import { t as ChoosePlanNotice } from "./ChoosePlanNotice-DITJNrh1.mjs";
8
9
  import { Head, Spinner } from "@lukoweb/apitogo/components";
9
10
  import { Link as Link$1, useSearchParams } from "@lukoweb/apitogo/router";
10
11
  import { useEffect } from "react";
@@ -37,6 +38,7 @@ function useBillingPortal() {
37
38
  //#endregion
38
39
  //#region src/pages/SubscriptionsPage.tsx
39
40
  const SubscriptionsPage = () => {
41
+ const { billing } = useDevPortalBillingConfig();
40
42
  const auth = useAuth();
41
43
  const [searchParams] = useSearchParams();
42
44
  const checkoutSuccess = searchParams.get("checkout") === "success";
@@ -144,19 +146,9 @@ const SubscriptionsPage = () => {
144
146
  }) : null
145
147
  ]
146
148
  }) : null,
147
- !isLoading && subscriptions.length === 0 ? /* @__PURE__ */ jsxs("div", {
149
+ !isLoading && subscriptions.length === 0 ? /* @__PURE__ */ jsx(ChoosePlanNotice, {
148
150
  className: "mb-8 rounded-xl border border-dashed p-8 text-center",
149
- children: [/* @__PURE__ */ jsx("p", {
150
- className: "text-muted-foreground",
151
- children: "You do not have an active subscription yet."
152
- }), /* @__PURE__ */ jsx(Button$1, {
153
- className: "mt-4",
154
- asChild: true,
155
- children: /* @__PURE__ */ jsx(Link$1, {
156
- to: ACCOUNT_PLANS_PATH,
157
- children: "View plans"
158
- })
159
- })]
151
+ body: "You do not have a plan yet. Choose one to start using the API; your billing details appear here afterwards."
160
152
  }) : null,
161
153
  !isLoading && subscriptions.length > 1 ? /* @__PURE__ */ jsx("div", {
162
154
  className: "mb-8 space-y-4",
@@ -196,7 +188,7 @@ const SubscriptionsPage = () => {
196
188
  children: "Change plan"
197
189
  })
198
190
  }),
199
- auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
191
+ auth.isAuthenticated && hasPaymentProvider(billing) ? /* @__PURE__ */ jsx(Button$1, {
200
192
  variant: "default",
201
193
  disabled: billingPortal.isPending,
202
194
  onClick: () => {
@@ -8,6 +8,13 @@ function isPricingLoading(state) {
8
8
  return state.authPending && !state.hasPlans && !state.plansFailed;
9
9
  }
10
10
  /**
11
+ * Whether the project takes payments. A project whose plans service has billing `none` has no payment provider, so
12
+ * the billing page must not offer "Manage payment & invoices". An unknown provider counts as having one.
13
+ */
14
+ function hasPaymentProvider(billing) {
15
+ return billing !== "none";
16
+ }
17
+ /**
11
18
  * The line shown on the billing page while the subscription is being confirmed after checkout. A free plan has no
12
19
  * payment, so it must not claim one; the paid return from the payment provider carries no marker and says so.
13
20
  */
@@ -34,4 +41,4 @@ function buildCompareRows(input) {
34
41
  }
35
42
 
36
43
  //#endregion
37
- export { isPricingLoading as i, buildCompareRows as n, checkoutConfirmingText as r, FREE_CHECKOUT_MARKER as t };
44
+ export { isPricingLoading as a, hasPaymentProvider as i, buildCompareRows as n, checkoutConfirmingText as r, FREE_CHECKOUT_MARKER as t };
package/dist/index.d.mts CHANGED
@@ -41,7 +41,8 @@ type DevPortalBillingConfig = {
41
41
  subtitle?: string;
42
42
  compareRows?: DevPortalPricingCompareRow[];
43
43
  faqs?: DevPortalPricingFaq[];
44
- }; /** Address of the plans backend; the portal's generated wrapper passes it. The plans request starts from it early. */
44
+ }; /** Billing provider of the plans service (`none`, `stripe`, ...). `none`: no payment is offered, so no payment page. */
45
+ billing?: string; /** Address of the plans backend; the portal's generated wrapper passes it. The plans request starts from it early. */
45
46
  apiBaseUrl?: string; /** Live gateway URL (publish bake or explicit plugin option). */
46
47
  gatewayBaseUrl?: string | null; /** Local preview plan catalog (from `apitogo.config.json` `plans`). */
47
48
  previewPlans?: Array<{
package/dist/index.mjs CHANGED
@@ -327,15 +327,49 @@ function buildDevPortalHeaderNavigation({ navigation, pricingEnabled, authNavEna
327
327
  return items;
328
328
  }
329
329
 
330
+ //#endregion
331
+ //#region src/utils/signed-in-identity.ts
332
+ const SIGNED_IN_IDENTITY_ID = "signed-in-user";
333
+ /**
334
+ * The API reference's "Try it" identity for the signed-in visitor: the request carries the visitor's own token as
335
+ * `Authorization: Bearer`, so an API behind the gateway's token check can be tried from the docs. The token comes from
336
+ * the plans backend (`/api/v1/credentials`, answered from the session cookie) and is asked for when the identity list
337
+ * is built. A visitor who is signed out or has no subscription gets no identity, and the playground offers none.
338
+ */
339
+ async function getSignedInIdentities(apiBaseUrl, fetchImpl = fetch) {
340
+ if (!apiBaseUrl) return [];
341
+ try {
342
+ const response = await fetchImpl(`${apiBaseUrl}/api/v1/credentials`, {
343
+ method: "GET",
344
+ headers: { Accept: "application/json" },
345
+ credentials: "include"
346
+ });
347
+ if (!response.ok) return [];
348
+ const credentials = await response.json();
349
+ const token = credentials.accessToken ?? credentials.idToken;
350
+ if (!token) return [];
351
+ return [{
352
+ id: SIGNED_IN_IDENTITY_ID,
353
+ label: "Signed-in user",
354
+ authorizeRequest: (request) => {
355
+ request.headers.set("Authorization", `Bearer ${token}`);
356
+ return request;
357
+ }
358
+ }];
359
+ } catch {
360
+ return [];
361
+ }
362
+ }
363
+
330
364
  //#endregion
331
365
  //#region src/DevPortalBillingPlugin.tsx
332
366
  const AccountPlansPage$1 = lazy(() => import("./AccountPlansPage-CXOfTwlX.mjs").then((n) => n.n));
333
- const CheckoutPage = lazy(() => import("./CheckoutPage-CW_EBGog.mjs"));
367
+ const CheckoutPage = lazy(() => import("./CheckoutPage-DiYurW2O.mjs"));
334
368
  const ContactSalesPage = lazy(() => import("./ContactSalesPage-Vjzm-RxO.mjs"));
335
- const CredentialsPage = lazy(() => import("./CredentialsPage-CtIEboEy.mjs"));
336
- const PricingPage = lazy(() => import("./PricingPage-MTslSnCh.mjs"));
337
- const SubscriptionsPage = lazy(() => import("./SubscriptionsPage-9lXnVks7.mjs"));
338
- const UsagePage = lazy(() => import("./UsagePage--mg_NzrA.mjs"));
369
+ const CredentialsPage = lazy(() => import("./CredentialsPage-Ir8Gf62T.mjs"));
370
+ const PricingPage = lazy(() => import("./PricingPage-DodoDd3h.mjs"));
371
+ const SubscriptionsPage = lazy(() => import("./SubscriptionsPage-CDmzL6QX.mjs"));
372
+ const UsagePage = lazy(() => import("./UsagePage-C4K_arXh.mjs"));
339
373
  function resolvePricingEnabled(config) {
340
374
  if (config.__meta?.pricingEnabled !== void 0) return config.__meta.pricingEnabled;
341
375
  return isPreviewPricingEnabled();
@@ -359,6 +393,7 @@ const devPortalBillingPlugin = createPlugin((options = {}) => {
359
393
  }
360
394
  });
361
395
  },
396
+ getIdentities: () => getSignedInIdentities(resolveDevPortalApiBaseUrl(options.apiBaseUrl)),
362
397
  getProfileMenuItems: () => [{
363
398
  label: "My Subscriptions",
364
399
  path: SUBSCRIPTIONS_PATH,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo-plugin-dev-portal-billing",
3
- "version": "0.2.36",
3
+ "version": "0.2.37",
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.36",
50
- "@lukoweb/apitogo-module-user-panel": "0.2.36"
49
+ "@lukoweb/apitogo": "0.2.37",
50
+ "@lukoweb/apitogo-module-user-panel": "0.2.37"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": ">=19.2.0",