@lukoweb/apitogo-plugin-dev-portal-billing 0.1.38 → 0.1.40

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
@@ -1,5 +1,9 @@
1
1
  import * as _lukoweb_apitogo0 from "@lukoweb/apitogo";
2
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
2
3
 
4
+ //#region src/pages/AccountPlansPage.d.ts
5
+ declare const AccountPlansPage: () => react_jsx_runtime0.JSX.Element;
6
+ //#endregion
3
7
  //#region src/types.d.ts
4
8
  type DevPortalBillingConfig = {
5
9
  pricing?: {
@@ -8,10 +12,13 @@ type DevPortalBillingConfig = {
8
12
  };
9
13
  };
10
14
  //#endregion
11
- //#region src/DevPortalBillingPlugin.d.ts
15
+ //#region src/paths.d.ts
12
16
  declare const PRICING_PATH = "/pricing";
13
17
  declare const CHECKOUT_PATH = "/checkout";
14
18
  declare const SUBSCRIPTIONS_PATH = "/account/subscriptions";
19
+ declare const CREDENTIALS_PATH = "/account/credentials";
20
+ //#endregion
21
+ //#region src/DevPortalBillingPlugin.d.ts
15
22
  declare const devPortalBillingPlugin: (options?: DevPortalBillingConfig | undefined) => _lukoweb_apitogo0.ApitogoPlugin;
16
23
  //#endregion
17
- export { CHECKOUT_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
24
+ export { AccountPlansPage, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
package/dist/index.mjs CHANGED
@@ -1,72 +1,16 @@
1
- import { cn, createPlugin } from "@lukoweb/apitogo";
2
- import { ShieldIcon, StarsIcon } from "@lukoweb/apitogo/icons";
3
1
  import { Button, ClientOnly, Head, Heading } from "@lukoweb/apitogo/components";
4
- import { Link, Outlet, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
5
- import { createContext, useContext, useEffect } from "react";
6
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { CreditCardIcon, KeyRoundIcon, ShieldIcon, StarsIcon } from "@lukoweb/apitogo/icons";
3
+ import { createContext, useContext, useEffect, useState } from "react";
4
+ import { cn, createPlugin } from "@lukoweb/apitogo";
5
+ import { Link, Navigate, Outlet, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
6
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@lukoweb/apitogo/ui/AlertDialog.js";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { useMutation, useQuery, useQueryClient } from "@lukoweb/apitogo/react-query";
7
9
  import { useAuth } from "@lukoweb/apitogo/hooks";
8
- import { useQuery } from "@lukoweb/apitogo/react-query";
9
10
  import { Alert, AlertAction, AlertDescription } from "@lukoweb/apitogo/ui/Alert";
10
11
  import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button";
12
+ import { Secret } from "@lukoweb/apitogo/ui/Secret.js";
11
13
 
12
- //#region src/DevPortalBillingContext.tsx
13
- const DevPortalBillingContext = createContext({});
14
- const DevPortalBillingProvider = ({ options, children }) => /* @__PURE__ */ jsx(DevPortalBillingContext.Provider, {
15
- value: options,
16
- children
17
- });
18
- const useDevPortalBillingConfig = () => useContext(DevPortalBillingContext);
19
-
20
- //#endregion
21
- //#region src/DevPortalBillingWrapper.tsx
22
- const DevPortalBillingWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(DevPortalBillingProvider, {
23
- options,
24
- children: /* @__PURE__ */ jsx(ClientOnly, { children: /* @__PURE__ */ jsx(Outlet, {}) })
25
- });
26
-
27
- //#endregion
28
- //#region src/components/RedirectPage.tsx
29
- const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
30
- useEffect(() => {
31
- if (url) window.location.assign(url);
32
- }, [url]);
33
- return /* @__PURE__ */ jsx("div", {
34
- className: "flex min-h-screen items-center justify-center bg-muted",
35
- children: /* @__PURE__ */ jsxs("div", {
36
- className: "flex max-w-md flex-col items-center space-y-6 text-center",
37
- children: [
38
- /* @__PURE__ */ jsx("div", {
39
- className: "relative",
40
- children: /* @__PURE__ */ jsx("div", {
41
- className: "flex h-24 w-24 items-center justify-center rounded-full bg-foreground/10",
42
- children: /* @__PURE__ */ jsx(Icon, { className: "h-12 w-12 text-foreground" })
43
- })
44
- }),
45
- children,
46
- /* @__PURE__ */ jsxs("div", {
47
- className: "space-y-2",
48
- children: [/* @__PURE__ */ jsx("h2", {
49
- className: "text-2xl font-bold text-card-foreground",
50
- children: title
51
- }), /* @__PURE__ */ jsx("p", {
52
- className: "text-muted-foreground",
53
- children: description
54
- })]
55
- }),
56
- /* @__PURE__ */ jsxs("div", {
57
- className: "flex space-x-2",
58
- children: [
59
- /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.3s]" }),
60
- /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.15s]" }),
61
- /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary" })
62
- ]
63
- })
64
- ]
65
- })
66
- });
67
- };
68
-
69
- //#endregion
70
14
  //#region src/utils/dev-portal-api.ts
71
15
  const DEV_PORTAL_PLACEHOLDER_API_URL = "https://dp-example.azurecontainerapps.io";
72
16
  function resolveDevPortalApiBaseUrl(configuredUrl) {
@@ -138,6 +82,99 @@ function isActiveSubscription(status) {
138
82
  return status.toLowerCase() === "active";
139
83
  }
140
84
 
85
+ //#endregion
86
+ //#region src/components/PricingCard.tsx
87
+ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, isCanceling = false, cancelDisabled = false }) => {
88
+ const limitsSummary = summarizeLimits(plan.limitsJson);
89
+ const isActive = isSubscribed || isActiveSubscription(subscriptionStatus ?? "");
90
+ return /* @__PURE__ */ jsxs("article", {
91
+ className: cn("flex w-full max-w-sm flex-col rounded-xl border bg-card/60 p-6 shadow-xs", isActive && mode === "account" && "border-primary/40 ring-1 ring-primary/20"),
92
+ "data-testid": `pricing-card-${plan.sku}`,
93
+ children: [
94
+ /* @__PURE__ */ jsxs("div", {
95
+ className: "flex items-start justify-between gap-2",
96
+ children: [/* @__PURE__ */ jsx("h2", {
97
+ className: "text-xl font-semibold",
98
+ children: plan.displayName
99
+ }), isActive && mode === "account" ? /* @__PURE__ */ jsx("span", {
100
+ className: "shrink-0 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary",
101
+ "data-testid": `subscribed-badge-${plan.sku}`,
102
+ children: "Subscribed"
103
+ }) : null]
104
+ }),
105
+ /* @__PURE__ */ jsx("p", {
106
+ className: "mt-2 text-3xl font-bold",
107
+ children: formatPlanPrice(plan)
108
+ }),
109
+ /* @__PURE__ */ jsx("p", {
110
+ className: "mt-1 text-sm text-muted-foreground",
111
+ children: formatBillingPeriod(plan.billingPeriod)
112
+ }),
113
+ limitsSummary ? /* @__PURE__ */ jsx("p", {
114
+ className: "mt-4 text-sm text-muted-foreground",
115
+ children: limitsSummary
116
+ }) : null,
117
+ /* @__PURE__ */ jsx("div", {
118
+ className: "mt-6 space-y-2",
119
+ children: mode === "account" ? isActive ? /* @__PURE__ */ jsxs(Fragment, { children: [
120
+ /* @__PURE__ */ jsx(Button, {
121
+ variant: "secondary",
122
+ className: "w-full",
123
+ disabled: true,
124
+ children: "Current plan"
125
+ }),
126
+ onUnsubscribe ? /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
127
+ asChild: true,
128
+ children: /* @__PURE__ */ jsx(Button, {
129
+ variant: "outline",
130
+ className: "w-full",
131
+ disabled: cancelDisabled || isCanceling,
132
+ "data-testid": `unsubscribe-${plan.sku}`,
133
+ children: isCanceling ? "Canceling…" : "Unsubscribe"
134
+ })
135
+ }), /* @__PURE__ */ jsxs(AlertDialogContent, { children: [/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [/* @__PURE__ */ jsxs(AlertDialogTitle, { children: [
136
+ "Unsubscribe from ",
137
+ plan.displayName,
138
+ "?"
139
+ ] }), /* @__PURE__ */ jsx(AlertDialogDescription, { children: "Your API access for this plan will be revoked. You can subscribe again later." })] }), /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [/* @__PURE__ */ jsx(AlertDialogCancel, { children: "Keep subscription" }), /* @__PURE__ */ jsx(AlertDialogAction, {
140
+ onClick: () => onUnsubscribe(plan.sku),
141
+ disabled: isCanceling,
142
+ children: "Unsubscribe"
143
+ })] })] })] }) : null,
144
+ cancelDisabled ? /* @__PURE__ */ jsx("p", {
145
+ className: "text-center text-xs text-muted-foreground",
146
+ children: "Unsubscribe is available after the portal is published."
147
+ }) : null
148
+ ] }) : /* @__PURE__ */ jsx(Button, {
149
+ variant: "default",
150
+ className: "w-full",
151
+ asChild: true,
152
+ children: /* @__PURE__ */ jsx(Link, {
153
+ to: `/checkout?planSku=${encodeURIComponent(plan.sku)}`,
154
+ children: plan.isFree ? "Get started" : "Subscribe"
155
+ })
156
+ }) : isActive ? /* @__PURE__ */ jsx(Button, {
157
+ variant: "secondary",
158
+ className: "w-full",
159
+ asChild: true,
160
+ children: /* @__PURE__ */ jsx(Link, {
161
+ to: "/account/subscriptions",
162
+ children: "Manage subscription"
163
+ })
164
+ }) : /* @__PURE__ */ jsx(Button, {
165
+ variant: "default",
166
+ className: "w-full",
167
+ asChild: true,
168
+ children: /* @__PURE__ */ jsx(Link, {
169
+ to: `/checkout?planSku=${encodeURIComponent(plan.sku)}`,
170
+ children: plan.isFree ? "Get started" : "Subscribe"
171
+ })
172
+ })
173
+ })
174
+ ]
175
+ });
176
+ };
177
+
141
178
  //#endregion
