@lukoweb/apitogo-plugin-dev-portal-billing 0.1.39 → 0.1.41
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 +335 -184
- package/package.json +7 -9
- package/src/DevPortalBillingContext.tsx +19 -0
- package/src/DevPortalBillingPlugin.test.tsx +29 -0
- package/src/DevPortalBillingPlugin.tsx +87 -0
- package/src/DevPortalBillingWrapper.tsx +18 -0
- package/src/components/PricingCard.test.tsx +47 -0
- package/src/components/PricingCard.tsx +142 -0
- package/src/components/RedirectPage.tsx +47 -0
- package/src/env.d.ts +2 -0
- package/src/hooks/useCancelSubscription.ts +44 -0
- package/src/hooks/useCredentials.ts +39 -0
- package/src/hooks/useDevPortalApiBaseUrl.ts +33 -0
- package/src/hooks/useMySubscriptions.ts +31 -0
- package/src/hooks/usePlans.ts +35 -0
- package/src/index.ts +8 -0
- package/src/pages/AccountPlansPage.tsx +106 -0
- package/src/pages/CheckoutPage.tsx +127 -0
- package/src/pages/CredentialsPage.tsx +145 -0
- package/src/pages/PricingPage.tsx +87 -0
- package/src/pages/SubscriptionsPage.tsx +129 -0
- package/src/paths.ts +4 -0
- package/src/types.ts +50 -0
- package/src/utils/dev-portal-api.test.ts +89 -0
- package/src/utils/dev-portal-api.ts +156 -0
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
|
})]
|
|
@@ -416,11 +645,14 @@ const CredentialsPage = () => {
|
|
|
416
645
|
className: "text-sm text-muted-foreground",
|
|
417
646
|
children: "Base URL for API requests through the gateway."
|
|
418
647
|
})]
|
|
419
|
-
}), /* @__PURE__ */ jsx(Secret, {
|
|
648
|
+
}), credentials.gatewayBaseUrl ? /* @__PURE__ */ jsx(Secret, {
|
|
420
649
|
className: "max-w-full w-full",
|
|
421
650
|
secret: credentials.gatewayBaseUrl,
|
|
422
651
|
status: "none",
|
|
423
652
|
noReveal: true
|
|
653
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
654
|
+
className: "text-sm text-muted-foreground",
|
|
655
|
+
children: "Available after your API is deployed to the gateway."
|
|
424
656
|
})]
|
|
425
657
|
})
|
|
426
658
|
]
|
|
@@ -447,75 +679,6 @@ const CredentialsPage = () => {
|
|
|
447
679
|
});
|
|
448
680
|
};
|
|
449
681
|
|
|
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
682
|
//#endregion
|
|
520
683
|
//#region src/pages/PricingPage.tsx
|
|
521
684
|
const PricingPage = () => {
|
|
@@ -524,6 +687,10 @@ const PricingPage = () => {
|
|
|
524
687
|
const isPreview = useDevPortalPreviewMode();
|
|
525
688
|
const { data: catalog, isLoading, isError } = usePlans();
|
|
526
689
|
const { data: me } = useMySubscriptions();
|
|
690
|
+
if (auth.isAuthenticated) return /* @__PURE__ */ jsx(Navigate, {
|
|
691
|
+
to: "/account/plans",
|
|
692
|
+
replace: true
|
|
693
|
+
});
|
|
527
694
|
const activeSkus = new Set((me?.subscriptions ?? []).filter((sub) => isActiveSubscription(sub.status)).map((sub) => sub.planSku));
|
|
528
695
|
const title = pricing?.title ?? "Pricing";
|
|
529
696
|
const subtitle = pricing?.subtitle ?? "Compare plans and subscribe when you are ready to integrate.";
|
|
@@ -570,34 +737,17 @@ const PricingPage = () => {
|
|
|
570
737
|
className: "grid grid-cols-1 justify-center gap-6 sm:grid-cols-[repeat(auto-fit,minmax(280px,max-content))]",
|
|
571
738
|
children: catalog.plans.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
|
|
572
739
|
plan,
|
|
740
|
+
mode: "public",
|
|
573
741
|
isSubscribed: activeSkus.has(plan.sku)
|
|
574
742
|
}, plan.sku))
|
|
575
743
|
}) : !isLoading && !isError ? /* @__PURE__ */ jsx("div", {
|
|
576
744
|
className: "mx-auto max-w-lg rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground",
|
|
577
745
|
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
746
|
}) : null
|
|
590
747
|
]
|
|
591
748
|
});
|
|
592
749
|
};
|
|
593
750
|
|
|
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
751
|
//#endregion
|
|
602
752
|
//#region src/pages/SubscriptionsPage.tsx
|
|
603
753
|
const SubscriptionsPage = () => {
|
|
@@ -648,7 +798,7 @@ const SubscriptionsPage = () => {
|
|
|
648
798
|
className: "mt-4",
|
|
649
799
|
asChild: true,
|
|
650
800
|
children: /* @__PURE__ */ jsx(Link, {
|
|
651
|
-
to: "/
|
|
801
|
+
to: "/account/plans",
|
|
652
802
|
children: "View plans"
|
|
653
803
|
})
|
|
654
804
|
})]
|
|
@@ -694,7 +844,7 @@ const SubscriptionsPage = () => {
|
|
|
694
844
|
variant: "outline",
|
|
695
845
|
asChild: true,
|
|
696
846
|
children: /* @__PURE__ */ jsx(Link, {
|
|
697
|
-
to: "/
|
|
847
|
+
to: "/account/plans",
|
|
698
848
|
children: "Change plan"
|
|
699
849
|
})
|
|
700
850
|
}),
|
|
@@ -725,7 +875,8 @@ const SubscriptionsPage = () => {
|
|
|
725
875
|
const devPortalBillingPlugin = createPlugin((options = {}) => ({
|
|
726
876
|
transformConfig: ({ config, merge }) => merge({ header: { navigation: [...config.header?.navigation ?? [], {
|
|
727
877
|
label: "Pricing",
|
|
728
|
-
to: PRICING_PATH
|
|
878
|
+
to: PRICING_PATH,
|
|
879
|
+
display: "anon"
|
|
729
880
|
}] } }),
|
|
730
881
|
getProfileMenuItems: () => [{
|
|
731
882
|
label: "My Subscriptions",
|
|
@@ -774,4 +925,4 @@ const devPortalBillingPlugin = createPlugin((options = {}) => ({
|
|
|
774
925
|
}));
|
|
775
926
|
|
|
776
927
|
//#endregion
|
|
777
|
-
export { CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
|
|
928
|
+
export { AccountPlansPage, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
|