@lukoweb/apitogo-plugin-dev-portal-billing 0.1.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.
@@ -0,0 +1,17 @@
1
+ import * as _lukoweb_apitogo0 from "@lukoweb/apitogo";
2
+
3
+ //#region src/types.d.ts
4
+ type DevPortalBillingConfig = {
5
+ pricing?: {
6
+ title?: string;
7
+ subtitle?: string;
8
+ };
9
+ };
10
+ //#endregion
11
+ //#region src/DevPortalBillingPlugin.d.ts
12
+ declare const PRICING_PATH = "/pricing";
13
+ declare const CHECKOUT_PATH = "/checkout";
14
+ declare const SUBSCRIPTIONS_PATH = "/account/subscriptions";
15
+ declare const devPortalBillingPlugin: (options?: DevPortalBillingConfig | undefined) => _lukoweb_apitogo0.ApitogoPlugin;
16
+ //#endregion
17
+ export { CHECKOUT_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
package/dist/index.mjs ADDED
@@ -0,0 +1,571 @@
1
+ import { cn, createPlugin } from "@lukoweb/apitogo";
2
+ import { ShieldIcon, StarsIcon } from "@lukoweb/apitogo/icons";
3
+ 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";
7
+ import { useAuth } from "@lukoweb/apitogo/hooks";
8
+ import { useQuery } from "@lukoweb/apitogo/react-query";
9
+ import { Alert, AlertAction, AlertDescription } from "@lukoweb/apitogo/ui/Alert";
10
+ import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button";
11
+
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
+ //#region src/utils/dev-portal-api.ts
71
+ const DEV_PORTAL_PLACEHOLDER_API_URL = "https://dp-example.azurecontainerapps.io";
72
+ function resolveDevPortalApiBaseUrl(configuredUrl) {
73
+ const fromConfig = configuredUrl?.trim();
74
+ if (fromConfig) return fromConfig.replace(/\/+$/, "");
75
+ const envUrl = typeof VITE_APITOGO_DEV_PORTAL_API_URL === "string" ? VITE_APITOGO_DEV_PORTAL_API_URL.trim() : "";
76
+ return envUrl ? envUrl.replace(/\/+$/, "") : void 0;
77
+ }
78
+ function isPlaceholderDevPortalApiUrl(apiBaseUrl) {
79
+ if (!apiBaseUrl) return true;
80
+ return apiBaseUrl === DEV_PORTAL_PLACEHOLDER_API_URL || apiBaseUrl.includes("dp-example.azurecontainerapps.io");
81
+ }
82
+ function parseMockPlans() {
83
+ const raw = typeof VITE_APITOGO_DEV_PORTAL_MOCK_PLANS === "string" ? VITE_APITOGO_DEV_PORTAL_MOCK_PLANS.trim() : "";
84
+ if (!raw) return { plans: [] };
85
+ try {
86
+ return { plans: JSON.parse(raw).map((plan) => ({
87
+ sku: plan.sku,
88
+ displayName: plan.displayName,
89
+ isFree: plan.isFree ?? false,
90
+ priceAmount: plan.priceAmount ?? 0,
91
+ priceCurrency: plan.priceCurrency ?? "usd",
92
+ billingPeriod: plan.billingPeriod ?? "monthly",
93
+ limitsJson: plan.limitsJson ?? null
94
+ })) };
95
+ } catch {
96
+ return { plans: [] };
97
+ }
98
+ }
99
+ function formatPlanPrice(plan) {
100
+ if (plan.isFree || plan.priceAmount <= 0) return "Free";
101
+ const amount = new Intl.NumberFormat(void 0, {
102
+ style: "currency",
103
+ currency: plan.priceCurrency.toUpperCase(),
104
+ minimumFractionDigits: plan.priceAmount % 1 === 0 ? 0 : 2
105
+ }).format(plan.priceAmount);
106
+ const period = plan.billingPeriod.toLowerCase();
107
+ if (period === "monthly" || period === "month") return `${amount}/mo`;
108
+ if (period === "annual" || period === "year") return `${amount}/yr`;
109
+ return amount;
110
+ }
111
+ function formatBillingPeriod(period) {
112
+ const normalized = period.toLowerCase();
113
+ if (normalized === "month" || normalized === "monthly") return "Monthly";
114
+ if (normalized === "year" || normalized === "annual") return "Annual";
115
+ return period;
116
+ }
117
+ function formatRateLimit(rate) {
118
+ if (!rate || typeof rate !== "object") return null;
119
+ const { calls, renewalPeriodSeconds } = rate;
120
+ if (calls == null || renewalPeriodSeconds == null) return null;
121
+ const window = renewalPeriodSeconds === 60 ? "minute" : `${renewalPeriodSeconds} seconds`;
122
+ return `${calls} request${calls === 1 ? "" : "s"} per ${window}`;
123
+ }
124
+ function summarizeLimits(limitsJson) {
125
+ if (!limitsJson?.trim()) return null;
126
+ try {
127
+ const limits = JSON.parse(limitsJson);
128
+ const rateSummary = formatRateLimit(limits.rateLimit);
129
+ if (rateSummary) return rateSummary;
130
+ const entries = Object.entries(limits).filter(([, value]) => value != null && typeof value !== "object");
131
+ if (entries.length === 0) return null;
132
+ return entries.slice(0, 3).map(([key, value]) => `${key}: ${String(value)}`).join(" · ");
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+ function isActiveSubscription(status) {
138
+ return status.toLowerCase() === "active";
139
+ }
140
+
141
+ //#endregion
142
+ //#region src/hooks/useDevPortalApiBaseUrl.ts
143
+ function useDevPortalApiBaseUrl() {
144
+ const providerData = useAuth().providerData;
145
+ if (providerData?.type === "dev-portal" && providerData.apiBaseUrl) return providerData.apiBaseUrl;
146
+ return resolveDevPortalApiBaseUrl();
147
+ }
148
+ function useDevPortalPreviewMode() {
149
+ const auth = useAuth();
150
+ const apiBaseUrl = useDevPortalApiBaseUrl();
151
+ if (auth.authMode === "preview" || auth.isBackendAvailable === false) return true;
152
+ return isPlaceholderDevPortalApiUrl(apiBaseUrl);
153
+ }
154
+
155
+ //#endregion
156
+ //#region src/pages/CheckoutPage.tsx
157
+ const CheckoutPage = () => {
158
+ const [searchParams] = useSearchParams();
159
+ const planSku = searchParams.get("planSku") ?? "";
160
+ const navigate = useNavigate();
161
+ const auth = useAuth();
162
+ const apiBaseUrl = useDevPortalApiBaseUrl();
163
+ const isPreview = useDevPortalPreviewMode();
164
+ const checkoutQuery = useQuery({
165
+ queryKey: [
166
+ "dev-portal-checkout",
167
+ apiBaseUrl,
168
+ planSku,
169
+ auth.profile?.sub
170
+ ],
171
+ enabled: Boolean(planSku) && !isPreview && auth.isAuthenticated && Boolean(apiBaseUrl),
172
+ retry: false,
173
+ queryFn: async () => {
174
+ const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/checkout`, {
175
+ method: "POST",
176
+ headers: {
177
+ Accept: "application/json",
178
+ "Content-Type": "application/json"
179
+ },
180
+ credentials: "include",
181
+ body: JSON.stringify({ planSku })
182
+ });
183
+ if (!response.ok) {
184
+ const detail = await response.text();
185
+ throw new Error(detail || "Checkout failed.");
186
+ }
187
+ return await response.json();
188
+ }
189
+ });
190
+ const freeProvisioned = checkoutQuery.data?.isFree === true && checkoutQuery.data.entitlementProvisioned === true;
191
+ useEffect(() => {
192
+ if (freeProvisioned) navigate("/account/subscriptions?checkout=success", { replace: true });
193
+ }, [freeProvisioned, navigate]);
194
+ if (!planSku) return /* @__PURE__ */ jsxs("div", {
195
+ className: "mx-auto max-w-lg px-4 py-16 text-center text-muted-foreground",
196
+ children: [
197
+ "Missing plan SKU.",
198
+ " ",
199
+ /* @__PURE__ */ jsx(Link, {
200
+ to: "/pricing",
201
+ className: "text-primary underline",
202
+ children: "Back to pricing"
203
+ })
204
+ ]
205
+ });
206
+ if (isPreview) return /* @__PURE__ */ jsxs("div", {
207
+ className: "mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center",
208
+ children: [
209
+ /* @__PURE__ */ jsx("h1", {
210
+ className: "text-2xl font-semibold tracking-tight",
211
+ children: "Checkout unlocks after publish"
212
+ }),
213
+ /* @__PURE__ */ jsx("p", {
214
+ className: "text-muted-foreground",
215
+ children: "Subscribe and billing flows require a live dev-portal backend. Publish your portal to APItoGo, then return here to complete checkout."
216
+ }),
217
+ /* @__PURE__ */ jsx(Button$1, {
218
+ asChild: true,
219
+ variant: "outline",
220
+ children: /* @__PURE__ */ jsx(Link, {
221
+ to: "/pricing",
222
+ children: "Back to pricing"
223
+ })
224
+ })
225
+ ]
226
+ });
227
+ if (freeProvisioned) return null;
228
+ return /* @__PURE__ */ jsx(RedirectPage, {
229
+ icon: ShieldIcon,
230
+ title: "Establishing secure checkout…",
231
+ description: "Redirecting you to complete your subscription.",
232
+ url: checkoutQuery.data?.checkoutUrl ?? void 0,
233
+ children: checkoutQuery.isError ? /* @__PURE__ */ jsxs(Alert, {
234
+ variant: "destructive",
235
+ children: [/* @__PURE__ */ jsx(AlertDescription, {
236
+ className: "first-letter:uppercase",
237
+ children: checkoutQuery.error.message
238
+ }), /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(Button$1, {
239
+ variant: "outline",
240
+ size: "xs",
241
+ asChild: true,
242
+ children: /* @__PURE__ */ jsx(Link, {
243
+ to: "/pricing",
244
+ children: "Back to pricing"
245
+ })
246
+ }) })]
247
+ }) : null
248
+ });
249
+ };
250
+
251
+ //#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() {
302
+ const auth = useAuth();
303
+ const apiBaseUrl = useDevPortalApiBaseUrl();
304
+ const isPreview = useDevPortalPreviewMode();
305
+ return useQuery({
306
+ queryKey: ["dev-portal-me", apiBaseUrl],
307
+ enabled: auth.isAuthenticated && !isPreview && Boolean(apiBaseUrl),
308
+ queryFn: async () => {
309
+ const response = await fetch(`${apiBaseUrl}/api/v1/auth/me`, {
310
+ method: "GET",
311
+ headers: { Accept: "application/json" },
312
+ credentials: "include"
313
+ });
314
+ if (!response.ok) throw new Error("Failed to load subscription status.");
315
+ return await response.json();
316
+ }
317
+ });
318
+ }
319
+
320
+ //#endregion
321
+ //#region src/hooks/usePlans.ts
322
+ function usePlans() {
323
+ const apiBaseUrl = useDevPortalApiBaseUrl();
324
+ 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
+ }
338
+ });
339
+ }
340
+
341
+ //#endregion
342
+ //#region src/pages/PricingPage.tsx
343
+ const PricingPage = () => {
344
+ const { pricing } = useDevPortalBillingConfig();
345
+ const auth = useAuth();
346
+ const isPreview = useDevPortalPreviewMode();
347
+ const { data: catalog, isLoading, isError } = usePlans();
348
+ const { data: me } = useMySubscriptions();
349
+ const activeSkus = new Set((me?.subscriptions ?? []).filter((sub) => isActiveSubscription(sub.status)).map((sub) => sub.planSku));
350
+ const title = pricing?.title ?? "Pricing";
351
+ const subtitle = pricing?.subtitle ?? "Compare plans and subscribe when you are ready to integrate.";
352
+ return /* @__PURE__ */ jsxs("div", {
353
+ className: "w-full px-4 pt-(--padding-content-top) pb-(--padding-content-bottom)",
354
+ children: [
355
+ /* @__PURE__ */ jsxs(Head, { children: [/* @__PURE__ */ jsx("title", { children: title }), /* @__PURE__ */ jsx("meta", {
356
+ name: "description",
357
+ content: subtitle
358
+ })] }),
359
+ /* @__PURE__ */ jsxs("div", {
360
+ className: "mb-12 space-y-4 text-center",
361
+ children: [/* @__PURE__ */ jsx(Heading, {
362
+ level: 1,
363
+ "data-testid": "title",
364
+ children: title
365
+ }), /* @__PURE__ */ jsx("p", {
366
+ className: "text-muted-foreground",
367
+ "data-testid": "subtitle",
368
+ children: subtitle
369
+ })]
370
+ }),
371
+ isPreview && !auth.isAuthenticated ? /* @__PURE__ */ jsxs("div", {
372
+ className: "mx-auto mb-8 max-w-2xl rounded-xl border border-dashed bg-muted/20 p-6 text-center text-sm text-muted-foreground",
373
+ children: [
374
+ "Local preview uses mock plans from",
375
+ " ",
376
+ /* @__PURE__ */ jsx("code", {
377
+ className: "rounded bg-muted px-1.5 py-0.5",
378
+ children: "VITE_APITOGO_DEV_PORTAL_MOCK_PLANS"
379
+ }),
380
+ ". Checkout activates after the portal is published."
381
+ ]
382
+ }) : null,
383
+ isLoading ? /* @__PURE__ */ jsx("p", {
384
+ className: "text-center text-muted-foreground",
385
+ children: "Loading plans…"
386
+ }) : null,
387
+ isError ? /* @__PURE__ */ jsx("p", {
388
+ className: "text-center text-destructive",
389
+ children: "Unable to load plans."
390
+ }) : null,
391
+ catalog && catalog.plans.length > 0 ? /* @__PURE__ */ jsx("div", {
392
+ className: "grid grid-cols-1 justify-center gap-6 sm:grid-cols-[repeat(auto-fit,minmax(280px,max-content))]",
393
+ children: catalog.plans.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
394
+ plan,
395
+ isSubscribed: activeSkus.has(plan.sku)
396
+ }, plan.sku))
397
+ }) : !isLoading && !isError ? /* @__PURE__ */ jsx("div", {
398
+ className: "mx-auto max-w-lg rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
399
+ 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
+ }) : null
412
+ ]
413
+ });
414
+ };
415
+
416
+ //#endregion
417
+ //#region src/pages/SubscriptionsPage.tsx
418
+ const SubscriptionsPage = () => {
419
+ const auth = useAuth();
420
+ const [searchParams] = useSearchParams();
421
+ const checkoutSuccess = searchParams.get("checkout") === "success";
422
+ const { data: me, isLoading } = useMySubscriptions();
423
+ const { data: catalog } = usePlans();
424
+ const planBySku = new Map((catalog?.plans ?? []).map((plan) => [plan.sku, plan]));
425
+ const subscriptions = me?.subscriptions ?? [];
426
+ return /* @__PURE__ */ jsxs("div", {
427
+ className: "mx-auto max-w-screen-lg px-4 pb-16 pt-(--padding-content-top) lg:px-8",
428
+ children: [
429
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "My subscriptions" }) }),
430
+ /* @__PURE__ */ jsxs("div", {
431
+ className: "mb-8 space-y-2",
432
+ children: [/* @__PURE__ */ jsx("h1", {
433
+ className: "text-3xl font-semibold tracking-tight",
434
+ children: "My subscriptions"
435
+ }), /* @__PURE__ */ jsx("p", {
436
+ className: "text-muted-foreground",
437
+ children: "Review your active plans and manage billing."
438
+ })]
439
+ }),
440
+ checkoutSuccess ? /* @__PURE__ */ jsx("div", {
441
+ 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."
443
+ }) : null,
444
+ isLoading ? /* @__PURE__ */ jsx("p", {
445
+ className: "text-muted-foreground",
446
+ children: "Loading subscriptions…"
447
+ }) : null,
448
+ !isLoading && subscriptions.length === 0 ? /* @__PURE__ */ jsxs("div", {
449
+ className: "rounded-xl border border-dashed p-8 text-center",
450
+ children: [/* @__PURE__ */ jsx("p", {
451
+ className: "text-muted-foreground",
452
+ children: "You do not have an active subscription yet."
453
+ }), /* @__PURE__ */ jsx(Button$1, {
454
+ className: "mt-4",
455
+ asChild: true,
456
+ children: /* @__PURE__ */ jsx(Link, {
457
+ to: "/pricing",
458
+ children: "View plans"
459
+ })
460
+ })]
461
+ }) : null,
462
+ subscriptions.length > 0 ? /* @__PURE__ */ jsx("div", {
463
+ className: "space-y-4",
464
+ children: subscriptions.map((subscription) => {
465
+ const plan = planBySku.get(subscription.planSku);
466
+ return /* @__PURE__ */ jsxs("article", {
467
+ className: "rounded-xl border bg-card/60 p-6",
468
+ children: [/* @__PURE__ */ jsxs("div", {
469
+ className: "flex flex-wrap items-start justify-between gap-4",
470
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
471
+ className: "text-lg font-medium",
472
+ children: plan?.displayName ?? subscription.planSku
473
+ }), plan ? /* @__PURE__ */ jsxs("p", {
474
+ className: "mt-1 text-sm text-muted-foreground",
475
+ children: [
476
+ formatPlanPrice(plan),
477
+ " ·",
478
+ " ",
479
+ formatBillingPeriod(plan.billingPeriod)
480
+ ]
481
+ }) : null] }), /* @__PURE__ */ jsx("span", {
482
+ className: isActiveSubscription(subscription.status) ? "rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary" : "rounded-full bg-muted px-3 py-1 text-xs font-medium text-muted-foreground",
483
+ children: subscription.status
484
+ })]
485
+ }), subscription.currentPeriodEnd ? /* @__PURE__ */ jsxs("p", {
486
+ className: "mt-3 text-sm text-muted-foreground",
487
+ children: [
488
+ "Current period ends",
489
+ " ",
490
+ new Date(subscription.currentPeriodEnd).toLocaleDateString()
491
+ ]
492
+ }) : null]
493
+ }, `${subscription.planSku}-${subscription.status}`);
494
+ })
495
+ }) : null,
496
+ /* @__PURE__ */ jsxs("div", {
497
+ className: "mt-8 flex flex-wrap gap-3",
498
+ children: [
499
+ /* @__PURE__ */ jsx(Button$1, {
500
+ variant: "outline",
501
+ asChild: true,
502
+ children: /* @__PURE__ */ jsx(Link, {
503
+ to: "/pricing",
504
+ children: "Change plan"
505
+ })
506
+ }),
507
+ /* @__PURE__ */ jsx(Button$1, {
508
+ variant: "outline",
509
+ asChild: true,
510
+ children: /* @__PURE__ */ jsx(Link, {
511
+ to: "/account/plans",
512
+ children: "Billing settings"
513
+ })
514
+ }),
515
+ auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
516
+ variant: "secondary",
517
+ asChild: true,
518
+ children: /* @__PURE__ */ jsx(Link, {
519
+ to: "/account/dashboard",
520
+ children: "Account dashboard"
521
+ })
522
+ }) : null
523
+ ]
524
+ })
525
+ ]
526
+ });
527
+ };
528
+
529
+ //#endregion
530
+ //#region src/DevPortalBillingPlugin.tsx
531
+ const PRICING_PATH = "/pricing";
532
+ const CHECKOUT_PATH = "/checkout";
533
+ const SUBSCRIPTIONS_PATH = "/account/subscriptions";
534
+ const devPortalBillingPlugin = createPlugin((options = {}) => ({
535
+ transformConfig: ({ config, merge }) => merge({ header: { navigation: [...config.header?.navigation ?? [], {
536
+ label: "Pricing",
537
+ to: PRICING_PATH
538
+ }] } }),
539
+ getProfileMenuItems: () => [{
540
+ label: "My Subscriptions",
541
+ path: SUBSCRIPTIONS_PATH,
542
+ icon: StarsIcon,
543
+ category: "middle",
544
+ weight: 25
545
+ }],
546
+ getRoutes: () => [{
547
+ element: /* @__PURE__ */ jsx(DevPortalBillingWrapper, { options }),
548
+ handle: { layout: "none" },
549
+ children: [
550
+ {
551
+ path: PRICING_PATH,
552
+ handle: { layout: "default" },
553
+ element: /* @__PURE__ */ jsx(PricingPage, {})
554
+ },
555
+ {
556
+ path: CHECKOUT_PATH,
557
+ handle: { layout: "default" },
558
+ element: /* @__PURE__ */ jsx(CheckoutPage, {})
559
+ },
560
+ {
561
+ path: SUBSCRIPTIONS_PATH,
562
+ handle: { layout: "default" },
563
+ element: /* @__PURE__ */ jsx(SubscriptionsPage, {})
564
+ }
565
+ ]
566
+ }],
567
+ getProtectedRoutes: () => [CHECKOUT_PATH, SUBSCRIPTIONS_PATH]
568
+ }));
569
+
570
+ //#endregion
571
+ export { CHECKOUT_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@lukoweb/apitogo-plugin-dev-portal-billing",
3
+ "version": "0.1.37",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/zuplo/zudoku",
7
+ "directory": "packages/plugin-dev-portal-billing"
8
+ },
9
+ "type": "module",
10
+ "main": "./dist/index.mjs",
11
+ "types": "./dist/index.d.mts",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.mjs",
15
+ "types": "./dist/index.d.mts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsdown",
23
+ "dev": "tsdown --watch",
24
+ "prepublishOnly": "pnpm typecheck && pnpm build",
25
+ "typecheck": "tsc --project tsconfig.json",
26
+ "test": "vitest run"
27
+ },
28
+ "publishConfig": {
29
+ "main": "./dist/index.mjs",
30
+ "types": "./dist/index.d.mts",
31
+ "exports": {
32
+ ".": {
33
+ "import": "./dist/index.mjs",
34
+ "types": "./dist/index.d.mts"
35
+ }
36
+ },
37
+ "access": "public"
38
+ },
39
+ "devDependencies": {
40
+ "@testing-library/dom": "10.4.1",
41
+ "@testing-library/jest-dom": "6.9.1",
42
+ "@testing-library/react": "16.3.2",
43
+ "@types/react": "19.2.14",
44
+ "@types/react-dom": "19.2.3",
45
+ "happy-dom": "20.8.3",
46
+ "react": "19.2.4",
47
+ "react-dom": "19.2.4",
48
+ "tsdown": "0.20.3",
49
+ "@lukoweb/apitogo": "0.1.37"
50
+ },
51
+ "peerDependencies": {
52
+ "react": ">=19.2.0",
53
+ "react-dom": ">=19.2.0",
54
+ "@lukoweb/apitogo": "*"
55
+ }
56
+ }