142
179
  //#region src/hooks/useDevPortalApiBaseUrl.ts
143
180
  function useDevPortalApiBaseUrl() {
@@ -152,8 +189,221 @@ function useDevPortalPreviewMode() {
152
189
  return isPlaceholderDevPortalApiUrl(apiBaseUrl);
153
190
  }
154
191
 
192
+ //#endregion
193
+ //#region src/hooks/useCancelSubscription.ts
194
+ function useCancelSubscription() {
195
+ const apiBaseUrl = useDevPortalApiBaseUrl();
196
+ const queryClient = useQueryClient();
197
+ return useMutation({
198
+ mutationFn: async (planSku) => {
199
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
200
+ const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/cancel`, {
201
+ method: "POST",
202
+ headers: {
203
+ Accept: "application/json",
204
+ "Content-Type": "application/json"
205
+ },
206
+ credentials: "include",
207
+ body: JSON.stringify({ planSku })
208
+ });
209
+ if (!response.ok) {
210
+ const detail = await response.text();
211
+ throw new Error(detail || "Failed to cancel subscription.");
212
+ }
213
+ return await response.json();
214
+ },
215
+ onSuccess: async () => {
216
+ await queryClient.invalidateQueries({ queryKey: ["dev-portal-me", apiBaseUrl] });
217
+ }
218
+ });
219
+ }
220
+
221
+ //#endregion
222
+ //#region src/hooks/useMySubscriptions.ts
223
+ function useMySubscriptions() {
224
+ const auth = useAuth();
225
+ const apiBaseUrl = useDevPortalApiBaseUrl();
226
+ const isPreview = useDevPortalPreviewMode();
227
+ return useQuery({
228
+ queryKey: ["dev-portal-me", apiBaseUrl],
229
+ enabled: auth.isAuthenticated && !isPreview && Boolean(apiBaseUrl),
230
+ queryFn: async () => {
231
+ const response = await fetch(`${apiBaseUrl}/api/v1/auth/me`, {
232
+ method: "GET",
233
+ headers: { Accept: "application/json" },
234
+ credentials: "include"
235
+ });
236
+ if (!response.ok) throw new Error("Failed to load subscription status.");
237
+ return await response.json();
238
+ }
239
+ });
240
+ }
241
+
242
+ //#endregion
243
+ //#region src/hooks/usePlans.ts
244
+ function usePlans() {
245
+ const apiBaseUrl = useDevPortalApiBaseUrl();
246
+ const isPreview = useDevPortalPreviewMode();
247
+ return useQuery({
248
+ queryKey: [
249
+ "dev-portal-plans",
250
+ apiBaseUrl,
251
+ isPreview
252
+ ],
253
+ queryFn: async () => {
254
+ if (isPreview) return parseMockPlans();
255
+ if (!apiBaseUrl) return { plans: [] };
256
+ const response = await fetch(`${apiBaseUrl}/api/v1/plans`, { headers: { Accept: "application/json" } });
257
+ if (!response.ok) throw new Error("Failed to load plans.");
258
+ return await response.json();
259
+ }
260
+ });
261
+ }
262
+
263
+ //#endregion
264
+ //#region src/pages/AccountPlansPage.tsx
265
+ const AccountPlansPage = () => {
266
+ const isPreview = useDevPortalPreviewMode();
267
+ const { data: catalog, isLoading, isError } = usePlans();
268
+ const { data: me } = useMySubscriptions();
269
+ const cancelMutation = useCancelSubscription();
270
+ const [cancelingSku, setCancelingSku] = useState(null);
271
+ const subscriptionBySku = new Map((me?.subscriptions ?? []).map((sub) => [sub.planSku, sub]));
272
+ const handleUnsubscribe = async (planSku) => {
273
+ setCancelingSku(planSku);
274
+ try {
275
+ await cancelMutation.mutateAsync(planSku);
276
+ } finally {
277
+ setCancelingSku(null);
278
+ }
279
+ };
280
+ return /* @__PURE__ */ jsxs("div", {
281
+ className: "mx-auto max-w-screen-xl px-4 pb-16 pt-(--padding-content-top) lg:px-8",
282
+ children: [
283
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Plans & billing" }) }),
284
+ /* @__PURE__ */ jsxs("div", {
285
+ className: "mb-8 flex items-start gap-4",
286
+ children: [/* @__PURE__ */ jsx("div", {
287
+ className: "flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary",
288
+ children: /* @__PURE__ */ jsx(CreditCardIcon, { size: 24 })
289
+ }), /* @__PURE__ */ jsxs("div", {
290
+ className: "space-y-1",
291
+ children: [/* @__PURE__ */ jsx("h1", {
292
+ className: "text-3xl font-semibold tracking-tight",
293
+ children: "Plans & billing"
294
+ }), /* @__PURE__ */ jsx("p", {
295
+ className: "max-w-2xl text-muted-foreground",
296
+ children: "Compare plans, manage your subscriptions, and change tiers."
297
+ })]
298
+ })]
299
+ }),
300
+ isPreview ? /* @__PURE__ */ jsx("div", {
301
+ className: "mb-8 rounded-xl border border-dashed bg-muted/20 p-6 text-center text-sm text-muted-foreground",
302
+ children: "Local preview uses mock plans. Subscribe and unsubscribe activate after the portal is published."
303
+ }) : null,
304
+ cancelMutation.isError ? /* @__PURE__ */ jsx("div", {
305
+ className: "mb-6 rounded-xl border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive",
306
+ children: cancelMutation.error instanceof Error ? cancelMutation.error.message : "Failed to cancel subscription."
307
+ }) : null,
308
+ isLoading ? /* @__PURE__ */ jsx("p", {
309
+ className: "text-muted-foreground",
310
+ children: "Loading plans…"
311
+ }) : null,
312
+ isError ? /* @__PURE__ */ jsx("p", {
313
+ className: "text-destructive",
314
+ children: "Unable to load plans."
315
+ }) : null,
316
+ catalog && catalog.plans.length > 0 ? /* @__PURE__ */ jsx("div", {
317
+ className: "grid grid-cols-1 justify-center gap-6 sm:grid-cols-[repeat(auto-fit,minmax(280px,max-content))]",
318
+ children: catalog.plans.map((plan) => {
319
+ const subscription = subscriptionBySku.get(plan.sku);
320
+ const isSubscribed = isActiveSubscription(subscription?.status ?? "");
321
+ return /* @__PURE__ */ jsx(PricingCard, {
322
+ plan,
323
+ mode: "account",
324
+ subscriptionStatus: subscription?.status,
325
+ isSubscribed,
326
+ onUnsubscribe: isPreview ? void 0 : handleUnsubscribe,
327
+ isCanceling: cancelingSku === plan.sku,
328
+ cancelDisabled: isPreview
329
+ }, plan.sku);
330
+ })
331
+ }) : !isLoading && !isError ? /* @__PURE__ */ jsx("div", {
332
+ className: "rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
333
+ children: "No plans are available yet. Sync plans from your APItoGo manifest or platform gateway."
334
+ }) : null
335
+ ]
336
+ });
337
+ };
338
+
339
+ //#endregion
340
+ //#region src/DevPortalBillingContext.tsx
341
+ const DevPortalBillingContext = createContext({});
342
+ const DevPortalBillingProvider = ({ options, children }) => /* @__PURE__ */ jsx(DevPortalBillingContext.Provider, {
343
+ value: options,
344
+ children
345
+ });
346
+ const useDevPortalBillingConfig = () => useContext(DevPortalBillingContext);
347
+
348
+ //#endregion
349
+ //#region src/DevPortalBillingWrapper.tsx
350
+ const DevPortalBillingWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(DevPortalBillingProvider, {
351
+ options,
352
+ children: /* @__PURE__ */ jsx(ClientOnly, { children: /* @__PURE__ */ jsx(Outlet, {}) })
353
+ });
354
+
355
+ //#endregion
356
+ //#region src/components/RedirectPage.tsx
357
+ const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
358
+ useEffect(() => {
359
+ if (url) window.location.assign(url);
360
+ }, [url]);
361
+ return /* @__PURE__ */ jsx("div", {
362
+ className: "flex min-h-screen items-center justify-center bg-muted",
363
+ children: /* @__PURE__ */ jsxs("div", {
364
+ className: "flex max-w-md flex-col items-center space-y-6 text-center",
365
+ children: [
366
+ /* @__PURE__ */ jsx("div", {
367
+ className: "relative",
368
+ children: /* @__PURE__ */ jsx("div", {
369
+ className: "flex h-24 w-24 items-center justify-center rounded-full bg-foreground/10",
370
+ children: /* @__PURE__ */ jsx(Icon, { className: "h-12 w-12 text-foreground" })
371
+ })
372
+ }),
373
+ children,
374
+ /* @__PURE__ */ jsxs("div", {
375
+ className: "space-y-2",
376
+ children: [/* @__PURE__ */ jsx("h2", {
377
+ className: "text-2xl font-bold text-card-foreground",
378
+ children: title
379
+ }), /* @__PURE__ */ jsx("p", {
380
+ className: "text-muted-foreground",
381
+ children: description
382
+ })]
383
+ }),
384
+ /* @__PURE__ */ jsxs("div", {
385
+ className: "flex space-x-2",
386
+ children: [
387
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.3s]" }),
388
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.15s]" }),
389
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary" })
390
+ ]
391
+ })
392
+ ]
393
+ })
394
+ });
395
+ };
396
+
397
+ //#endregion
398
+ //#region src/paths.ts
399
+ const PRICING_PATH = "/pricing";
400
+ const CHECKOUT_PATH = "/checkout";
401
+ const SUBSCRIPTIONS_PATH = "/account/subscriptions";
402
+ const CREDENTIALS_PATH = "/account/credentials";
403
+
155
404
  //#endregion
156
405
  //#region src/pages/CheckoutPage.tsx
406
+ const ACCOUNT_PLANS_PATH = "/account/plans";
157
407
  const CheckoutPage = () => {
158
408
  const [searchParams] = useSearchParams();
159
409
  const planSku = searchParams.get("planSku") ?? "";
@@ -161,6 +411,7 @@ const CheckoutPage = () => {
161
411
  const auth = useAuth();
162
412
  const apiBaseUrl = useDevPortalApiBaseUrl();
163
413
  const isPreview = useDevPortalPreviewMode();
414
+ const plansPath = auth.isAuthenticated ? ACCOUNT_PLANS_PATH : PRICING_PATH;
164
415
  const checkoutQuery = useQuery({
165
416
  queryKey: [
166
417
  "dev-portal-checkout",
@@ -197,9 +448,9 @@ const CheckoutPage = () => {
197
448
  "Missing plan SKU.",
198
449
  " ",
199
450
  /* @__PURE__ */ jsx(Link, {
200
- to: "/pricing",
451
+ to: plansPath,
201
452
  className: "text-primary underline",
202
- children: "Back to pricing"
453
+ children: "Back to plans"
203
454
  })
204
455
  ]
205
456
  });
@@ -218,8 +469,8 @@ const CheckoutPage = () => {
218
469
  asChild: true,
219
470
  variant: "outline",
220
471
  children: /* @__PURE__ */ jsx(Link, {
221
- to: "/pricing",
222
- children: "Back to pricing"
472
+ to: plansPath,
473
+ children: "Back to plans"
223
474
  })
224
475
  })
225
476
  ]
@@ -240,8 +491,8 @@ const CheckoutPage = () => {
240
491
  size: "xs",
241
492
  asChild: true,
242
493
  children: /* @__PURE__ */ jsx(Link, {
243
- to: "/pricing",
244
- children: "Back to pricing"
494
+ to: plansPath,
495
+ children: "Back to plans"
245
496
  })
246
497
  }) })]
247
498
  }) : null
@@ -249,94 +500,181 @@ const CheckoutPage = () => {
249
500
  };
250
501
 
251
502
  //#endregion
252
- //#region src/components/PricingCard.tsx
253
- const PricingCard = ({ plan, isSubscribed = false }) => {
254
- const limitsSummary = summarizeLimits(plan.limitsJson);
255
- return /* @__PURE__ */ jsxs("article", {
256
- className: cn("flex w-full max-w-sm flex-col rounded-xl border bg-card/60 p-6 shadow-xs"),
257
- "data-testid": `pricing-card-${plan.sku}`,
258
- children: [
259
- /* @__PURE__ */ jsx("h2", {
260
- className: "text-xl font-semibold",
261
- children: plan.displayName
262
- }),
263
- /* @__PURE__ */ jsx("p", {
264
- className: "mt-2 text-3xl font-bold",
265
- children: formatPlanPrice(plan)
266
- }),
267
- /* @__PURE__ */ jsx("p", {
268
- className: "mt-1 text-sm text-muted-foreground",
269
- children: formatBillingPeriod(plan.billingPeriod)
270
- }),
271
- limitsSummary ? /* @__PURE__ */ jsx("p", {
272
- className: "mt-4 text-sm text-muted-foreground",
273
- children: limitsSummary
274
- }) : null,
275
- /* @__PURE__ */ jsx("div", {
276
- className: "mt-6",
277
- children: isSubscribed ? /* @__PURE__ */ jsx(Button, {
278
- variant: "secondary",
279
- className: "w-full",
280
- asChild: true,
281
- children: /* @__PURE__ */ jsx(Link, {
282
- to: "/account/subscriptions",
283
- children: "Manage subscription"
284
- })
285
- }) : /* @__PURE__ */ jsx(Button, {
286
- variant: "default",
287
- className: "w-full",
288
- asChild: true,
289
- children: /* @__PURE__ */ jsx(Link, {
290
- to: `/checkout?planSku=${encodeURIComponent(plan.sku)}`,
291
- children: plan.isFree ? "Get started" : "Subscribe"
292
- })
293
- })
294
- })
295
- ]
296
- });
297
- };
298
-
299
- //#endregion
300
- //#region src/hooks/useMySubscriptions.ts
301
- function useMySubscriptions() {
503
+ //#region src/hooks/useCredentials.ts
504
+ function useCredentials() {
302
505
  const auth = useAuth();
303
506
  const apiBaseUrl = useDevPortalApiBaseUrl();
304
507
  const isPreview = useDevPortalPreviewMode();
305
508
  return useQuery({
306
- queryKey: ["dev-portal-me", apiBaseUrl],
509
+ queryKey: ["dev-portal-credentials", apiBaseUrl],
307
510
  enabled: auth.isAuthenticated && !isPreview && Boolean(apiBaseUrl),
308
511
  queryFn: async () => {
309
- const response = await fetch(`${apiBaseUrl}/api/v1/auth/me`, {
512
+ const response = await fetch(`${apiBaseUrl}/api/v1/credentials`, {
310
513
  method: "GET",
311
514
  headers: { Accept: "application/json" },
312
515
  credentials: "include"
313
516
  });
314
- if (!response.ok) throw new Error("Failed to load subscription status.");
517
+ if (response.status === 404) throw new Error("NO_ENTITLEMENT");
518
+ if (!response.ok) throw new Error("Failed to load API credentials.");
315
519
  return await response.json();
316
- }
520
+ },
521
+ retry: (failureCount, error) => error instanceof Error && error.message === "NO_ENTITLEMENT" ? false : failureCount < 2
317
522
  });
318
523
  }
319
524
 
320
525
  //#endregion
321
- //#region src/hooks/usePlans.ts
322
- function usePlans() {
323
- const apiBaseUrl = useDevPortalApiBaseUrl();
526
+ //#region src/pages/CredentialsPage.tsx
527
+ const CredentialsPage = () => {
528
+ const auth = useAuth();
324
529
  const isPreview = useDevPortalPreviewMode();
325
- return useQuery({
326
- queryKey: [
327
- "dev-portal-plans",
328
- apiBaseUrl,
329
- isPreview
330
- ],
331
- queryFn: async () => {
332
- if (isPreview) return parseMockPlans();
333
- if (!apiBaseUrl) return { plans: [] };
334
- const response = await fetch(`${apiBaseUrl}/api/v1/plans`, { headers: { Accept: "application/json" } });
335
- if (!response.ok) throw new Error("Failed to load plans.");
336
- return await response.json();
337
- }
530
+ const { data: credentials, isLoading, error } = useCredentials();
531
+ const { data: catalog } = usePlans();
532
+ const [revealed, setRevealed] = useState(false);
533
+ const plan = catalog?.plans.find((item) => item.sku === credentials?.planSku);
534
+ return /* @__PURE__ */ jsxs("div", {
535
+ className: "mx-auto max-w-screen-lg px-4 pb-16 pt-(--padding-content-top) lg:px-8",
536
+ children: [
537
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "API credentials" }) }),
538
+ /* @__PURE__ */ jsxs("div", {
539
+ className: "mb-8 flex items-start gap-4",
540
+ children: [/* @__PURE__ */ jsx("div", {
541
+ className: "flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary",
542
+ children: /* @__PURE__ */ jsx(KeyRoundIcon, { size: 24 })
543
+ }), /* @__PURE__ */ jsxs("div", {
544
+ className: "space-y-1",
545
+ children: [/* @__PURE__ */ jsx("h1", {
546
+ className: "text-3xl font-semibold tracking-tight",
547
+ children: "API credentials"
548
+ }), /* @__PURE__ */ jsx("p", {
549
+ className: "text-muted-foreground",
550
+ children: "Use your subscription key to authenticate requests to the API gateway."
551
+ })]
552
+ })]
553
+ }),
554
+ isPreview ? /* @__PURE__ */ jsx("div", {
555
+ className: "rounded-xl border border-dashed p-8 text-center",
556
+ children: /* @__PURE__ */ jsx("p", {
557
+ className: "text-muted-foreground",
558
+ children: "API credentials are available after the developer portal is published."
559
+ })
560
+ }) : null,
561
+ !isPreview && !auth.isAuthenticated ? /* @__PURE__ */ jsxs("div", {
562
+ className: "rounded-xl border border-dashed p-8 text-center",
563
+ children: [/* @__PURE__ */ jsx("p", {
564
+ className: "text-muted-foreground",
565
+ children: "Sign in to view your subscription key."
566
+ }), /* @__PURE__ */ jsx(Button$1, {
567
+ className: "mt-4",
568
+ asChild: true,
569
+ children: /* @__PURE__ */ jsx(Link, {
570
+ to: "/account/plans",
571
+ children: "View plans"
572
+ })
573
+ })]
574
+ }) : null,
575
+ !isPreview && auth.isAuthenticated && isLoading ? /* @__PURE__ */ jsx("p", {
576
+ className: "text-muted-foreground",
577
+ children: "Loading credentials…"
578
+ }) : null,
579
+ !isPreview && auth.isAuthenticated && error ? /* @__PURE__ */ jsxs("div", {
580
+ className: "rounded-xl border border-dashed p-8 text-center",
581
+ children: [/* @__PURE__ */ jsx("p", {
582
+ className: "text-muted-foreground",
583
+ 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."
584
+ }), /* @__PURE__ */ jsx(Button$1, {
585
+ className: "mt-4",
586
+ asChild: true,
587
+ children: /* @__PURE__ */ jsx(Link, {
588
+ to: "/account/plans",
589
+ children: "View plans"
590
+ })
591
+ })]
592
+ }) : null,
593
+ !isPreview && credentials ? /* @__PURE__ */ jsxs("div", {
594
+ className: "space-y-6",
595
+ children: [
596
+ /* @__PURE__ */ jsx("article", {
597
+ className: "rounded-xl border bg-card/60 p-6",
598
+ children: /* @__PURE__ */ jsxs("div", {
599
+ className: "space-y-1",
600
+ children: [/* @__PURE__ */ jsx("h2", {
601
+ className: "text-lg font-medium",
602
+ children: "Active plan"
603
+ }), /* @__PURE__ */ jsx("p", {
604
+ className: "text-sm text-muted-foreground",
605
+ children: plan?.displayName ?? credentials.planSku
606
+ })]
607
+ })
608
+ }),
609
+ /* @__PURE__ */ jsxs("article", {
610
+ className: "rounded-xl border bg-card/60 p-6 space-y-4",
611
+ children: [/* @__PURE__ */ jsxs("div", {
612
+ className: "space-y-1",
613
+ children: [/* @__PURE__ */ jsx("h2", {
614
+ className: "text-lg font-medium",
615
+ children: "Subscription key"
616
+ }), /* @__PURE__ */ jsxs("p", {
617
+ className: "text-sm text-muted-foreground",
618
+ children: [
619
+ "Send this value in the",
620
+ " ",
621
+ /* @__PURE__ */ jsx("code", {
622
+ className: "rounded bg-muted px-1 py-0.5 text-xs",
623
+ children: "Ocp-Apim-Subscription-Key"
624
+ }),
625
+ " ",
626
+ "header when calling the gateway."
627
+ ]
628
+ })]
629
+ }), /* @__PURE__ */ jsx(Secret, {
630
+ className: "max-w-full w-full",
631
+ secret: credentials.subscriptionKey,
632
+ status: "active",
633
+ revealed,
634
+ onReveal: setRevealed
635
+ })]
636
+ }),
637
+ /* @__PURE__ */ jsxs("article", {
638
+ className: "rounded-xl border bg-card/60 p-6 space-y-4",
639
+ children: [/* @__PURE__ */ jsxs("div", {
640
+ className: "space-y-1",
641
+ children: [/* @__PURE__ */ jsx("h2", {
642
+ className: "text-lg font-medium",
643
+ children: "Gateway URL"
644
+ }), /* @__PURE__ */ jsx("p", {
645
+ className: "text-sm text-muted-foreground",
646
+ children: "Base URL for API requests through the gateway."
647
+ })]
648
+ }), /* @__PURE__ */ jsx(Secret, {
649
+ className: "max-w-full w-full",
650
+ secret: credentials.gatewayBaseUrl,
651
+ status: "none",
652
+ noReveal: true
653
+ })]
654
+ })
655
+ ]
656
+ }) : null,
657
+ /* @__PURE__ */ jsxs("div", {
658
+ className: "mt-8 flex flex-wrap gap-3",
659
+ children: [/* @__PURE__ */ jsx(Button$1, {
660
+ variant: "outline",
661
+ asChild: true,
662
+ children: /* @__PURE__ */ jsx(Link, {
663
+ to: "/account/subscriptions",
664
+ children: "My subscriptions"
665
+ })
666
+ }), /* @__PURE__ */ jsx(Button$1, {
667
+ variant: "outline",
668
+ asChild: true,
669
+ children: /* @__PURE__ */ jsx(Link, {
670
+ to: "/api",
671
+ children: "API reference"
672
+ })
673
+ })]
674
+ })
675
+ ]
338
676
  });
339
- }
677
+ };
340
678
 
341
679
  //#endregion
342
680
  //#region src/pages/PricingPage.tsx
@@ -346,6 +684,10 @@ const PricingPage = () => {
346
684
  const isPreview = useDevPortalPreviewMode();
347
685
  const { data: catalog, isLoading, isError } = usePlans();
348
686
  const { data: me } = useMySubscriptions();
687
+ if (auth.isAuthenticated) return /* @__PURE__ */ jsx(Navigate, {
688
+ to: "/account/plans",
689
+ replace: true
690
+ });
349
691
  const activeSkus = new Set((me?.subscriptions ?? []).filter((sub) => isActiveSubscription(sub.status)).map((sub) => sub.planSku));
350
692
  const title = pricing?.title ?? "Pricing";
351
693
  const subtitle = pricing?.subtitle ?? "Compare plans and subscribe when you are ready to integrate.";
@@ -392,22 +734,12 @@ const PricingPage = () => {
392
734
  className: "grid grid-cols-1 justify-center gap-6 sm:grid-cols-[repeat(auto-fit,minmax(280px,max-content))]",
393
735
  children: catalog.plans.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
394
736
  plan,
737
+ mode: "public",
395
738
  isSubscribed: activeSkus.has(plan.sku)
396
739
  }, plan.sku))
397
740
  }) : !isLoading && !isError ? /* @__PURE__ */ jsx("div", {
398
741
  className: "mx-auto max-w-lg rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
399
742
  children: "No plans are available yet. Sync plans from your APItoGo manifest or platform gateway."
400
- }) : null,
401
- auth.isAuthenticated ? /* @__PURE__ */ jsx("div", {
402
- className: "mt-10 text-center",
403
- children: /* @__PURE__ */ jsx(Button$1, {
404
- variant: "outline",
405
- asChild: true,
406
- children: /* @__PURE__ */ jsx(Link, {
407
- to: "/account/plans",
408
- children: "Manage billing"
409
- })
410
- })
411
743
  }) : null
412
744
  ]
413
745
  });
@@ -437,9 +769,18 @@ const SubscriptionsPage = () => {
437
769
  children: "Review your active plans and manage billing."
438
770
  })]
439
771
  }),
440
- checkoutSuccess ? /* @__PURE__ */ jsx("div", {
772
+ checkoutSuccess ? /* @__PURE__ */ jsxs("div", {
441
773
  className: "mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-sm",
442
- children: "Your subscription is active. API credentials are available from your account dashboard."
774
+ children: [
775
+ "Your subscription is active.",
776
+ " ",
777
+ /* @__PURE__ */ jsx(Link, {
778
+ to: CREDENTIALS_PATH,
779
+ className: "font-medium underline",
780
+ children: "View your API credentials"
781
+ }),
782
+ "."
783
+ ]
443
784
  }) : null,
444
785
  isLoading ? /* @__PURE__ */ jsx("p", {
445
786
  className: "text-muted-foreground",
@@ -454,7 +795,7 @@ const SubscriptionsPage = () => {
454
795
  className: "mt-4",
455
796
  asChild: true,
456
797
  children: /* @__PURE__ */ jsx(Link, {
457
- to: "/pricing",
798
+ to: "/account/plans",
458
799
  children: "View plans"
459
800
  })
460
801
  })]
@@ -500,7 +841,7 @@ const SubscriptionsPage = () => {
500
841
  variant: "outline",
501
842
  asChild: true,
502
843
  children: /* @__PURE__ */ jsx(Link, {
503
- to: "/pricing",
844
+ to: "/account/plans",
504
845
  children: "Change plan"
505
846
  })
506
847
  }),
@@ -516,8 +857,8 @@ const SubscriptionsPage = () => {
516
857
  variant: "secondary",
517
858
  asChild: true,
518
859
  children: /* @__PURE__ */ jsx(Link, {
519
- to: "/account/dashboard",
520
- children: "Account dashboard"
860
+ to: CREDENTIALS_PATH,
861
+ children: "API credentials"
521
862
  })
522
863
  }) : null
523
864
  ]
@@ -528,13 +869,11 @@ const SubscriptionsPage = () => {
528
869
 
529
870
  //#endregion
530
871
  //#region src/DevPortalBillingPlugin.tsx
531
- const PRICING_PATH = "/pricing";
532
- const CHECKOUT_PATH = "/checkout";
533
- const SUBSCRIPTIONS_PATH = "/account/subscriptions";
534
872
  const devPortalBillingPlugin = createPlugin((options = {}) => ({
535
873
  transformConfig: ({ config, merge }) => merge({ header: { navigation: [...config.header?.navigation ?? [], {
536
874
  label: "Pricing",
537
- to: PRICING_PATH
875
+ to: PRICING_PATH,
876
+ display: "anon"
538
877
  }] } }),
539
878
  getProfileMenuItems: () => [{
540
879
  label: "My Subscriptions",
@@ -542,6 +881,12 @@ const devPortalBillingPlugin = createPlugin((options = {}) => ({
542
881
  icon: StarsIcon,
543
882
  category: "middle",
544
883
  weight: 25
884
+ }, {
885
+ label: "API credentials",
886
+ path: CREDENTIALS_PATH,
887
+ icon: KeyRoundIcon,
888
+ category: "middle",
889
+ weight: 26
545
890
  }],
546
891
  getRoutes: () => [{
547
892
  element: /* @__PURE__ */ jsx(DevPortalBillingWrapper, { options }),
@@ -561,11 +906,20 @@ const devPortalBillingPlugin = createPlugin((options = {}) => ({
561
906
  path: SUBSCRIPTIONS_PATH,
562
907
  handle: { layout: "default" },
563
908
  element: /* @__PURE__ */ jsx(SubscriptionsPage, {})
909
+ },
910
+ {
911
+ path: CREDENTIALS_PATH,
912
+ handle: { layout: "default" },
913
+ element: /* @__PURE__ */ jsx(CredentialsPage, {})
564
914
  }
565
915
  ]
566
916
  }],
567
- getProtectedRoutes: () => [CHECKOUT_PATH, SUBSCRIPTIONS_PATH]
917
+ getProtectedRoutes: () => [
918
+ CHECKOUT_PATH,
919
+ SUBSCRIPTIONS_PATH,
920
+ CREDENTIALS_PATH
921
+ ]
568
922
  }));
569
923
 
570
924
  //#endregion
571
- export { CHECKOUT_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
925
+ export { AccountPlansPage, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo-plugin-dev-portal-billing",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
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.38"
49
+ "@lukoweb/apitogo": "0.1.40"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "react": ">=19.2.0",