@lukoweb/apitogo-plugin-dev-portal-billing 0.2.31 → 0.2.34

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,256 @@
1
+ import { o as PRICING_PATH } from "./paths-ujy9wjVK.mjs";
2
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl, u as isPlanCustomPriced } from "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
3
+ import "./DevPortalBillingContext-CR83s4LO.mjs";
4
+ import { t as usePlans } from "./usePlans-DUex2tzT.mjs";
5
+ import { Link, useSearchParams } from "@lukoweb/apitogo/router";
6
+ import { useState } from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { useMutation } from "@lukoweb/apitogo/react-query";
9
+ import { useAuth } from "@lukoweb/apitogo/hooks";
10
+ import { Alert, AlertDescription, AlertTitle } from "@lukoweb/apitogo/ui/Alert";
11
+ import { Button } from "@lukoweb/apitogo/ui/Button";
12
+ import { Input } from "@lukoweb/apitogo/ui/Input";
13
+ import { Label } from "@lukoweb/apitogo/ui/Label";
14
+ import { Textarea } from "@lukoweb/apitogo/ui/Textarea";
15
+
16
+ //#region src/hooks/usePlanInquiry.ts
17
+ const PLAN_INQUIRY_RATE_LIMITED = "Too many messages were sent from your network. Please try again later.";
18
+ function useSubmitPlanInquiry() {
19
+ const apiBaseUrl = useDevPortalApiBaseUrl();
20
+ return useMutation({ mutationFn: async ({ planSku, inquiry }) => {
21
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
22
+ const response = await fetch(`${apiBaseUrl}/api/v1/plans/${encodeURIComponent(planSku)}/inquiries`, {
23
+ method: "POST",
24
+ headers: {
25
+ Accept: "application/json",
26
+ "Content-Type": "application/json"
27
+ },
28
+ credentials: "include",
29
+ body: JSON.stringify(inquiry)
30
+ });
31
+ if (response.status === 429) throw new Error(PLAN_INQUIRY_RATE_LIMITED);
32
+ if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Your message could not be sent. Please try again later.");
33
+ } });
34
+ }
35
+ async function readProblemDetail(response) {
36
+ try {
37
+ const body = await response.json();
38
+ return (body.errors ? Object.values(body.errors).flat()[0] : void 0) ?? body.detail ?? body.title ?? null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ //#endregion
45
+ //#region src/pages/ContactSalesPage.tsx
46
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
47
+ const validate = (form) => ({
48
+ ...form.name.trim() ? {} : { name: "Enter your name." },
49
+ ...EMAIL_PATTERN.test(form.email.trim()) ? {} : { email: "Enter a valid email address." },
50
+ ...form.message.trim() ? {} : { message: "Tell us what you need." }
51
+ });
52
+ const ContactSalesPage = () => {
53
+ const [searchParams] = useSearchParams();
54
+ const planSku = searchParams.get("planSku") ?? "";
55
+ const auth = useAuth();
56
+ const isPreview = useDevPortalPreviewMode();
57
+ const isDemoSession = useIsDemoSession();
58
+ const plans = usePlans();
59
+ const submit = useSubmitPlanInquiry();
60
+ const [form, setForm] = useState(() => ({
61
+ name: auth.profile?.name ?? "",
62
+ email: auth.profile?.email ?? "",
63
+ company: "",
64
+ message: "",
65
+ website: ""
66
+ }));
67
+ const [errors, setErrors] = useState({});
68
+ const plan = plans.data?.plans.find((p) => p.sku === planSku);
69
+ const isSimulated = isPreview || isDemoSession;
70
+ if (!planSku || plans.isSuccess && (!plan || !isPlanCustomPriced(plan))) return /* @__PURE__ */ jsxs("div", {
71
+ className: "mx-auto max-w-lg px-4 py-16 text-center text-muted-foreground",
72
+ children: [
73
+ "This plan is not available for sales inquiries.",
74
+ " ",
75
+ /* @__PURE__ */ jsx(Link, {
76
+ to: PRICING_PATH,
77
+ className: "text-primary underline",
78
+ children: "Back to pricing"
79
+ })
80
+ ]
81
+ });
82
+ if (submit.isSuccess) return /* @__PURE__ */ jsxs("div", {
83
+ className: "mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center",
84
+ children: [
85
+ /* @__PURE__ */ jsx("h1", {
86
+ className: "text-2xl font-semibold tracking-tight",
87
+ children: "Thanks, we got your message"
88
+ }),
89
+ /* @__PURE__ */ jsxs("p", {
90
+ className: "text-muted-foreground",
91
+ children: [
92
+ "The team will reply to ",
93
+ form.email.trim(),
94
+ " soon."
95
+ ]
96
+ }),
97
+ /* @__PURE__ */ jsx(Button, {
98
+ asChild: true,
99
+ variant: "outline",
100
+ children: /* @__PURE__ */ jsx(Link, {
101
+ to: PRICING_PATH,
102
+ children: "Back to pricing"
103
+ })
104
+ })
105
+ ]
106
+ });
107
+ const update = (field) => (event) => setForm((prev) => ({
108
+ ...prev,
109
+ [field]: event.target.value
110
+ }));
111
+ const handleSubmit = (event) => {
112
+ event.preventDefault();
113
+ const nextErrors = validate(form);
114
+ setErrors(nextErrors);
115
+ if (Object.keys(nextErrors).length > 0 || isSimulated) return;
116
+ submit.mutate({
117
+ planSku,
118
+ inquiry: {
119
+ name: form.name.trim(),
120
+ email: form.email.trim(),
121
+ company: form.company.trim() || void 0,
122
+ message: form.message.trim(),
123
+ website: form.website || void 0
124
+ }
125
+ });
126
+ };
127
+ return /* @__PURE__ */ jsxs("div", {
128
+ className: "mx-auto w-full max-w-lg px-4 py-12",
129
+ children: [
130
+ /* @__PURE__ */ jsx("h1", {
131
+ className: "text-2xl font-semibold tracking-tight",
132
+ children: "Contact sales"
133
+ }),
134
+ /* @__PURE__ */ jsx("p", {
135
+ className: "mt-2 text-muted-foreground",
136
+ children: plan ? `Tell us about your needs for the ${plan.displayName} plan and we'll get back to you with pricing.` : "Tell us about your needs and we'll get back to you with pricing."
137
+ }),
138
+ isSimulated ? /* @__PURE__ */ jsxs(Alert, {
139
+ className: "mt-6",
140
+ children: [/* @__PURE__ */ jsx(AlertTitle, { children: "Preview only" }), /* @__PURE__ */ jsx(AlertDescription, { children: "Messages are sent once your portal is published to API ToGo." })]
141
+ }) : null,
142
+ /* @__PURE__ */ jsxs("form", {
143
+ className: "mt-6 space-y-4",
144
+ noValidate: true,
145
+ onSubmit: handleSubmit,
146
+ children: [
147
+ /* @__PURE__ */ jsxs("div", {
148
+ className: "space-y-1.5",
149
+ children: [
150
+ /* @__PURE__ */ jsx(Label, {
151
+ htmlFor: "contact-name",
152
+ children: "Name"
153
+ }),
154
+ /* @__PURE__ */ jsx(Input, {
155
+ id: "contact-name",
156
+ autoComplete: "name",
157
+ maxLength: 200,
158
+ value: form.name,
159
+ onChange: update("name"),
160
+ "aria-invalid": Boolean(errors.name)
161
+ }),
162
+ errors.name ? /* @__PURE__ */ jsx("p", {
163
+ className: "text-sm text-destructive",
164
+ children: errors.name
165
+ }) : null
166
+ ]
167
+ }),
168
+ /* @__PURE__ */ jsxs("div", {
169
+ className: "space-y-1.5",
170
+ children: [
171
+ /* @__PURE__ */ jsx(Label, {
172
+ htmlFor: "contact-email",
173
+ children: "Work email"
174
+ }),
175
+ /* @__PURE__ */ jsx(Input, {
176
+ id: "contact-email",
177
+ type: "email",
178
+ autoComplete: "email",
179
+ maxLength: 320,
180
+ value: form.email,
181
+ onChange: update("email"),
182
+ "aria-invalid": Boolean(errors.email)
183
+ }),
184
+ errors.email ? /* @__PURE__ */ jsx("p", {
185
+ className: "text-sm text-destructive",
186
+ children: errors.email
187
+ }) : null
188
+ ]
189
+ }),
190
+ /* @__PURE__ */ jsxs("div", {
191
+ className: "space-y-1.5",
192
+ children: [/* @__PURE__ */ jsx(Label, {
193
+ htmlFor: "contact-company",
194
+ children: "Company (optional)"
195
+ }), /* @__PURE__ */ jsx(Input, {
196
+ id: "contact-company",
197
+ autoComplete: "organization",
198
+ maxLength: 200,
199
+ value: form.company,
200
+ onChange: update("company")
201
+ })]
202
+ }),
203
+ /* @__PURE__ */ jsxs("div", {
204
+ className: "space-y-1.5",
205
+ children: [
206
+ /* @__PURE__ */ jsx(Label, {
207
+ htmlFor: "contact-message",
208
+ children: "Message"
209
+ }),
210
+ /* @__PURE__ */ jsx(Textarea, {
211
+ id: "contact-message",
212
+ rows: 5,
213
+ maxLength: 5e3,
214
+ placeholder: "Expected traffic, use case, timeline…",
215
+ value: form.message,
216
+ onChange: update("message"),
217
+ "aria-invalid": Boolean(errors.message)
218
+ }),
219
+ errors.message ? /* @__PURE__ */ jsx("p", {
220
+ className: "text-sm text-destructive",
221
+ children: errors.message
222
+ }) : null
223
+ ]
224
+ }),
225
+ /* @__PURE__ */ jsxs("div", {
226
+ className: "absolute -left-[9999px]",
227
+ "aria-hidden": "true",
228
+ children: [/* @__PURE__ */ jsx("label", {
229
+ htmlFor: "contact-website",
230
+ children: "Website"
231
+ }), /* @__PURE__ */ jsx("input", {
232
+ id: "contact-website",
233
+ tabIndex: -1,
234
+ autoComplete: "off",
235
+ value: form.website,
236
+ onChange: update("website")
237
+ })]
238
+ }),
239
+ submit.isError ? /* @__PURE__ */ jsx(Alert, {
240
+ variant: "destructive",
241
+ children: /* @__PURE__ */ jsx(AlertDescription, { children: submit.error.message })
242
+ }) : null,
243
+ /* @__PURE__ */ jsx(Button, {
244
+ type: "submit",
245
+ className: "w-full",
246
+ disabled: submit.isPending || isSimulated,
247
+ children: submit.isPending ? "Sending…" : "Send message"
248
+ })
249
+ ]
250
+ })
251
+ ]
252
+ });
253
+ };
254
+
255
+ //#endregion
256
+ export { ContactSalesPage as default };
@@ -0,0 +1,189 @@
1
+ import { t as ACCOUNT_PLANS_PATH } from "./paths-ujy9wjVK.mjs";
2
+ import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
3
+ import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
4
+ import { n as createDemoCredentials } from "./demo-session-Cez740bi.mjs";
5
+ import { r as useApiKeys, t as ApiKeysPanel } from "./ApiKeysPanel-DrArkHXf.mjs";
6
+ import { Head } from "@lukoweb/apitogo/components";
7
+ import { KeyRoundIcon } from "@lukoweb/apitogo/icons";
8
+ import { Link as Link$1 } from "@lukoweb/apitogo/router";
9
+ import zudokuConfig from "virtual:zudoku-config";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { useQuery } from "@lukoweb/apitogo/react-query";
12
+ import { useAuth } from "@lukoweb/apitogo/hooks";
13
+ import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button";
14
+ import { Value } from "@lukoweb/apitogo/ui/Value.js";
15
+
16
+ //#region src/hooks/useCredentials.ts
17
+ function useCredentials() {
18
+ const auth = useAuth();
19
+ const apiBaseUrl = useDevPortalApiBaseUrl();
20
+ const isPreview = useDevPortalPreviewMode();
21
+ const isDemoSession = useIsDemoSession();
22
+ const { previewPlans } = useDevPortalBillingConfig();
23
+ const liveQuery = useQuery({
24
+ queryKey: ["dev-portal-credentials", apiBaseUrl],
25
+ enabled: auth.isAuthenticated && !isPreview && !isDemoSession && Boolean(apiBaseUrl),
26
+ queryFn: async () => {
27
+ const response = await fetch(`${apiBaseUrl}/api/v1/credentials`, {
28
+ method: "GET",
29
+ headers: { Accept: "application/json" },
30
+ credentials: "include"
31
+ });
32
+ if (response.status === 404) throw new Error("NO_ENTITLEMENT");
33
+ if (!response.ok) throw new Error("Failed to load API credentials.");
34
+ return await response.json();
35
+ },
36
+ retry: (failureCount, error) => error instanceof Error && error.message === "NO_ENTITLEMENT" ? false : failureCount < 2
37
+ });
38
+ if (isDemoSession && auth.isAuthenticated) return {
39
+ ...liveQuery,
40
+ data: createDemoCredentials(previewPlans),
41
+ error: null,
42
+ isError: false,
43
+ isFetching: false,
44
+ isLoading: false,
45
+ isPending: false,
46
+ isSuccess: true,
47
+ status: "success"
48
+ };
49
+ return liveQuery;
50
+ }
51
+
52
+ //#endregion
53
+ //#region src/pages/CredentialsPage.tsx
54
+ const CredentialsPage = () => {
55
+ const auth = useAuth();
56
+ const isPreview = useDevPortalPreviewMode();
57
+ const isDemoSession = useIsDemoSession();
58
+ const { data: credentials, isLoading, error } = useCredentials();
59
+ const { data: apiKeys, isLoading: keysLoading, error: keysError } = useApiKeys();
60
+ const { gatewayBaseUrl: configuredGatewayBaseUrl } = useDevPortalBillingConfig();
61
+ const gatewayBaseUrl = credentials?.gatewayBaseUrl?.trim() || apiKeys?.gatewayBaseUrl?.trim() || configuredGatewayBaseUrl?.trim() || zudokuConfig.__meta?.gatewayBaseUrl?.trim() || null;
62
+ const header = apiKeys?.header ?? "Ocp-Apim-Subscription-Key";
63
+ const plans = apiKeys?.plans ?? [];
64
+ return /* @__PURE__ */ jsxs("div", {
65
+ className: "mx-auto max-w-screen-lg px-4 pb-16 pt-(--padding-content-top) lg:px-8",
66
+ children: [
67
+ /* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "API credentials" }) }),
68
+ /* @__PURE__ */ jsxs("div", {
69
+ className: "mb-8 flex items-start gap-4",
70
+ children: [/* @__PURE__ */ jsx("div", {
71
+ className: "flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary",
72
+ children: /* @__PURE__ */ jsx(KeyRoundIcon, { size: 24 })
73
+ }), /* @__PURE__ */ jsxs("div", {
74
+ className: "space-y-1",
75
+ children: [/* @__PURE__ */ jsx("h1", {
76
+ className: "text-3xl font-semibold tracking-tight",
77
+ children: "API credentials"
78
+ }), /* @__PURE__ */ jsx("p", {
79
+ className: "text-muted-foreground",
80
+ children: "Create API keys for each subscribed plan and use them to authenticate requests to the API gateway."
81
+ })]
82
+ })]
83
+ }),
84
+ isDemoSession ? /* @__PURE__ */ jsx("div", {
85
+ className: "mb-6 rounded-xl border border-dashed bg-muted/20 p-4 text-sm text-muted-foreground",
86
+ children: "Local demo credentials. Publish the portal to receive live subscription keys from your dev-portal backend."
87
+ }) : null,
88
+ isPreview && !isDemoSession ? /* @__PURE__ */ jsx("div", {
89
+ className: "rounded-xl border border-dashed p-8 text-center",
90
+ children: /* @__PURE__ */ jsx("p", {
91
+ className: "text-muted-foreground",
92
+ children: "API credentials are available after the developer portal is published."
93
+ })
94
+ }) : null,
95
+ !isPreview && !isDemoSession && !auth.isAuthenticated ? /* @__PURE__ */ jsxs("div", {
96
+ className: "rounded-xl border border-dashed p-8 text-center",
97
+ children: [/* @__PURE__ */ jsx("p", {
98
+ className: "text-muted-foreground",
99
+ children: "Sign in to view your subscription keys."
100
+ }), /* @__PURE__ */ jsx(Button$1, {
101
+ className: "mt-4",
102
+ asChild: true,
103
+ children: /* @__PURE__ */ jsx(Link$1, {
104
+ to: ACCOUNT_PLANS_PATH,
105
+ children: "View plans"
106
+ })
107
+ })]
108
+ }) : null,
109
+ !isPreview && !isDemoSession && auth.isAuthenticated && (isLoading || keysLoading) ? /* @__PURE__ */ jsx("p", {
110
+ className: "text-muted-foreground",
111
+ children: "Loading credentials…"
112
+ }) : null,
113
+ !isPreview && !isDemoSession && auth.isAuthenticated && (error || keysError) ? /* @__PURE__ */ jsxs("div", {
114
+ className: "rounded-xl border border-dashed p-8 text-center",
115
+ children: [/* @__PURE__ */ jsx("p", {
116
+ className: "text-muted-foreground",
117
+ children: error instanceof Error && error.message === "NO_ENTITLEMENT" || keysError instanceof Error && keysError.message === "NO_ENTITLEMENT" ? "No active subscription found. Choose a plan to get your API key." : "Unable to load API credentials. Try again in a moment."
118
+ }), /* @__PURE__ */ jsx(Button$1, {
119
+ className: "mt-4",
120
+ asChild: true,
121
+ children: /* @__PURE__ */ jsx(Link$1, {
122
+ to: ACCOUNT_PLANS_PATH,
123
+ children: "View plans"
124
+ })
125
+ })]
126
+ }) : null,
127
+ (isDemoSession || !isPreview) && auth.isAuthenticated && !error && !keysError && !isLoading && !keysLoading ? /* @__PURE__ */ jsxs("div", {
128
+ className: "space-y-6",
129
+ children: [plans.length === 0 ? /* @__PURE__ */ jsxs("div", {
130
+ className: "rounded-xl border border-dashed p-8 text-center",
131
+ children: [/* @__PURE__ */ jsx("p", {
132
+ className: "text-muted-foreground",
133
+ children: "Choose a plan to get your first API key."
134
+ }), /* @__PURE__ */ jsx(Button$1, {
135
+ className: "mt-4",
136
+ asChild: true,
137
+ children: /* @__PURE__ */ jsx(Link$1, {
138
+ to: ACCOUNT_PLANS_PATH,
139
+ children: "View plans"
140
+ })
141
+ })]
142
+ }) : plans.map((plan) => /* @__PURE__ */ jsx(ApiKeysPanel, {
143
+ plan,
144
+ header,
145
+ readOnly: isDemoSession
146
+ }, plan.planSku)), /* @__PURE__ */ jsxs("article", {
147
+ className: "rounded-xl border bg-card/60 p-6 space-y-4",
148
+ children: [/* @__PURE__ */ jsxs("div", {
149
+ className: "space-y-1",
150
+ children: [/* @__PURE__ */ jsx("h2", {
151
+ className: "text-lg font-medium",
152
+ children: "Gateway URL"
153
+ }), /* @__PURE__ */ jsx("p", {
154
+ className: "text-sm text-muted-foreground",
155
+ children: "Base URL for API requests through the gateway."
156
+ })]
157
+ }), gatewayBaseUrl ? /* @__PURE__ */ jsx(Value, {
158
+ className: "max-w-full w-full",
159
+ value: gatewayBaseUrl
160
+ }) : /* @__PURE__ */ jsx("p", {
161
+ className: "text-sm text-muted-foreground",
162
+ children: "Available after your API is deployed to the gateway."
163
+ })]
164
+ })]
165
+ }) : null,
166
+ /* @__PURE__ */ jsxs("div", {
167
+ className: "mt-8 flex flex-wrap gap-3",
168
+ children: [/* @__PURE__ */ jsx(Button$1, {
169
+ variant: "outline",
170
+ asChild: true,
171
+ children: /* @__PURE__ */ jsx(Link$1, {
172
+ to: "/account/subscriptions",
173
+ children: "My subscriptions"
174
+ })
175
+ }), /* @__PURE__ */ jsx(Button$1, {
176
+ variant: "outline",
177
+ asChild: true,
178
+ children: /* @__PURE__ */ jsx(Link$1, {
179
+ to: "/api",
180
+ children: "API reference"
181
+ })
182
+ })]
183
+ })
184
+ ]
185
+ });
186
+ };
187
+
188
+ //#endregion
189
+ export { CredentialsPage as default };
@@ -0,0 +1,13 @@
1
+ import { createContext, useContext } from "react";
2
+ import { jsx } from "react/jsx-runtime";
3
+
4
+ //#region src/DevPortalBillingContext.tsx
5
+ const DevPortalBillingContext = createContext({});
6
+ const DevPortalBillingProvider = ({ options, children }) => /* @__PURE__ */ jsx(DevPortalBillingContext.Provider, {
7
+ value: options,
8
+ children
9
+ });
10
+ const useDevPortalBillingConfig = () => useContext(DevPortalBillingContext);
11
+
12
+ //#endregion
13
+ export { useDevPortalBillingConfig as n, DevPortalBillingProvider as t };
@@ -0,0 +1,145 @@
1
+ import { n as CHECKOUT_PATH, r as CONTACT_SALES_PATH } from "./paths-ujy9wjVK.mjs";
2
+ import { a as formatPlanDisplayPrice, c as isActiveSubscription, d as isPlanFree, f as listPlanFeatureLines, s as formatPlanPriceUnit, u as isPlanCustomPriced } from "./useDevPortalApiBaseUrl-CgygF2z5.mjs";
3
+ import { Button } from "@lukoweb/apitogo/components";
4
+ import { Link as Link$1 } from "@lukoweb/apitogo/router";
5
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@lukoweb/apitogo/ui/AlertDialog.js";
6
+ import { cn } from "@lukoweb/apitogo/ui/util.js";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+
9
+ //#region src/components/PricingCard.tsx
10
+ const publicCtaLabel = (plan) => {
11
+ if (isPlanCustomPriced(plan)) return "Contact sales";
12
+ if (isPlanFree(plan)) return "Get started for free";
13
+ return "Subscribe now";
14
+ };
15
+ const accountCtaLabel = (plan) => {
16
+ if (isPlanCustomPriced(plan)) return "Contact sales";
17
+ return isPlanFree(plan) ? "Get started" : "Subscribe";
18
+ };
19
+ /** Custom-priced plans skip checkout and open the contact-sales form. */
20
+ const planCtaPath = (plan) => `${isPlanCustomPriced(plan) ? CONTACT_SALES_PATH : CHECKOUT_PATH}?planSku=${encodeURIComponent(plan.sku)}`;
21
+ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, onSwitch, isSwitching = false, isCanceling = false, cancelDisabled = false, featured = false }) => {
22
+ const isActive = isSubscribed || isActiveSubscription(subscriptionStatus ?? "");
23
+ const features = listPlanFeatureLines(plan);
24
+ const displayPrice = formatPlanDisplayPrice(plan);
25
+ return /* @__PURE__ */ jsxs("article", {
26
+ className: cn("flex h-full w-full flex-col rounded-xl border border-border bg-card p-7", featured && "border-primary/50 shadow-[0_10px_28px_rgba(13,148,136,0.28)] ring-1 ring-primary/25", isActive && mode === "account" && "border-primary/40 ring-1 ring-primary/20"),
27
+ "data-testid": `pricing-card-${plan.sku}`,
28
+ children: [
29
+ /* @__PURE__ */ jsxs("div", {
30
+ className: "flex items-center justify-between gap-2",
31
+ children: [
32
+ /* @__PURE__ */ jsx("h2", {
33
+ className: "text-sm font-bold uppercase tracking-[0.04em]",
34
+ children: plan.displayName
35
+ }),
36
+ featured && !isActive ? /* @__PURE__ */ jsx("span", {
37
+ className: "shrink-0 rounded-md bg-primary/10 px-2.5 py-0.5 text-xs font-semibold text-primary",
38
+ children: "Popular"
39
+ }) : null,
40
+ isActive && mode === "account" ? /* @__PURE__ */ jsx("span", {
41
+ className: "shrink-0 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary",
42
+ "data-testid": `subscribed-badge-${plan.sku}`,
43
+ children: "Subscribed"
44
+ }) : null
45
+ ]
46
+ }),
47
+ /* @__PURE__ */ jsx("p", {
48
+ className: "mt-3.5 text-[2.1rem] font-extrabold tracking-[-0.02em]",
49
+ children: displayPrice
50
+ }),
51
+ /* @__PURE__ */ jsx("p", {
52
+ className: "mb-[18px] mt-0.5 text-[0.78125rem] text-muted-foreground",
53
+ children: formatPlanPriceUnit(plan)
54
+ }),
55
+ /* @__PURE__ */ jsx("div", {
56
+ className: "space-y-2",
57
+ children: mode === "account" ? isActive ? /* @__PURE__ */ jsxs(Fragment, { children: [
58
+ /* @__PURE__ */ jsx(Button, {
59
+ variant: "secondary",
60
+ className: "w-full",
61
+ disabled: true,
62
+ children: "Subscribed"
63
+ }),
64
+ onUnsubscribe ? /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
65
+ asChild: true,
66
+ children: /* @__PURE__ */ jsx(Button, {
67
+ variant: "outline",
68
+ className: "w-full",
69
+ disabled: cancelDisabled || isCanceling,
70
+ "data-testid": `unsubscribe-${plan.sku}`,
71
+ children: isCanceling ? "Canceling…" : "Unsubscribe"
72
+ })
73
+ }), /* @__PURE__ */ jsxs(AlertDialogContent, { children: [/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [/* @__PURE__ */ jsxs(AlertDialogTitle, { children: [
74
+ "Unsubscribe from ",
75
+ plan.displayName,
76
+ "?"
77
+ ] }), /* @__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, {
78
+ onClick: () => onUnsubscribe(plan.sku),
79
+ disabled: isCanceling,
80
+ children: "Unsubscribe"
81
+ })] })] })] }) : null,
82
+ cancelDisabled ? /* @__PURE__ */ jsx("p", {
83
+ className: "text-center text-xs text-muted-foreground",
84
+ children: "Unsubscribe is available after the portal is published."
85
+ }) : null
86
+ ] }) : onSwitch && !isPlanCustomPriced(plan) ? /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
87
+ asChild: true,
88
+ children: /* @__PURE__ */ jsx(Button, {
89
+ variant: featured ? "default" : "outline",
90
+ className: "w-full",
91
+ disabled: isSwitching,
92
+ "data-testid": `switch-${plan.sku}`,
93
+ children: isSwitching ? "Switching…" : "Switch to this plan"
94
+ })
95
+ }), /* @__PURE__ */ jsxs(AlertDialogContent, { children: [/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [/* @__PURE__ */ jsxs(AlertDialogTitle, { children: [
96
+ "Switch to ",
97
+ plan.displayName,
98
+ "?"
99
+ ] }), /* @__PURE__ */ jsxs(AlertDialogDescription, { children: [
100
+ "Your current plan is replaced by ",
101
+ plan.displayName,
102
+ ". The API keys of your current plan stop working. Create a key for",
103
+ " ",
104
+ plan.displayName,
105
+ " on the API keys page."
106
+ ] })] }), /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [/* @__PURE__ */ jsx(AlertDialogCancel, { children: "Keep current plan" }), /* @__PURE__ */ jsx(AlertDialogAction, {
107
+ onClick: () => onSwitch(plan.sku),
108
+ disabled: isSwitching,
109
+ children: "Switch plan"
110
+ })] })] })] }) : /* @__PURE__ */ jsx(Button, {
111
+ variant: featured ? "default" : "outline",
112
+ className: "w-full",
113
+ asChild: true,
114
+ children: /* @__PURE__ */ jsx(Link$1, {
115
+ to: planCtaPath(plan),
116
+ children: accountCtaLabel(plan)
117
+ })
118
+ }) : /* @__PURE__ */ jsx(Button, {
119
+ variant: featured ? "default" : "outline",
120
+ className: "w-full",
121
+ asChild: true,
122
+ children: /* @__PURE__ */ jsx(Link$1, {
123
+ to: planCtaPath(plan),
124
+ children: publicCtaLabel(plan)
125
+ })
126
+ })
127
+ }),
128
+ features.length > 0 ? /* @__PURE__ */ jsx("div", {
129
+ className: "mt-5 flex flex-1 flex-col gap-[11px] border-t border-border pt-[18px]",
130
+ "data-testid": plan.includedActionKeys && plan.includedActionKeys.length > 0 ? `included-endpoints-${plan.sku}` : void 0,
131
+ children: features.map((feat) => /* @__PURE__ */ jsxs("div", {
132
+ className: "flex items-start gap-2.5 text-[0.84375rem] text-muted-foreground",
133
+ children: [/* @__PURE__ */ jsx("span", {
134
+ className: "mt-0.5 shrink-0 text-emerald-600",
135
+ "aria-hidden": true,
136
+ children: "✓"
137
+ }), /* @__PURE__ */ jsx("span", { children: feat })]
138
+ }, feat))
139
+ }) : null
140
+ ]
141
+ });
142
+ };
143
+
144
+ //#endregion
145
+ export { PricingCard as t };