@lukoweb/apitogo-plugin-dev-portal-billing 0.1.39 → 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 +5 -1
- package/dist/index.mjs +331 -183
- package/package.json +2 -2
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?: {
|
|
@@ -17,4 +21,4 @@ declare const CREDENTIALS_PATH = "/account/credentials";
|
|
|
17
21
|
//#region src/DevPortalBillingPlugin.d.ts
|
|
18
22
|
declare const devPortalBillingPlugin: (options?: DevPortalBillingConfig | undefined) => _lukoweb_apitogo0.ApitogoPlugin;
|
|
19
23
|
//#endregion
|
|
20
|
-
export { CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
|
|
24
|
+
export { AccountPlansPage, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
|
package/dist/index.mjs
CHANGED
|
@@ -1,73 +1,16 @@
|
|
|
1
|
-
import { cn, createPlugin } from "@lukoweb/apitogo";
|
|
2
|
-
import { KeyRoundIcon, ShieldIcon, StarsIcon } from "@lukoweb/apitogo/icons";
|
|
3
1
|
import { Button, ClientOnly, Head, Heading } from "@lukoweb/apitogo/components";
|
|
4
|
-
import {
|
|
2
|
+
import { CreditCardIcon, KeyRoundIcon, ShieldIcon, StarsIcon } from "@lukoweb/apitogo/icons";
|
|
5
3
|
import { createContext, useContext, useEffect, useState } from "react";
|
|
6
|
-
import {
|
|
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";
|
|
11
12
|
import { Secret } from "@lukoweb/apitogo/ui/Secret.js";
|
|
12
13
|
|
|
13
|
-
//#region src/DevPortalBillingContext.tsx
|
|
14
|
-
const DevPortalBillingContext = createContext({});
|
|
15
|
-
const DevPortalBillingProvider = ({ options, children }) => /* @__PURE__ */ jsx(DevPortalBillingContext.Provider, {
|
|
16
|
-
value: options,
|
|
17
|
-
children
|
|
18
|
-
});
|
|
19
|
-
const useDevPortalBillingConfig = () => useContext(DevPortalBillingContext);
|
|
20
|
-
|
|
21
|
-
//#endregion
|
|
22
|
-
//#region src/DevPortalBillingWrapper.tsx
|
|
23
|
-
const DevPortalBillingWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(DevPortalBillingProvider, {
|
|
24
|
-
options,
|
|
25
|
-
children: /* @__PURE__ */ jsx(ClientOnly, { children: /* @__PURE__ */ jsx(Outlet, {}) })
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
//#endregion
|
|
29
|
-
//#region src/components/RedirectPage.tsx
|
|
30
|
-
const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
if (url) window.location.assign(url);
|
|
33
|
-
}, [url]);
|
|
34
|
-
return /* @__PURE__ */ jsx("div", {
|
|
35
|
-
className: "flex min-h-screen items-center justify-center bg-muted",
|
|
36
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
37
|
-
className: "flex max-w-md flex-col items-center space-y-6 text-center",
|
|
38
|
-
children: [
|
|
39
|
-
/* @__PURE__ */ jsx("div", {
|
|
40
|
-
className: "relative",
|
|
41
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
42
|
-
className: "flex h-24 w-24 items-center justify-center rounded-full bg-foreground/10",
|
|
43
|
-
children: /* @__PURE__ */ jsx(Icon, { className: "h-12 w-12 text-foreground" })
|
|
44
|
-
})
|
|
45
|
-
}),
|
|
46
|
-
children,
|
|
47
|
-
/* @__PURE__ */ jsxs("div", {
|
|
48
|
-
className: "space-y-2",
|
|
49
|
-
children: [/* @__PURE__ */ jsx("h2", {
|
|
50
|
-
className: "text-2xl font-bold text-card-foreground",
|
|
51
|
-
children: title
|
|
52
|
-
}), /* @__PURE__ */ jsx("p", {
|
|
53
|
-
className: "text-muted-foreground",
|
|
54
|
-
children: description
|
|
55
|
-
})]
|
|
56
|
-
}),
|
|
57
|
-
/* @__PURE__ */ jsxs("div", {
|
|
58
|
-
className: "flex space-x-2",
|
|
59
|
-
children: [
|
|
60
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.3s]" }),
|
|
61
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.15s]" }),
|
|
62
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary" })
|
|
63
|
-
]
|
|
64
|
-
})
|
|
65
|
-
]
|
|
66
|
-
})
|
|
67
|
-
});
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
//#endregion
|
|
71
14
|
//#region src/utils/dev-portal-api.ts
|
|
72
15
|
const DEV_PORTAL_PLACEHOLDER_API_URL = "https://dp-example.azurecontainerapps.io";
|
|
73
16
|
function resolveDevPortalApiBaseUrl(configuredUrl) {
|
|
@@ -139,6 +82,99 @@ function isActiveSubscription(status) {
|
|
|
139
82
|
return status.toLowerCase() === "active";
|
|
140
83
|
}
|
|
141
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
|
+
|
|
142
178
|
//#endregion
|
|
143
179
|
//#region src/hooks/useDevPortalApiBaseUrl.ts
|
|
144
180
|
function useDevPortalApiBaseUrl() {
|
|
@@ -153,8 +189,221 @@ function useDevPortalPreviewMode() {
|
|
|
153
189
|
return isPlaceholderDevPortalApiUrl(apiBaseUrl);
|
|
154
190
|
}
|
|
155
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
|
+
|
|
156
404
|
//#endregion
|
|
157
405
|
//#region src/pages/CheckoutPage.tsx
|
|
406
|
+
const ACCOUNT_PLANS_PATH = "/account/plans";
|
|
158
407
|
const CheckoutPage = () => {
|
|
159
408
|
const [searchParams] = useSearchParams();
|
|
160
409
|
const planSku = searchParams.get("planSku") ?? "";
|
|
@@ -162,6 +411,7 @@ const CheckoutPage = () => {
|
|
|
162
411
|
const auth = useAuth();
|
|
163
412
|
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
164
413
|
const isPreview = useDevPortalPreviewMode();
|
|
414
|
+
const plansPath = auth.isAuthenticated ? ACCOUNT_PLANS_PATH : PRICING_PATH;
|
|
165
415
|
const checkoutQuery = useQuery({
|
|
166
416
|
queryKey: [
|
|
167
417
|
"dev-portal-checkout",
|
|
@@ -198,9 +448,9 @@ const CheckoutPage = () => {
|
|
|
198
448
|
"Missing plan SKU.",
|
|
199
449
|
" ",
|
|
200
450
|
/* @__PURE__ */ jsx(Link, {
|
|
201
|
-
to:
|
|
451
|
+
to: plansPath,
|
|
202
452
|
className: "text-primary underline",
|
|
203
|
-
children: "Back to
|
|
453
|
+
children: "Back to plans"
|
|
204
454
|
})
|
|
205
455
|
]
|
|
206
456
|
});
|
|
@@ -219,8 +469,8 @@ const CheckoutPage = () => {
|
|
|
219
469
|
asChild: true,
|
|
220
470
|
variant: "outline",
|
|
221
471
|
children: /* @__PURE__ */ jsx(Link, {
|
|
222
|
-
to:
|
|
223
|
-
children: "Back to
|
|
472
|
+
to: plansPath,
|
|
473
|
+
children: "Back to plans"
|
|
224
474
|
})
|
|
225
475
|
})
|
|
226
476
|
]
|
|
@@ -241,8 +491,8 @@ const CheckoutPage = () => {
|
|
|
241
491
|
size: "xs",
|
|
242
492
|
asChild: true,
|
|
243
493
|
children: /* @__PURE__ */ jsx(Link, {
|
|
244
|
-
to:
|
|
245
|
-
children: "Back to
|
|
494
|
+
to: plansPath,
|
|
495
|
+
children: "Back to plans"
|
|
246
496
|
})
|
|
247
497
|
}) })]
|
|
248
498
|
}) : null
|
|
@@ -272,27 +522,6 @@ function useCredentials() {
|
|
|
272
522
|
});
|
|
273
523
|
}
|
|
274
524
|
|
|
275
|
-
//#endregion
|
|
276
|
-
//#region src/hooks/usePlans.ts
|
|
277
|
-
function usePlans() {
|
|
278
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
279
|
-
const isPreview = useDevPortalPreviewMode();
|
|
280
|
-
return useQuery({
|
|
281
|
-
queryKey: [
|
|
282
|
-
"dev-portal-plans",
|
|
283
|
-
apiBaseUrl,
|
|
284
|
-
isPreview
|
|
285
|
-
],
|
|
286
|
-
queryFn: async () => {
|
|
287
|
-
if (isPreview) return parseMockPlans();
|
|
288
|
-
if (!apiBaseUrl) return { plans: [] };
|
|
289
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/plans`, { headers: { Accept: "application/json" } });
|
|
290
|
-
if (!response.ok) throw new Error("Failed to load plans.");
|
|
291
|
-
return await response.json();
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
525
|
//#endregion
|
|
297
526
|
//#region src/pages/CredentialsPage.tsx
|
|
298
527
|
const CredentialsPage = () => {
|
|
@@ -338,7 +567,7 @@ const CredentialsPage = () => {
|
|
|
338
567
|
className: "mt-4",
|
|
339
568
|
asChild: true,
|
|
340
569
|
children: /* @__PURE__ */ jsx(Link, {
|
|
341
|
-
to: "/
|
|
570
|
+
to: "/account/plans",
|
|
342
571
|
children: "View plans"
|
|
343
572
|
})
|
|
344
573
|
})]
|
|
@@ -356,7 +585,7 @@ const CredentialsPage = () => {
|
|
|
356
585
|
className: "mt-4",
|
|
357
586
|
asChild: true,
|
|
358
587
|
children: /* @__PURE__ */ jsx(Link, {
|
|
359
|
-
to: "/
|
|
588
|
+
to: "/account/plans",
|
|
360
589
|
children: "View plans"
|
|
361
590
|
})
|
|
362
591
|
})]
|
|
@@ -447,75 +676,6 @@ const CredentialsPage = () => {
|
|
|
447
676
|
});
|
|
448
677
|
};
|
|
449
678
|
|
|
450
|
-
//#endregion
|
|
451
|
-
//#region src/components/PricingCard.tsx
|
|
452
|
-
const PricingCard = ({ plan, isSubscribed = false }) => {
|
|
453
|
-
const limitsSummary = summarizeLimits(plan.limitsJson);
|
|
454
|
-
return /* @__PURE__ */ jsxs("article", {
|
|
455
|
-
className: cn("flex w-full max-w-sm flex-col rounded-xl border bg-card/60 p-6 shadow-xs"),
|
|
456
|
-
"data-testid": `pricing-card-${plan.sku}`,
|
|
457
|
-
children: [
|
|
458
|
-
/* @__PURE__ */ jsx("h2", {
|
|
459
|
-
className: "text-xl font-semibold",
|
|
460
|
-
children: plan.displayName
|
|
461
|
-
}),
|
|
462
|
-
/* @__PURE__ */ jsx("p", {
|
|
463
|
-
className: "mt-2 text-3xl font-bold",
|
|
464
|
-
children: formatPlanPrice(plan)
|
|
465
|
-
}),
|
|
466
|
-
/* @__PURE__ */ jsx("p", {
|
|
467
|
-
className: "mt-1 text-sm text-muted-foreground",
|
|
468
|
-
children: formatBillingPeriod(plan.billingPeriod)
|
|
469
|
-
}),
|
|
470
|
-
limitsSummary ? /* @__PURE__ */ jsx("p", {
|
|
471
|
-
className: "mt-4 text-sm text-muted-foreground",
|
|
472
|
-
children: limitsSummary
|
|
473
|
-
}) : null,
|
|
474
|
-
/* @__PURE__ */ jsx("div", {
|
|
475
|
-
className: "mt-6",
|
|
476
|
-
children: isSubscribed ? /* @__PURE__ */ jsx(Button, {
|
|
477
|
-
variant: "secondary",
|
|
478
|
-
className: "w-full",
|
|
479
|
-
asChild: true,
|
|
480
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
481
|
-
to: "/account/subscriptions",
|
|
482
|
-
children: "Manage subscription"
|
|
483
|
-
})
|
|
484
|
-
}) : /* @__PURE__ */ jsx(Button, {
|
|
485
|
-
variant: "default",
|
|
486
|
-
className: "w-full",
|
|
487
|
-
asChild: true,
|
|
488
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
489
|
-
to: `/checkout?planSku=${encodeURIComponent(plan.sku)}`,
|
|
490
|
-
children: plan.isFree ? "Get started" : "Subscribe"
|
|
491
|
-
})
|
|
492
|
-
})
|
|
493
|
-
})
|
|
494
|
-
]
|
|
495
|
-
});
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
//#endregion
|
|
499
|
-
//#region src/hooks/useMySubscriptions.ts
|
|
500
|
-
function useMySubscriptions() {
|
|
501
|
-
const auth = useAuth();
|
|
502
|
-
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
503
|
-
const isPreview = useDevPortalPreviewMode();
|
|
504
|
-
return useQuery({
|
|
505
|
-
queryKey: ["dev-portal-me", apiBaseUrl],
|
|
506
|
-
enabled: auth.isAuthenticated && !isPreview && Boolean(apiBaseUrl),
|
|
507
|
-
queryFn: async () => {
|
|
508
|
-
const response = await fetch(`${apiBaseUrl}/api/v1/auth/me`, {
|
|
509
|
-
method: "GET",
|
|
510
|
-
headers: { Accept: "application/json" },
|
|
511
|
-
credentials: "include"
|
|
512
|
-
});
|
|
513
|
-
if (!response.ok) throw new Error("Failed to load subscription status.");
|
|
514
|
-
return await response.json();
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
|
|
519
679
|
//#endregion
|
|
520
680
|
//#region src/pages/PricingPage.tsx
|
|
521
681
|
const PricingPage = () => {
|
|
@@ -524,6 +684,10 @@ const PricingPage = () => {
|
|
|
524
684
|
const isPreview = useDevPortalPreviewMode();
|
|
525
685
|
const { data: catalog, isLoading, isError } = usePlans();
|
|
526
686
|
const { data: me } = useMySubscriptions();
|
|
687
|
+
if (auth.isAuthenticated) return /* @__PURE__ */ jsx(Navigate, {
|
|
688
|
+
to: "/account/plans",
|
|
689
|
+
replace: true
|
|
690
|
+
});
|
|
527
691
|
const activeSkus = new Set((me?.subscriptions ?? []).filter((sub) => isActiveSubscription(sub.status)).map((sub) => sub.planSku));
|
|
528
692
|
const title = pricing?.title ?? "Pricing";
|
|
529
693
|
const subtitle = pricing?.subtitle ?? "Compare plans and subscribe when you are ready to integrate.";
|
|
@@ -570,34 +734,17 @@ const PricingPage = () => {
|
|
|
570
734
|
className: "grid grid-cols-1 justify-center gap-6 sm:grid-cols-[repeat(auto-fit,minmax(280px,max-content))]",
|
|
571
735
|
children: catalog.plans.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
|
|
572
736
|
plan,
|
|
737
|
+
mode: "public",
|
|
573
738
|
isSubscribed: activeSkus.has(plan.sku)
|
|
574
739
|
}, plan.sku))
|
|
575
740
|
}) : !isLoading && !isError ? /* @__PURE__ */ jsx("div", {
|
|
576
741
|
className: "mx-auto max-w-lg rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
|
|
577
742
|
children: "No plans are available yet. Sync plans from your APItoGo manifest or platform gateway."
|
|
578
|
-
}) : null,
|
|
579
|
-
auth.isAuthenticated ? /* @__PURE__ */ jsx("div", {
|
|
580
|
-
className: "mt-10 text-center",
|
|
581
|
-
children: /* @__PURE__ */ jsx(Button$1, {
|
|
582
|
-
variant: "outline",
|
|
583
|
-
asChild: true,
|
|
584
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
585
|
-
to: "/account/plans",
|
|
586
|
-
children: "Manage billing"
|
|
587
|
-
})
|
|
588
|
-
})
|
|
589
743
|
}) : null
|
|
590
744
|
]
|
|
591
745
|
});
|
|
592
746
|
};
|
|
593
747
|
|
|
594
|
-
//#endregion
|
|
595
|
-
//#region src/paths.ts
|
|
596
|
-
const PRICING_PATH = "/pricing";
|
|
597
|
-
const CHECKOUT_PATH = "/checkout";
|
|
598
|
-
const SUBSCRIPTIONS_PATH = "/account/subscriptions";
|
|
599
|
-
const CREDENTIALS_PATH = "/account/credentials";
|
|
600
|
-
|
|
601
748
|
//#endregion
|
|
602
749
|
//#region src/pages/SubscriptionsPage.tsx
|
|
603
750
|
const SubscriptionsPage = () => {
|
|
@@ -648,7 +795,7 @@ const SubscriptionsPage = () => {
|
|
|
648
795
|
className: "mt-4",
|
|
649
796
|
asChild: true,
|
|
650
797
|
children: /* @__PURE__ */ jsx(Link, {
|
|
651
|
-
to: "/
|
|
798
|
+
to: "/account/plans",
|
|
652
799
|
children: "View plans"
|
|
653
800
|
})
|
|
654
801
|
})]
|
|
@@ -694,7 +841,7 @@ const SubscriptionsPage = () => {
|
|
|
694
841
|
variant: "outline",
|
|
695
842
|
asChild: true,
|
|
696
843
|
children: /* @__PURE__ */ jsx(Link, {
|
|
697
|
-
to: "/
|
|
844
|
+
to: "/account/plans",
|
|
698
845
|
children: "Change plan"
|
|
699
846
|
})
|
|
700
847
|
}),
|
|
@@ -725,7 +872,8 @@ const SubscriptionsPage = () => {
|
|
|
725
872
|
const devPortalBillingPlugin = createPlugin((options = {}) => ({
|
|
726
873
|
transformConfig: ({ config, merge }) => merge({ header: { navigation: [...config.header?.navigation ?? [], {
|
|
727
874
|
label: "Pricing",
|
|
728
|
-
to: PRICING_PATH
|
|
875
|
+
to: PRICING_PATH,
|
|
876
|
+
display: "anon"
|
|
729
877
|
}] } }),
|
|
730
878
|
getProfileMenuItems: () => [{
|
|
731
879
|
label: "My Subscriptions",
|
|
@@ -774,4 +922,4 @@ const devPortalBillingPlugin = createPlugin((options = {}) => ({
|
|
|
774
922
|
}));
|
|
775
923
|
|
|
776
924
|
//#endregion
|
|
777
|
-
export { CHECKOUT_PATH, CREDENTIALS_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.
|
|
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.
|
|
49
|
+
"@lukoweb/apitogo": "0.1.40"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"react": ">=19.2.0",
|