@lukoweb/zudoku-plugin-monetization 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.
package/dist/index.mjs ADDED
@@ -0,0 +1,2216 @@
1
+ import { cn, createPlugin, joinUrl, throwIfProblemJson } from "@lukoweb/apitogo";
2
+ import { AlertTriangleIcon, ArrowDownIcon, ArrowLeftRightIcon, ArrowUpIcon, CalendarIcon, CheckCheckIcon, CheckIcon, CircleAlert, CircleSlashIcon, ClockIcon, CreditCardIcon, Grid2x2XIcon, InfoIcon, Loader2Icon, LockIcon, MoreVerticalIcon, RefreshCcw, RefreshCwIcon, Settings, ShieldIcon, StarsIcon, Trash2Icon, XIcon } from "@lukoweb/apitogo/icons";
3
+ import { Button, ClientOnly, Head, Heading, Link, Slot } from "@lukoweb/apitogo/components";
4
+ import { useAuth, useZudoku } from "@lukoweb/apitogo/hooks";
5
+ import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient, useSuspenseQuery } from "@lukoweb/apitogo/react-query";
6
+ import { Link as Link$1, Outlet, useLocation, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
7
+ import { Alert, AlertAction, AlertDescription, AlertTitle } from "@lukoweb/apitogo/ui/Alert";
8
+ import { Card, CardContent, CardHeader, CardTitle } from "@lukoweb/apitogo/ui/Card";
9
+ import { Separator } from "@lukoweb/apitogo/ui/Separator";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ import { createContext, use, useEffect, useMemo, useState } from "react";
12
+ import { parse } from "tinyduration";
13
+ import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button";
14
+ import { DismissibleAlert, DismissibleAlertAction } from "@lukoweb/apitogo/ui/DismissibleAlert";
15
+ import { ActionButton } from "@lukoweb/apitogo/ui/ActionButton";
16
+ import { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "@lukoweb/apitogo/ui/Item";
17
+ import { Badge } from "@lukoweb/apitogo/ui/Badge";
18
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@lukoweb/apitogo/ui/DropdownMenu";
19
+ import { Frame, FrameFooter, FramePanel } from "@lukoweb/apitogo/ui/Frame";
20
+ import { Secret } from "@lukoweb/apitogo/ui/Secret";
21
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@lukoweb/apitogo/ui/AlertDialog";
22
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@lukoweb/apitogo/ui/Tooltip";
23
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@lukoweb/apitogo/ui/Dialog";
24
+ import { Input } from "@lukoweb/apitogo/ui/Input";
25
+ import { Progress } from "@lukoweb/apitogo/ui/Progress";
26
+
27
+ //#region src/components/FeatureItem.tsx
28
+ const FeatureItem = ({ feature, className }) => {
29
+ return /* @__PURE__ */ jsxs("div", {
30
+ className: cn("flex items-start gap-2", className),
31
+ children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("div", {
32
+ className: "text-sm",
33
+ children: feature.value ? /* @__PURE__ */ jsxs(Fragment, { children: [
34
+ /* @__PURE__ */ jsxs("span", {
35
+ className: "font-medium",
36
+ children: [feature.name, ":"]
37
+ }),
38
+ " ",
39
+ feature.value
40
+ ] }) : feature.name
41
+ })]
42
+ });
43
+ };
44
+
45
+ //#endregion
46
+ //#region src/components/QuotaItem.tsx
47
+ const QuotaItem = ({ quota, className }) => {
48
+ return /* @__PURE__ */ jsxs("div", {
49
+ className: cn("flex items-start gap-2", className),
50
+ children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsxs("div", {
51
+ className: "text-sm",
52
+ children: [
53
+ /* @__PURE__ */ jsxs("span", {
54
+ className: "font-medium",
55
+ children: [quota.name, ":"]
56
+ }),
57
+ " ",
58
+ quota.limit.toLocaleString(),
59
+ " / ",
60
+ quota.period,
61
+ quota.overagePrice && /* @__PURE__ */ jsxs("div", {
62
+ className: "text-xs text-muted-foreground mt-0.5",
63
+ children: [
64
+ "+",
65
+ quota.overagePrice,
66
+ " after quota"
67
+ ]
68
+ })
69
+ ]
70
+ })]
71
+ });
72
+ };
73
+
74
+ //#endregion
75
+ //#region src/hooks/useDeploymentName.ts
76
+ const useDeploymentName = () => {
77
+ const deploymentName = useZudoku().env.ZUPLO_PUBLIC_DEPLOYMENT_NAME;
78
+ if (!deploymentName) throw new Error("ZUPLO_PUBLIC_DEPLOYMENT_NAME is not set");
79
+ return deploymentName;
80
+ };
81
+
82
+ //#endregion
83
+ //#region src/hooks/usePlans.ts
84
+ const usePlans = () => {
85
+ const zudoku = useZudoku();
86
+ const auth = useAuth();
87
+ return useSuspenseQuery({
88
+ queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/pricing-page`],
89
+ meta: { context: auth.isAuthenticated ? zudoku : void 0 }
90
+ });
91
+ };
92
+
93
+ //#endregion
94
+ //#region src/MonetizationContext.tsx
95
+ const MonetizationContext = createContext({});
96
+ const useMonetizationConfig = () => use(MonetizationContext);
97
+
98
+ //#endregion
99
+ //#region src/utils/formatDuration.ts
100
+ const formatDuration = (iso) => {
101
+ try {
102
+ const d = parse(iso);
103
+ if (d.months === 1) return "month";
104
+ if (d.months && d.months > 1) return `${d.months} months`;
105
+ if (d.years === 1) return "year";
106
+ if (d.years && d.years > 1) return `${d.years} years`;
107
+ if (d.weeks === 1) return "week";
108
+ if (d.weeks && d.weeks > 1) return `${d.weeks} weeks`;
109
+ if (d.days === 1) return "day";
110
+ if (d.days && d.days > 1) return `${d.days} days`;
111
+ return iso;
112
+ } catch {
113
+ return iso;
114
+ }
115
+ };
116
+ const formatDurationInterval = (iso) => {
117
+ try {
118
+ const d = parse(iso);
119
+ if (d.years === 1) return "yearly";
120
+ if (d.years && d.years > 1) return `every ${d.years} years`;
121
+ if (d.months === 1) return "monthly";
122
+ if (d.months && d.months > 1) return `every ${d.months} months`;
123
+ if (d.weeks === 1) return "weekly";
124
+ if (d.weeks && d.weeks > 1) return `every ${d.weeks} weeks`;
125
+ if (d.days === 1) return "daily";
126
+ if (d.days && d.days > 1) return `every ${d.days} days`;
127
+ return iso;
128
+ } catch {
129
+ return iso;
130
+ }
131
+ };
132
+ /**
133
+ * Returns an adjective form suitable for possessive context
134
+ * e.g. "your monthly quota", "your weekly limit".
135
+ * Falls back to "billing period" for multi-unit cadences
136
+ * where "every 3 months" would be grammatically awkward.
137
+ */
138
+ const formatDurationAdjective = (iso) => {
139
+ try {
140
+ const d = parse(iso);
141
+ if (d.years === 1) return "yearly";
142
+ if (d.months === 1) return "monthly";
143
+ if (d.weeks === 1) return "weekly";
144
+ if (d.days === 1) return "daily";
145
+ return "billing period";
146
+ } catch {
147
+ return "billing period";
148
+ }
149
+ };
150
+
151
+ //#endregion
152
+ //#region src/utils/formatPrice.ts
153
+ const formatPrice = (amount, currency) => new Intl.NumberFormat("en-US", {
154
+ style: "currency",
155
+ currency: currency ?? "USD",
156
+ minimumFractionDigits: 2,
157
+ maximumFractionDigits: 6,
158
+ trailingZeroDisplay: "stripIfInteger"
159
+ }).format(amount);
160
+
161
+ //#endregion
162
+ //#region src/utils/categorizeRateCards.ts
163
+ const categorizeRateCards = (rateCards, options) => {
164
+ const { currency, units, planBillingCadence } = options ?? {};
165
+ const quotas = [];
166
+ const features = [];
167
+ for (const rc of rateCards) {
168
+ const et = rc.entitlementTemplate;
169
+ if (!et) continue;
170
+ if (et.type === "metered" && et.issueAfterReset != null) {
171
+ let overagePrice;
172
+ if (et.isSoftLimit !== false && rc.price?.type === "tiered" && rc.price.tiers) {
173
+ const overageTier = rc.price.tiers.find((t) => t.unitPrice?.amount && parseFloat(t.unitPrice.amount) > 0);
174
+ if (overageTier?.unitPrice) {
175
+ const amount = parseFloat(overageTier.unitPrice.amount);
176
+ const unitLabel = units?.[rc.key] ?? units?.[rc.featureKey ?? ""] ?? "unit";
177
+ overagePrice = `${formatPrice(amount, currency)}/${unitLabel}`;
178
+ }
179
+ }
180
+ quotas.push({
181
+ key: rc.featureKey ?? rc.key,
182
+ name: rc.name,
183
+ limit: et.issueAfterReset,
184
+ period: rc.billingCadence ? formatDuration(rc.billingCadence) : planBillingCadence ? formatDuration(planBillingCadence) : "month",
185
+ overagePrice
186
+ });
187
+ } else if (et.type === "boolean") features.push({
188
+ key: rc.featureKey ?? rc.key,
189
+ name: rc.name
190
+ });
191
+ else if (et.type === "static" && et.config) try {
192
+ const config = JSON.parse(et.config);
193
+ features.push({
194
+ key: rc.featureKey ?? rc.key,
195
+ name: rc.name,
196
+ value: String(config.value)
197
+ });
198
+ } catch {
199
+ features.push({
200
+ key: rc.featureKey ?? rc.key,
201
+ name: rc.name
202
+ });
203
+ }
204
+ }
205
+ return {
206
+ quotas,
207
+ features
208
+ };
209
+ };
210
+
211
+ //#endregion
212
+ //#region src/utils/formatBillingCycle.ts
213
+ const formatBillingCycle = (duration) => {
214
+ if (duration === "month") return "monthly";
215
+ if (duration === "year") return "annually";
216
+ if (duration === "week") return "weekly";
217
+ if (duration === "day") return "daily";
218
+ return `every ${duration}`;
219
+ };
220
+
221
+ //#endregion
222
+ //#region src/utils/getPriceFromPlan.ts
223
+ const getPriceFromPlan = (plan) => {
224
+ return {
225
+ monthly: plan.monthlyPrice != null ? parseFloat(plan.monthlyPrice) : 0,
226
+ yearly: plan.yearlyPrice != null ? parseFloat(plan.yearlyPrice) : 0
227
+ };
228
+ };
229
+
230
+ //#endregion
231
+ //#region src/ZuploMonetizationWrapper.tsx
232
+ const DEFAULT_GATEWAY_URL = "https://api.zuploedge.com";
233
+ const getBaseUrl = (context) => {
234
+ if (!context) return DEFAULT_GATEWAY_URL;
235
+ return context.env.ZUPLO_GATEWAY_SERVICE_URL || DEFAULT_GATEWAY_URL;
236
+ };
237
+ const hasVariables = (value) => typeof value === "object" && value != null;
238
+ const queryClient = new QueryClient({ defaultOptions: {
239
+ queries: {
240
+ retry: false,
241
+ queryFn: async (q) => {
242
+ if (!Array.isArray(q.queryKey)) throw new Error("Query key must be an array");
243
+ if (q.queryKey.length === 0) throw new Error("Query key must be a non-empty array");
244
+ const url = q.queryKey[0];
245
+ if (!url || typeof url !== "string") throw new Error("URL is required");
246
+ const request = new Request(joinUrl(getBaseUrl(q.meta?.context), url), {
247
+ ...q.meta?.request,
248
+ headers: {
249
+ "Content-Type": "application/json",
250
+ ...q.meta?.request?.headers
251
+ }
252
+ });
253
+ const response = await fetch(q.meta?.context ? await q.meta.context.signRequest(request) : request);
254
+ await throwIfProblemJson(response);
255
+ if (!response.ok) throw new Error("Failed to fetch request");
256
+ return response.json();
257
+ }
258
+ },
259
+ mutations: {
260
+ retry: false,
261
+ mutationFn: async (variables, m) => {
262
+ if (!m.mutationKey || !Array.isArray(m.mutationKey)) throw new Error("Mutation key must be an array");
263
+ if (m.mutationKey.length === 0) throw new Error("Mutation key must be a non-empty array");
264
+ let url = m.mutationKey[0];
265
+ if (!url || typeof url !== "string") throw new Error("URL is required as first element of mutationKey");
266
+ if (typeof url === "string" && hasVariables(variables)) url = url.replace(/{(\w+)}/g, (_, key) => {
267
+ const value = variables[key];
268
+ if (value === void 0) throw new Error(`Missing variable "${key}" for URL template`);
269
+ return encodeURIComponent(String(value));
270
+ });
271
+ const init = typeof m.meta?.request === "function" ? m.meta.request(variables) : m.meta?.request ?? {};
272
+ const method = init.method || "POST";
273
+ const body = init.body ?? (method !== "GET" && method !== "HEAD" ? JSON.stringify(variables) : void 0);
274
+ const request = new Request(joinUrl(getBaseUrl(m.meta?.context), url), {
275
+ ...init,
276
+ method,
277
+ body,
278
+ headers: {
279
+ "Content-Type": "application/json",
280
+ ...init.headers
281
+ }
282
+ });
283
+ const response = await fetch(m.meta?.context ? await m.meta.context.signRequest(request) : request);
284
+ await throwIfProblemJson(response);
285
+ if (!response.ok) {
286
+ const errorText = await response.text();
287
+ throw new Error(`Request failed: ${response.status} ${errorText}`);
288
+ }
289
+ if (response.headers.get("content-type")?.includes("application/json")) return response.json();
290
+ return response.text();
291
+ }
292
+ }
293
+ } });
294
+ const ZuploMonetizationWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(QueryClientProvider, {
295
+ client: queryClient,
296
+ children: /* @__PURE__ */ jsx(MonetizationContext, {
297
+ value: options,
298
+ children: /* @__PURE__ */ jsx(ClientOnly, { children: /* @__PURE__ */ jsx(Outlet, {}) })
299
+ })
300
+ });
301
+
302
+ //#endregion
303
+ //#region src/pages/CheckoutConfirmPage.tsx
304
+ const CheckoutConfirmPage = () => {
305
+ const [search] = useSearchParams();
306
+ const planId = search.get("planId");
307
+ const zudoku = useZudoku();
308
+ const deploymentName = useDeploymentName();
309
+ const navigate = useNavigate();
310
+ const { data: plans } = usePlans();
311
+ const { pricing } = useMonetizationConfig();
312
+ const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
313
+ if (!planId) throw new Error("Parameter `planId` missing");
314
+ const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
315
+ const { quotas, features } = categorizeRateCards(rateCards ?? [], {
316
+ currency: selectedPlan?.currency,
317
+ units: pricing?.units,
318
+ planBillingCadence: selectedPlan?.billingCadence
319
+ });
320
+ const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
321
+ const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
322
+ const createSubscriptionMutation = useMutation({
323
+ mutationKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions`],
324
+ meta: {
325
+ context: zudoku,
326
+ request: {
327
+ method: "POST",
328
+ body: JSON.stringify({ planId })
329
+ }
330
+ },
331
+ onSuccess: async (subscription) => {
332
+ await queryClient.invalidateQueries();
333
+ navigate(`/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`);
334
+ }
335
+ });
336
+ return /* @__PURE__ */ jsx("div", {
337
+ className: "w-full bg-muted min-h-screen flex items-center justify-center px-4 py-12 gap-4",
338
+ children: /* @__PURE__ */ jsxs("div", {
339
+ className: "max-w-2xl w-full",
340
+ children: [
341
+ createSubscriptionMutation.isError && /* @__PURE__ */ jsxs(Alert, {
342
+ className: "mb-4",
343
+ variant: "destructive",
344
+ children: [/* @__PURE__ */ jsx(AlertTitle, { children: "Error" }), /* @__PURE__ */ jsx(AlertDescription, { children: createSubscriptionMutation.error.message })]
345
+ }),
346
+ /* @__PURE__ */ jsxs(Card, {
347
+ className: "p-8 w-full max-w-7xl",
348
+ children: [
349
+ /* @__PURE__ */ jsx("div", {
350
+ className: "flex justify-center mb-6",
351
+ children: /* @__PURE__ */ jsx("div", {
352
+ className: "rounded-full bg-primary/10 p-3",
353
+ children: /* @__PURE__ */ jsx(CheckIcon, { className: "size-9 text-primary" })
354
+ })
355
+ }),
356
+ /* @__PURE__ */ jsxs("div", {
357
+ className: "text-center mb-8",
358
+ children: [/* @__PURE__ */ jsx("h1", {
359
+ className: "text-2xl font-bold text-card-foreground mb-3",
360
+ children: "Review your subscription"
361
+ }), /* @__PURE__ */ jsx("p", {
362
+ className: "text-muted-foreground text-base",
363
+ children: "Please confirm the details below before completing your purchase."
364
+ })]
365
+ }),
366
+ selectedPlan && /* @__PURE__ */ jsxs(Card, {
367
+ className: "bg-muted/50",
368
+ children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, {
369
+ className: "flex justify-between items-start",
370
+ children: [
371
+ /* @__PURE__ */ jsxs("div", {
372
+ className: "flex items-center gap-3",
373
+ children: [/* @__PURE__ */ jsx("div", {
374
+ className: "flex flex-col text-2xl font-bold bg-primary text-primary-foreground items-center justify-center rounded size-12",
375
+ children: selectedPlan.name.at(0)?.toUpperCase()
376
+ }), /* @__PURE__ */ jsxs("div", {
377
+ className: "flex flex-col",
378
+ children: [/* @__PURE__ */ jsx("span", {
379
+ className: "text-lg font-bold",
380
+ children: selectedPlan.name
381
+ }), /* @__PURE__ */ jsx("span", {
382
+ className: "text-sm font-normal text-muted-foreground",
383
+ children: selectedPlan.description || "Selected plan"
384
+ })]
385
+ })]
386
+ }),
387
+ price && price.monthly > 0 && /* @__PURE__ */ jsxs("div", {
388
+ className: "text-right",
389
+ children: [/* @__PURE__ */ jsx("div", {
390
+ className: "text-2xl font-bold",
391
+ children: formatPrice(price.monthly, selectedPlan?.currency)
392
+ }), billingCycle && /* @__PURE__ */ jsxs("div", {
393
+ className: "text-sm text-muted-foreground font-normal",
394
+ children: ["Billed ", formatBillingCycle(billingCycle)]
395
+ })]
396
+ }),
397
+ price && price.monthly === 0 && /* @__PURE__ */ jsx("div", {
398
+ className: "text-2xl text-muted-foreground font-bold",
399
+ children: "Free"
400
+ })
401
+ ]
402
+ }) }), /* @__PURE__ */ jsxs(CardContent, { children: [
403
+ /* @__PURE__ */ jsx(Separator, {}),
404
+ /* @__PURE__ */ jsx("div", {
405
+ className: "text-sm font-medium mb-3 mt-3",
406
+ children: "What's included:"
407
+ }),
408
+ /* @__PURE__ */ jsxs("div", {
409
+ className: "grid grid-cols-2 gap-2 text-muted-foreground",
410
+ children: [quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, {
411
+ quota,
412
+ className: "text-muted-foreground"
413
+ }, quota.key)), features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, {
414
+ feature,
415
+ className: "text-muted-foreground"
416
+ }, feature.key))]
417
+ })
418
+ ] })]
419
+ }),
420
+ /* @__PURE__ */ jsxs("div", {
421
+ className: "space-y-3 mt-4",
422
+ children: [/* @__PURE__ */ jsx(Button, {
423
+ className: "w-full",
424
+ onClick: () => createSubscriptionMutation.mutate(),
425
+ disabled: createSubscriptionMutation.isPending,
426
+ children: createSubscriptionMutation.isPending ? "Processing Payment..." : "Confirm & Subscribe"
427
+ }), /* @__PURE__ */ jsx(Button, {
428
+ variant: "ghost",
429
+ className: "w-full",
430
+ disabled: createSubscriptionMutation.isPending,
431
+ asChild: !createSubscriptionMutation.isPending,
432
+ children: /* @__PURE__ */ jsx(Link$1, {
433
+ to: "/pricing",
434
+ children: "Cancel"
435
+ })
436
+ })]
437
+ }),
438
+ /* @__PURE__ */ jsx("div", {
439
+ className: "mt-6 pt-6 border-t text-center",
440
+ children: /* @__PURE__ */ jsx("p", {
441
+ className: "text-xs text-muted-foreground",
442
+ children: "By confirming, you agree to our Terms of Service and Privacy Policy. You can cancel anytime."
443
+ })
444
+ })
445
+ ]
446
+ }),
447
+ /* @__PURE__ */ jsxs("div", {
448
+ className: "flex items-center gap-2 text-muted-foreground text-xs item-center justify-center pt-4",
449
+ children: [/* @__PURE__ */ jsx(LockIcon, { className: "size-3" }), "Your payment is secured by Stripe"]
450
+ })
451
+ ]
452
+ })
453
+ });
454
+ };
455
+
456
+ //#endregion
457
+ //#region src/components/RedirectPage.tsx
458
+ const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
459
+ useEffect(() => {
460
+ if (url) window.location.href = url;
461
+ }, [url]);
462
+ return /* @__PURE__ */ jsx("div", {
463
+ className: "flex min-h-screen items-center justify-center bg-muted",
464
+ children: /* @__PURE__ */ jsxs("div", {
465
+ className: "flex max-w-md flex-col items-center space-y-6 text-center",
466
+ children: [
467
+ /* @__PURE__ */ jsx("div", {
468
+ className: "relative",
469
+ children: /* @__PURE__ */ jsx("div", {
470
+ className: "flex h-24 w-24 items-center justify-center rounded-full bg-foreground/10",
471
+ children: /* @__PURE__ */ jsx(Icon, { className: "w-12 h-12 text-foreground" })
472
+ })
473
+ }),
474
+ children,
475
+ /* @__PURE__ */ jsxs("div", {
476
+ className: "space-y-2",
477
+ children: [
478
+ /* @__PURE__ */ jsx("h2", {
479
+ className: "text-2xl font-bold text-card-foreground",
480
+ children: title
481
+ }),
482
+ /* @__PURE__ */ jsx("p", {
483
+ className: "text-muted-foreground",
484
+ children: description
485
+ }),
486
+ /* @__PURE__ */ jsx("p", {
487
+ className: "text-sm text-muted-foreground",
488
+ children: "Powered by Stripe for maximum security"
489
+ })
490
+ ]
491
+ }),
492
+ /* @__PURE__ */ jsxs("div", {
493
+ className: "flex space-x-2",
494
+ children: [
495
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.3s]" }),
496
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.15s]" }),
497
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary" })
498
+ ]
499
+ })
500
+ ]
501
+ })
502
+ });
503
+ };
504
+
505
+ //#endregion
506
+ //#region src/hooks/useUrlUtils.ts
507
+ const useUrlUtils = () => {
508
+ const basePath = useZudoku().options.basePath;
509
+ return { generateUrl: (path, { searchParams } = {}) => {
510
+ if (!window.location.origin) throw new Error("Only works in browser environment");
511
+ return joinUrl(window.location.origin, basePath, path, searchParams ? `?${new URLSearchParams(searchParams)}` : void 0);
512
+ } };
513
+ };
514
+
515
+ //#endregion
516
+ //#region src/pages/CheckoutPage.tsx
517
+ const CheckoutPage = () => {
518
+ const [searchParams] = useSearchParams();
519
+ const planId = searchParams.get("planId");
520
+ const zudoku = useZudoku();
521
+ const auth = useAuth();
522
+ const { generateUrl } = useUrlUtils();
523
+ const deploymentName = useDeploymentName();
524
+ if (!planId) throw new Error(`missing planId in URL`);
525
+ const checkoutLink = useQuery({
526
+ queryKey: [
527
+ `/v3/zudoku-metering/${deploymentName}/stripe/checkout`,
528
+ planId,
529
+ auth.profile?.sub
530
+ ],
531
+ meta: {
532
+ context: zudoku,
533
+ request: {
534
+ method: "POST",
535
+ body: JSON.stringify({
536
+ planId,
537
+ successURL: generateUrl("/checkout-confirm", { searchParams: { planId } }),
538
+ cancelURL: generateUrl("/pricing")
539
+ })
540
+ }
541
+ }
542
+ });
543
+ return /* @__PURE__ */ jsx(RedirectPage, {
544
+ icon: ShieldIcon,
545
+ title: "Establishing encrypted connection...",
546
+ description: "Setting up your secure checkout experience",
547
+ url: checkoutLink.data?.url,
548
+ children: checkoutLink.isError && /* @__PURE__ */ jsxs(Alert, {
549
+ variant: "destructive",
550
+ children: [/* @__PURE__ */ jsx(AlertDescription, {
551
+ className: "first-letter:uppercase",
552
+ children: checkoutLink.error.message
553
+ }), /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(Button$1, {
554
+ variant: "outline",
555
+ size: "xs",
556
+ asChild: true,
557
+ children: /* @__PURE__ */ jsx(Link$1, {
558
+ to: "/subscriptions",
559
+ children: "Back"
560
+ })
561
+ }) })]
562
+ })
563
+ });
564
+ };
565
+
566
+ //#endregion
567
+ //#region src/pages/ManagePaymentPage.tsx
568
+ const ManagePaymentPage = () => {
569
+ const zudoku = useZudoku();
570
+ const { generateUrl } = useUrlUtils();
571
+ const deploymentName = useDeploymentName();
572
+ const auth = useAuth();
573
+ const billingPortal = useQuery({
574
+ queryKey: [`/v3/zudoku-metering/${deploymentName}/stripe/portal`],
575
+ meta: {
576
+ context: zudoku,
577
+ request: {
578
+ method: "POST",
579
+ body: JSON.stringify({
580
+ email: auth.profile?.email,
581
+ returnURL: generateUrl("/subscriptions")
582
+ })
583
+ }
584
+ }
585
+ });
586
+ return /* @__PURE__ */ jsx(RedirectPage, {
587
+ icon: CreditCardIcon,
588
+ title: "Redirecting to payment portal...",
589
+ description: "Setting up your secure connection",
590
+ url: billingPortal.data?.url,
591
+ children: billingPortal.isError && /* @__PURE__ */ jsxs(Alert, {
592
+ variant: "destructive",
593
+ children: [/* @__PURE__ */ jsx(AlertDescription, {
594
+ className: "first-letter:uppercase",
595
+ children: billingPortal.error.message
596
+ }), /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(Button$1, {
597
+ variant: "outline",
598
+ size: "xs",
599
+ asChild: true,
600
+ children: /* @__PURE__ */ jsx(Link, {
601
+ to: "/subscriptions",
602
+ children: "Back"
603
+ })
604
+ }) })]
605
+ })
606
+ });
607
+ };
608
+
609
+ //#endregion
610
+ //#region src/pages/pricing/PricingCard.tsx
611
+ const PhaseSection = ({ phase, currency, showName, billingCadence }) => {
612
+ const { pricing } = useMonetizationConfig();
613
+ const { quotas, features } = categorizeRateCards(phase.rateCards, {
614
+ currency,
615
+ units: pricing?.units,
616
+ planBillingCadence: billingCadence
617
+ });
618
+ if (quotas.length === 0 && features.length === 0) return null;
619
+ return /* @__PURE__ */ jsxs("div", {
620
+ className: "space-y-2",
621
+ children: [
622
+ showName && /* @__PURE__ */ jsxs("div", {
623
+ className: "text-sm font-medium text-card-foreground",
624
+ children: [phase.name, phase.duration && /* @__PURE__ */ jsxs("span", {
625
+ className: "text-muted-foreground font-normal",
626
+ children: [
627
+ " ",
628
+ "— ",
629
+ formatDuration(phase.duration)
630
+ ]
631
+ })]
632
+ }),
633
+ quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, { quota }, quota.key)),
634
+ features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, { feature }, feature.key))
635
+ ]
636
+ });
637
+ };
638
+ const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
639
+ const { pricing } = useMonetizationConfig();
640
+ if (plan.phases.length === 0) return null;
641
+ const price = getPriceFromPlan(plan);
642
+ const isFree = price.monthly === 0;
643
+ const isCustom = plan.metadata?.isCustom === true;
644
+ const hasMultiplePhases = plan.phases.length > 1;
645
+ const billingInterval = formatDuration(plan.billingCadence);
646
+ return /* @__PURE__ */ jsxs("div", {
647
+ className: cn("relative rounded-lg border p-6 flex flex-col", isPopular && "border-primary border-2"),
648
+ children: [
649
+ isPopular && /* @__PURE__ */ jsx("div", {
650
+ className: "absolute top-0 -translate-y-1/2 left-1/2 -translate-x-1/2 whitespace-nowrap",
651
+ children: /* @__PURE__ */ jsx("span", {
652
+ className: "bg-primary text-primary-foreground text-xs font-semibold px-3 py-1 rounded-full uppercase",
653
+ children: "Most Popular"
654
+ })
655
+ }),
656
+ /* @__PURE__ */ jsxs("div", {
657
+ className: "mb-4 pb-4 border-b",
658
+ children: [
659
+ /* @__PURE__ */ jsx("h3", {
660
+ className: "text-base font-semibold text-muted-foreground mb-2",
661
+ children: plan.name
662
+ }),
663
+ /* @__PURE__ */ jsx("div", {
664
+ className: "flex items-baseline gap-1 flex-wrap",
665
+ children: isCustom ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
666
+ className: "text-3xl font-bold text-card-foreground",
667
+ children: "Custom"
668
+ }), /* @__PURE__ */ jsx("div", {
669
+ className: "text-sm text-muted-foreground mt-1",
670
+ children: "Contact Sales"
671
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
672
+ className: "text-3xl font-bold text-card-foreground",
673
+ children: isFree ? "Free" : formatPrice(price.monthly, plan.currency)
674
+ }), !isFree && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
675
+ className: "text-muted-foreground text-sm",
676
+ children: ["/", billingInterval]
677
+ }), pricing?.showYearlyPrice !== false && price.yearly > 0 && /* @__PURE__ */ jsxs("div", {
678
+ className: "w-full text-sm text-muted-foreground mt-1",
679
+ children: [formatPrice(price.yearly, plan.currency), "/year"]
680
+ })] })] })
681
+ }),
682
+ plan.paymentRequired === false && /* @__PURE__ */ jsx("div", {
683
+ className: "text-sm text-muted-foreground mt-1",
684
+ children: "No CC required"
685
+ })
686
+ ]
687
+ }),
688
+ /* @__PURE__ */ jsx("div", {
689
+ className: "space-y-4 mb-6 grow",
690
+ children: plan.phases.map((phase) => /* @__PURE__ */ jsx(PhaseSection, {
691
+ phase,
692
+ currency: plan.currency,
693
+ showName: hasMultiplePhases,
694
+ billingCadence: plan.billingCadence
695
+ }, phase.key))
696
+ }),
697
+ isSubscribed ? /* @__PURE__ */ jsx(Button, {
698
+ variant: isPopular ? "default" : "secondary",
699
+ asChild: true,
700
+ children: /* @__PURE__ */ jsx(Link$1, {
701
+ to: `/subscriptions#manage`,
702
+ children: "Manage Subscriptions"
703
+ })
704
+ }) : /* @__PURE__ */ jsx(Button, {
705
+ variant: isPopular ? "default" : "secondary",
706
+ asChild: true,
707
+ children: /* @__PURE__ */ jsx(Link$1, {
708
+ to: `/checkout?planId=${encodeURIComponent(plan.id)}`,
709
+ children: "Subscribe"
710
+ })
711
+ })
712
+ ]
713
+ });
714
+ };
715
+
716
+ //#endregion
717
+ //#region src/pages/PricingPage.tsx
718
+ const PricingPage = () => {
719
+ const { pricing } = useMonetizationConfig();
720
+ const zudoku = useZudoku();
721
+ const deploymentName = useDeploymentName();
722
+ const auth = useAuth();
723
+ const { data: pricingTable } = usePlans();
724
+ const { data: subscriptions = { items: [] } } = useQuery({
725
+ meta: { context: zudoku },
726
+ queryKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions`],
727
+ enabled: auth.isAuthenticated
728
+ });
729
+ return /* @__PURE__ */ jsxs("div", {
730
+ className: "w-full px-4 pt-(--padding-content-top) pb-(--padding-content-bottom)",
731
+ children: [
732
+ /* @__PURE__ */ jsxs(Head, { children: [/* @__PURE__ */ jsx("title", { children: pricing?.title ?? "Pricing" }), /* @__PURE__ */ jsx("meta", {
733
+ name: "description",
734
+ content: pricing?.subtitle ?? "See our pricing options and choose the one that best suits your needs."
735
+ })] }),
736
+ /* @__PURE__ */ jsxs("div", {
737
+ className: "text-center space-y-4 mb-12",
738
+ children: [/* @__PURE__ */ jsx(Heading, {
739
+ level: 1,
740
+ "data-testid": "title",
741
+ children: pricing?.title ?? "Pricing"
742
+ }), /* @__PURE__ */ jsx("p", {
743
+ className: "text-muted-foreground",
744
+ "data-testid": "subtitle",
745
+ children: pricing?.subtitle ?? "See our pricing options and choose the one that best suits your needs."
746
+ })]
747
+ }),
748
+ /* @__PURE__ */ jsx("div", {
749
+ className: "w-full grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(300px,max-content))] justify-center gap-6",
750
+ children: pricingTable.items.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
751
+ plan,
752
+ isPopular: plan.metadata?.zuplo_most_popular === "true",
753
+ isSubscribed: subscriptions.items.some((subscription) => ["active", "canceled"].includes(subscription.status))
754
+ }, plan.id))
755
+ }),
756
+ /* @__PURE__ */ jsx(Slot.Target, { name: "pricing-page-after" })
757
+ ]
758
+ });
759
+ };
760
+
761
+ //#endregion
762
+ //#region src/pages/SubscriptionChangeConfirmPage.tsx
763
+ const SubscriptionChangeConfirmPage = () => {
764
+ const [search] = useSearchParams();
765
+ const planId = search.get("planId");
766
+ const subscriptionId = search.get("subscriptionId");
767
+ const zudoku = useZudoku();
768
+ const deploymentName = useDeploymentName();
769
+ const navigate = useNavigate();
770
+ const { data: plans } = usePlans();
771
+ const { pricing } = useMonetizationConfig();
772
+ const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
773
+ if (!planId) throw new Error("Parameter `planId` missing");
774
+ if (!subscriptionId) throw new Error("Parameter `subscriptionId` missing");
775
+ const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
776
+ const { quotas, features } = categorizeRateCards(rateCards ?? [], {
777
+ currency: selectedPlan?.currency,
778
+ units: pricing?.units,
779
+ planBillingCadence: selectedPlan?.billingCadence
780
+ });
781
+ const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
782
+ const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
783
+ const changeMutation = useMutation({
784
+ mutationKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions/${subscriptionId}/change`],
785
+ meta: {
786
+ context: zudoku,
787
+ request: {
788
+ method: "POST",
789
+ body: JSON.stringify({ planId })
790
+ }
791
+ },
792
+ onSuccess: async (subscription) => {
793
+ await queryClient.invalidateQueries();
794
+ navigate(`/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`, { state: { planSwitched: { newPlanName: selectedPlan?.name } } });
795
+ }
796
+ });
797
+ return /* @__PURE__ */ jsx("div", {
798
+ className: "w-full bg-muted min-h-screen flex items-center justify-center px-4 py-12 gap-4",
799
+ children: /* @__PURE__ */ jsxs("div", {
800
+ className: "max-w-2xl w-full",
801
+ children: [changeMutation.isError && /* @__PURE__ */ jsxs(Alert, {
802
+ className: "mb-4",
803
+ variant: "destructive",
804
+ children: [/* @__PURE__ */ jsx(AlertTitle, { children: "Error" }), /* @__PURE__ */ jsx(AlertDescription, { children: changeMutation.error.message })]
805
+ }), /* @__PURE__ */ jsxs(Card, {
806
+ className: "p-8 w-full max-w-7xl",
807
+ children: [
808
+ /* @__PURE__ */ jsx("div", {
809
+ className: "flex justify-center mb-6",
810
+ children: /* @__PURE__ */ jsx("div", {
811
+ className: "rounded-full bg-primary/10 p-3",
812
+ children: /* @__PURE__ */ jsx(CheckIcon, { className: "size-9 text-primary" })
813
+ })
814
+ }),
815
+ /* @__PURE__ */ jsxs("div", {
816
+ className: "text-center mb-8",
817
+ children: [/* @__PURE__ */ jsx("h1", {
818
+ className: "text-2xl font-bold text-card-foreground mb-3",
819
+ children: "Confirm plan change"
820
+ }), /* @__PURE__ */ jsx("p", {
821
+ className: "text-muted-foreground text-base",
822
+ children: "Please confirm the details below to change your subscription."
823
+ })]
824
+ }),
825
+ selectedPlan && /* @__PURE__ */ jsxs(Card, {
826
+ className: "bg-muted/50",
827
+ children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, {
828
+ className: "flex justify-between items-start",
829
+ children: [
830
+ /* @__PURE__ */ jsxs("div", {
831
+ className: "flex items-center gap-3",
832
+ children: [/* @__PURE__ */ jsx("div", {
833
+ className: "flex flex-col text-2xl font-bold bg-primary text-primary-foreground items-center justify-center rounded size-12",
834
+ children: selectedPlan.name.at(0)?.toUpperCase()
835
+ }), /* @__PURE__ */ jsxs("div", {
836
+ className: "flex flex-col",
837
+ children: [/* @__PURE__ */ jsx("span", {
838
+ className: "text-lg font-bold",
839
+ children: selectedPlan.name
840
+ }), /* @__PURE__ */ jsx("span", {
841
+ className: "text-sm font-normal text-muted-foreground",
842
+ children: selectedPlan.description || "New plan"
843
+ })]
844
+ })]
845
+ }),
846
+ price && price.monthly > 0 && /* @__PURE__ */ jsxs("div", {
847
+ className: "text-right",
848
+ children: [/* @__PURE__ */ jsx("div", {
849
+ className: "text-2xl font-bold",
850
+ children: formatPrice(price.monthly, selectedPlan?.currency)
851
+ }), billingCycle && /* @__PURE__ */ jsxs("div", {
852
+ className: "text-sm text-muted-foreground font-normal",
853
+ children: ["Billed ", formatBillingCycle(billingCycle)]
854
+ })]
855
+ }),
856
+ price && price.monthly === 0 && /* @__PURE__ */ jsx("div", {
857
+ className: "text-2xl text-muted-foreground font-bold",
858
+ children: "Free"
859
+ })
860
+ ]
861
+ }) }), /* @__PURE__ */ jsxs(CardContent, { children: [
862
+ /* @__PURE__ */ jsx(Separator, {}),
863
+ /* @__PURE__ */ jsx("div", {
864
+ className: "text-sm font-medium mb-3 mt-3",
865
+ children: "What's included:"
866
+ }),
867
+ /* @__PURE__ */ jsxs("div", {
868
+ className: "grid grid-cols-2 gap-2 text-muted-foreground",
869
+ children: [quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, {
870
+ quota,
871
+ className: "text-muted-foreground"
872
+ }, quota.key)), features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, {
873
+ feature,
874
+ className: "text-muted-foreground"
875
+ }, feature.key))]
876
+ })
877
+ ] })]
878
+ }),
879
+ /* @__PURE__ */ jsxs("div", {
880
+ className: "space-y-3 mt-4",
881
+ children: [/* @__PURE__ */ jsx(Button, {
882
+ className: "w-full",
883
+ onClick: () => changeMutation.mutate(),
884
+ disabled: changeMutation.isPending,
885
+ children: changeMutation.isPending ? "Changing plan..." : "Confirm & Change Plan"
886
+ }), /* @__PURE__ */ jsx(Button, {
887
+ variant: "ghost",
888
+ className: "w-full",
889
+ disabled: changeMutation.isPending,
890
+ asChild: !changeMutation.isPending,
891
+ children: /* @__PURE__ */ jsx(Link$1, {
892
+ to: `/subscriptions?${new URLSearchParams({ subscriptionId: subscriptionId ?? "" })}`,
893
+ children: "Cancel"
894
+ })
895
+ })]
896
+ }),
897
+ /* @__PURE__ */ jsx("div", {
898
+ className: "mt-6 pt-6 border-t text-center",
899
+ children: /* @__PURE__ */ jsx("p", {
900
+ className: "text-xs text-muted-foreground",
901
+ children: "By confirming, you agree to our Terms of Service and Privacy Policy."
902
+ })
903
+ })
904
+ ]
905
+ })]
906
+ })
907
+ });
908
+ };
909
+
910
+ //#endregion
911
+ //#region src/hooks/useSubscriptions.ts
912
+ const useSubscriptions = (environmentName) => {
913
+ const zudoku = useZudoku();
914
+ return useSuspenseQuery({
915
+ queryKey: [`/v3/zudoku-metering/${environmentName}/subscriptions`],
916
+ meta: { context: zudoku }
917
+ });
918
+ };
919
+
920
+ //#endregion
921
+ //#region src/pages/subscriptions/ConfirmDeleteKeyAlert.tsx
922
+ const ConfirmDeleteKeyAlert = ({ children, onDelete }) => {
923
+ return /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
924
+ asChild: true,
925
+ children
926
+ }), /* @__PURE__ */ jsxs(AlertDialogContent, { children: [/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [/* @__PURE__ */ jsx(AlertDialogTitle, { children: "Are you absolutely sure?" }), /* @__PURE__ */ jsx(AlertDialogDescription, { children: "This action cannot be undone. This will permanently delete your API key." })] }), /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [/* @__PURE__ */ jsx(AlertDialogCancel, { children: "Cancel" }), /* @__PURE__ */ jsx(AlertDialogAction, {
927
+ onClick: onDelete,
928
+ children: "Continue"
929
+ })] })] })] });
930
+ };
931
+
932
+ //#endregion
933
+ //#region src/pages/subscriptions/ApiKey.tsx
934
+ const formatDate$1 = (dateString) => {
935
+ if (!dateString) return "";
936
+ return new Date(dateString).toLocaleDateString("en-US", {
937
+ month: "short",
938
+ day: "numeric",
939
+ year: "numeric"
940
+ });
941
+ };
942
+ const getTimeAgo = (dateString) => {
943
+ if (!dateString) return "Never";
944
+ const date = new Date(dateString);
945
+ const now = /* @__PURE__ */ new Date();
946
+ const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1e3 * 60));
947
+ if (diffInMinutes < 1) return "Just now";
948
+ if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`;
949
+ const diffInHours = Math.floor(diffInMinutes / 60);
950
+ if (diffInHours < 24) return `${diffInHours} hours ago`;
951
+ const diffInDays = Math.floor(diffInHours / 24);
952
+ if (diffInDays === 1) return "1 day ago";
953
+ if (diffInDays < 30) return `${diffInDays} days ago`;
954
+ return formatDate$1(dateString);
955
+ };
956
+ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label, onDelete }) => {
957
+ const isExpiring = expiresOn && new Date(expiresOn) < new Date(Date.now() + 720 * 60 * 60 * 1e3);
958
+ const isExpired = expiresOn && new Date(expiresOn) < /* @__PURE__ */ new Date();
959
+ return /* @__PURE__ */ jsxs(Frame, {
960
+ className: cn(isExpired && "opacity-50 hover:opacity-100 transition-opacity"),
961
+ children: [/* @__PURE__ */ jsx(FramePanel, {
962
+ className: cn(isExpiring && "border-amber-300 dark:border-yellow-400/30"),
963
+ children: /* @__PURE__ */ jsxs("div", {
964
+ className: "space-y-4",
965
+ children: [/* @__PURE__ */ jsxs("div", {
966
+ className: "flex items-center justify-between",
967
+ children: [/* @__PURE__ */ jsxs("div", {
968
+ className: "flex items-center gap-2",
969
+ children: [/* @__PURE__ */ jsx("span", {
970
+ className: "font-semibold text-base",
971
+ children: label || "API Key"
972
+ }), isActive ? /* @__PURE__ */ jsx(Badge, {
973
+ variant: "muted",
974
+ children: "Active"
975
+ }) : /* @__PURE__ */ jsx(Badge, {
976
+ variant: "warning",
977
+ children: "Expiring"
978
+ })]
979
+ }), !isActive && /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
980
+ asChild: true,
981
+ children: /* @__PURE__ */ jsx(Button$1, {
982
+ variant: "ghost",
983
+ size: "icon-sm",
984
+ "aria-label": "More options",
985
+ children: /* @__PURE__ */ jsx(MoreVerticalIcon, { className: "size-4" })
986
+ })
987
+ }), /* @__PURE__ */ jsx(DropdownMenuContent, {
988
+ align: "end",
989
+ children: /* @__PURE__ */ jsx(ConfirmDeleteKeyAlert, {
990
+ onDelete,
991
+ children: /* @__PURE__ */ jsxs(DropdownMenuItem, {
992
+ variant: "destructive",
993
+ onSelect: (e) => e.preventDefault(),
994
+ children: [/* @__PURE__ */ jsx(Trash2Icon, { className: "size-4" }), "Delete key"]
995
+ })
996
+ })
997
+ })] })]
998
+ }), /* @__PURE__ */ jsx(Secret, {
999
+ secret: apiKey,
1000
+ status: isActive ? "active" : "expiring",
1001
+ className: "max-w-fit"
1002
+ })]
1003
+ })
1004
+ }), /* @__PURE__ */ jsx(FrameFooter, {
1005
+ className: "text-xs text-muted-foreground p-2.5",
1006
+ children: /* @__PURE__ */ jsxs("div", {
1007
+ className: "flex items-center flex-wrap gap-x-3 gap-y-1",
1008
+ children: [
1009
+ /* @__PURE__ */ jsxs("div", {
1010
+ className: "flex items-center gap-1.5",
1011
+ children: [/* @__PURE__ */ jsx(ClockIcon, { className: "size-3" }), /* @__PURE__ */ jsxs("span", { children: ["Created ", formatDate$1(createdAt)] })]
1012
+ }),
1013
+ /* @__PURE__ */ jsx("span", {
1014
+ className: "text-muted-foreground/40",
1015
+ children: "•"
1016
+ }),
1017
+ /* @__PURE__ */ jsxs("span", { children: ["Last updated ", getTimeAgo(lastUsed)] }),
1018
+ expiresOn && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1019
+ className: "text-muted-foreground/40",
1020
+ children: "•"
1021
+ }), /* @__PURE__ */ jsxs("span", {
1022
+ className: cn(isExpired && "text-red-600 font-medium dark:text-red-400", isExpiring && !isExpired && "text-amber-500 font-medium dark:text-amber-400"),
1023
+ children: [
1024
+ isExpired ? "Expired" : "Expires",
1025
+ " on ",
1026
+ formatDate$1(expiresOn)
1027
+ ]
1028
+ })] })
1029
+ ]
1030
+ })
1031
+ })]
1032
+ });
1033
+ };
1034
+
1035
+ //#endregion
1036
+ //#region src/pages/subscriptions/ApiKeyInfo.tsx
1037
+ const ApiKeyInfo = () => /* @__PURE__ */ jsxs(DismissibleAlert, {
1038
+ className: "space-y-2",
1039
+ fit: "loose",
1040
+ children: [
1041
+ /* @__PURE__ */ jsx(InfoIcon, {}),
1042
+ /* @__PURE__ */ jsx(AlertTitle, { children: "How API key management works" }),
1043
+ /* @__PURE__ */ jsx(AlertDescription, { children: /* @__PURE__ */ jsxs("ul", {
1044
+ className: "list-disc list-inside space-y-1",
1045
+ children: [
1046
+ /* @__PURE__ */ jsx("li", { children: "Each subscription includes one active API key automatically" }),
1047
+ /* @__PURE__ */ jsx("li", { children: "Rolling your key creates a new current key and sets the old one to expire in 7 days" }),
1048
+ /* @__PURE__ */ jsx("li", { children: "Expiring keys can be deleted early if you no longer need them" })
1049
+ ]
1050
+ }) }),
1051
+ /* @__PURE__ */ jsx(DismissibleAlertAction, {})
1052
+ ]
1053
+ });
1054
+
1055
+ //#endregion
1056
+ //#region src/pages/subscriptions/ConfirmRollKeyAlert.tsx
1057
+ const ConfirmRollKeyAlert = (props) => /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
1058
+ asChild: true,
1059
+ children: props.children
1060
+ }), /* @__PURE__ */ jsxs(AlertDialogContent, { children: [/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [/* @__PURE__ */ jsx(AlertDialogTitle, { children: "Are you absolutely sure?" }), /* @__PURE__ */ jsx(AlertDialogDescription, { children: "This will roll your API key and create a new one. You will need to update your API client to use the new key. The old key will be deleted after 7 days." })] }), /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [/* @__PURE__ */ jsx(AlertDialogCancel, { children: "Cancel" }), /* @__PURE__ */ jsx(AlertDialogAction, {
1061
+ onClick: props.onRollKey,
1062
+ children: "Continue"
1063
+ })] })] })] });
1064
+
1065
+ //#endregion
1066
+ //#region src/pages/subscriptions/ApiKeysList.tsx
1067
+ const PendingFirstPaymentAlert = ({ children }) => /* @__PURE__ */ jsxs("div", {
1068
+ className: "relative rounded-lg overflow-hidden",
1069
+ "data-testid": "pending-first-payment-alert",
1070
+ children: [/* @__PURE__ */ jsx("div", { children }), /* @__PURE__ */ jsx("div", {
1071
+ className: "absolute inset-0 bg-background/80 flex items-center justify-center",
1072
+ children: /* @__PURE__ */ jsxs(Item, {
1073
+ className: "max-w-md bg-muted",
1074
+ children: [/* @__PURE__ */ jsx(ItemMedia, { children: /* @__PURE__ */ jsx(InfoIcon, { className: "size-4" }) }), /* @__PURE__ */ jsx(ItemContent, { children: /* @__PURE__ */ jsx(ItemTitle, { children: "Your keys will be available once the payment has been successfully processed." }) })]
1075
+ })
1076
+ })]
1077
+ });
1078
+ const ApiKeysList = ({ isPendingFirstPayment, apiKeys, deploymentName, consumerId }) => {
1079
+ const context = useZudoku();
1080
+ const rollKeyMutation = useMutation({
1081
+ mutationKey: [`/v3/zudoku-metering/${deploymentName}/consumers/${consumerId}/roll-key`],
1082
+ meta: {
1083
+ request: {
1084
+ method: "POST",
1085
+ body: "{}"
1086
+ },
1087
+ context
1088
+ },
1089
+ onSuccess: async () => {
1090
+ await queryClient.invalidateQueries();
1091
+ }
1092
+ });
1093
+ const deleteKeyMutation = useMutation({
1094
+ mutationKey: [`/v3/zudoku-metering/${deploymentName}/consumers/${consumerId}/keys/{keyId}`],
1095
+ meta: {
1096
+ context,
1097
+ request: { method: "DELETE" }
1098
+ },
1099
+ onSuccess: async () => {
1100
+ await queryClient.invalidateQueries();
1101
+ }
1102
+ });
1103
+ if (!apiKeys || apiKeys.length === 0) return null;
1104
+ const sortedKeys = [...apiKeys].sort((a, b) => {
1105
+ const aExpired = a.expiresOn && new Date(a.expiresOn) < /* @__PURE__ */ new Date();
1106
+ if (aExpired !== (b.expiresOn && new Date(b.expiresOn) < /* @__PURE__ */ new Date())) return aExpired ? 1 : -1;
1107
+ return new Date(b.createdOn).getTime() - new Date(a.createdOn).getTime();
1108
+ });
1109
+ const activeKey = sortedKeys.find((k) => !k.expiresOn);
1110
+ const expiringKeys = sortedKeys.filter((k) => typeof k.expiresOn === "string");
1111
+ return /* @__PURE__ */ jsxs("div", {
1112
+ className: "space-y-4",
1113
+ children: [
1114
+ /* @__PURE__ */ jsxs("div", {
1115
+ className: "flex items-center justify-between",
1116
+ children: [/* @__PURE__ */ jsx(Heading, {
1117
+ level: 3,
1118
+ children: "API Keys"
1119
+ }), /* @__PURE__ */ jsx(ConfirmRollKeyAlert, {
1120
+ onRollKey: () => rollKeyMutation.mutateAsync(),
1121
+ children: /* @__PURE__ */ jsx(ActionButton, {
1122
+ isPending: rollKeyMutation.isPending,
1123
+ variant: "outline",
1124
+ children: /* @__PURE__ */ jsxs("div", {
1125
+ className: "flex items-center gap-2",
1126
+ children: [/* @__PURE__ */ jsx(RefreshCwIcon, { className: `size-4 ${rollKeyMutation.isPending ? "animate-spin" : ""}` }), "Roll API Key"]
1127
+ })
1128
+ })
1129
+ })]
1130
+ }),
1131
+ rollKeyMutation.isError && /* @__PURE__ */ jsxs(Alert, {
1132
+ variant: "destructive",
1133
+ children: [/* @__PURE__ */ jsx(AlertTitle, { children: "Could not roll API key" }), /* @__PURE__ */ jsx(AlertDescription, { children: rollKeyMutation.error.message })]
1134
+ }),
1135
+ /* @__PURE__ */ jsx(ApiKeyInfo, {}),
1136
+ deleteKeyMutation.isSuccess && deleteKeyMutation.variables && /* @__PURE__ */ jsxs(DismissibleAlert, {
1137
+ variant: "info",
1138
+ onDismiss: () => deleteKeyMutation.reset(),
1139
+ children: [
1140
+ /* @__PURE__ */ jsx(CheckCheckIcon, { className: "size-4" }),
1141
+ /* @__PURE__ */ jsx(AlertTitle, { children: "API key was deleted" }),
1142
+ /* @__PURE__ */ jsx(AlertDescription, { children: (() => {
1143
+ const deletedKey = apiKeys.find((k) => k.id === deleteKeyMutation.variables?.keyId);
1144
+ return deletedKey ? `API key created ${formatDate$1(deletedKey.createdOn)} has been removed.` : "The API key has been deleted.";
1145
+ })() }),
1146
+ /* @__PURE__ */ jsx(DismissibleAlertAction, {})
1147
+ ]
1148
+ }),
1149
+ rollKeyMutation.isSuccess && /* @__PURE__ */ jsxs(DismissibleAlert, {
1150
+ variant: "info",
1151
+ onDismiss: () => rollKeyMutation.reset(),
1152
+ children: [
1153
+ /* @__PURE__ */ jsx(CheckCheckIcon, { className: "size-4" }),
1154
+ /* @__PURE__ */ jsx(AlertTitle, { children: "API key was rolled" }),
1155
+ /* @__PURE__ */ jsx(AlertDescription, { children: "A new API key has been created and the old one has been set to expire in 7 days." }),
1156
+ /* @__PURE__ */ jsx(DismissibleAlertAction, {})
1157
+ ]
1158
+ }),
1159
+ deleteKeyMutation.isError && /* @__PURE__ */ jsxs(Alert, {
1160
+ variant: "destructive",
1161
+ children: [
1162
+ /* @__PURE__ */ jsx(CircleSlashIcon, { className: "size-4" }),
1163
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Could not delete API key" }),
1164
+ /* @__PURE__ */ jsx(AlertDescription, { children: deleteKeyMutation.error.message }),
1165
+ /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(Button, {
1166
+ variant: "outline",
1167
+ onClick: () => {
1168
+ const keyId = deleteKeyMutation.variables?.keyId;
1169
+ if (!keyId) return;
1170
+ deleteKeyMutation.mutateAsync({ keyId });
1171
+ },
1172
+ children: "Retry"
1173
+ }) })
1174
+ ]
1175
+ }),
1176
+ /* @__PURE__ */ jsxs("div", {
1177
+ className: "space-y-4",
1178
+ children: [activeKey && (isPendingFirstPayment ? /* @__PURE__ */ jsx(PendingFirstPaymentAlert, { children: /* @__PURE__ */ jsx(ApiKey, {
1179
+ deploymentName,
1180
+ consumerId,
1181
+ apiKeyId: activeKey.id,
1182
+ apiKey: activeKey.key,
1183
+ createdAt: activeKey.createdOn,
1184
+ lastUsed: activeKey.updatedOn,
1185
+ expiresOn: activeKey.expiresOn,
1186
+ isActive: true,
1187
+ label: "Current Key",
1188
+ onDelete: () => deleteKeyMutation.mutateAsync({ keyId: activeKey.id })
1189
+ }, activeKey.id) }) : /* @__PURE__ */ jsx(ApiKey, {
1190
+ deploymentName,
1191
+ consumerId,
1192
+ apiKeyId: activeKey.id,
1193
+ apiKey: activeKey.key,
1194
+ createdAt: activeKey.createdOn,
1195
+ lastUsed: activeKey.updatedOn,
1196
+ expiresOn: activeKey.expiresOn,
1197
+ isActive: true,
1198
+ label: "Current Key",
1199
+ onDelete: () => deleteKeyMutation.mutateAsync({ keyId: activeKey.id })
1200
+ }, activeKey.id)), expiringKeys.map((apiKey) => /* @__PURE__ */ jsx(ApiKey, {
1201
+ deploymentName,
1202
+ consumerId,
1203
+ apiKeyId: apiKey.id,
1204
+ apiKey: apiKey.key,
1205
+ createdAt: apiKey.createdOn,
1206
+ lastUsed: apiKey.updatedOn,
1207
+ expiresOn: apiKey.expiresOn,
1208
+ isActive: false,
1209
+ label: "Previous Key",
1210
+ onDelete: () => deleteKeyMutation.mutateAsync({ keyId: apiKey.id })
1211
+ }, apiKey.id))]
1212
+ })
1213
+ ]
1214
+ });
1215
+ };
1216
+
1217
+ //#endregion
1218
+ //#region src/pages/subscriptions/CancelSubscriptionDialog.tsx
1219
+ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId, billingPeriodEnd }) => {
1220
+ const [confirmationText, setConfirmationText] = useState("");
1221
+ const isConfirmed = planName.startsWith(confirmationText);
1222
+ const deploymentName = useDeploymentName();
1223
+ const context = useZudoku();
1224
+ const queryClient = useQueryClient();
1225
+ const cancelSubscriptionMutation = useMutation({
1226
+ mutationKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions/${subscriptionId}/cancel`],
1227
+ meta: {
1228
+ context,
1229
+ request: {
1230
+ method: "POST",
1231
+ body: JSON.stringify({ timing: "next_billing_cycle" })
1232
+ }
1233
+ },
1234
+ onSuccess: async () => {
1235
+ await queryClient.invalidateQueries();
1236
+ onOpenChange(false);
1237
+ }
1238
+ });
1239
+ return /* @__PURE__ */ jsx(Dialog, {
1240
+ open,
1241
+ onOpenChange,
1242
+ children: /* @__PURE__ */ jsxs(DialogContent, {
1243
+ className: "sm:max-w-md",
1244
+ children: [
1245
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogTitle, { children: "Cancel Subscription" }), /* @__PURE__ */ jsx(DialogDescription, { children: "We're sorry to see you go." })] }),
1246
+ /* @__PURE__ */ jsxs("div", {
1247
+ className: "space-y-4 mt-4",
1248
+ children: [
1249
+ /* @__PURE__ */ jsxs(Alert, {
1250
+ variant: "warning",
1251
+ children: [
1252
+ /* @__PURE__ */ jsx(CalendarIcon, { className: "size-4" }),
1253
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Your plan will be canceled at the end of your billing cycle." }),
1254
+ /* @__PURE__ */ jsxs(AlertDescription, { children: ["You'll retain access until ", formatDate$1(billingPeriodEnd)] })
1255
+ ]
1256
+ }),
1257
+ /* @__PURE__ */ jsxs(Alert, {
1258
+ variant: "destructive",
1259
+ children: [
1260
+ /* @__PURE__ */ jsx(CircleAlert, { className: "size-4" }),
1261
+ /* @__PURE__ */ jsx(AlertTitle, { children: "This action cannot be undone" }),
1262
+ /* @__PURE__ */ jsx(AlertDescription, { children: "Once cancelled, you will not be able to recover this plan or its associated settings. You would need to subscribe again." })
1263
+ ]
1264
+ }),
1265
+ /* @__PURE__ */ jsxs("div", {
1266
+ className: "space-y-2",
1267
+ children: [
1268
+ /* @__PURE__ */ jsxs("label", {
1269
+ className: "text-sm text-foreground",
1270
+ htmlFor: "confirmationText",
1271
+ children: [
1272
+ "Type",
1273
+ " ",
1274
+ /* @__PURE__ */ jsx("span", {
1275
+ className: "font-medium text-destructive",
1276
+ children: planName
1277
+ }),
1278
+ " ",
1279
+ "to confirm"
1280
+ ]
1281
+ }),
1282
+ /* @__PURE__ */ jsx(Input, {
1283
+ id: "confirmationText",
1284
+ value: confirmationText,
1285
+ onChange: (e) => setConfirmationText(e.target.value),
1286
+ placeholder: ""
1287
+ }),
1288
+ /* @__PURE__ */ jsx("p", {
1289
+ className: cn("text-xs text-destructive transition-opacity opacity-0", confirmationText && !isConfirmed && "opacity-100"),
1290
+ children: "Please type the plan name exactly as shown"
1291
+ })
1292
+ ]
1293
+ })
1294
+ ]
1295
+ }),
1296
+ /* @__PURE__ */ jsxs("div", {
1297
+ className: "flex flex-col gap-2",
1298
+ children: [/* @__PURE__ */ jsx(ActionButton, {
1299
+ variant: "destructive",
1300
+ disabled: !isConfirmed || confirmationText !== planName || cancelSubscriptionMutation.isPending,
1301
+ isPending: cancelSubscriptionMutation.isPending || cancelSubscriptionMutation.isSuccess,
1302
+ onClick: () => cancelSubscriptionMutation.mutate(),
1303
+ children: "Cancel subscription"
1304
+ }), /* @__PURE__ */ jsx(Button$1, {
1305
+ variant: "ghost",
1306
+ onClick: () => onOpenChange(false),
1307
+ children: "Keep my subscription"
1308
+ })]
1309
+ })
1310
+ ]
1311
+ })
1312
+ });
1313
+ };
1314
+
1315
+ //#endregion
1316
+ //#region src/pages/subscriptions/SwitchPlanModal.tsx
1317
+ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units) => {
1318
+ const isUpgrade = targetIndex > currentIndex;
1319
+ const currentPhase = currentPlan?.phases.at(-1);
1320
+ const targetPhase = targetPlan.phases.at(-1);
1321
+ const { quotas: currentQuotas, features: currentFeatures } = currentPhase ? categorizeRateCards(currentPhase.rateCards, {
1322
+ currency: currentPlan?.currency,
1323
+ units,
1324
+ planBillingCadence: currentPlan?.billingCadence
1325
+ }) : {
1326
+ quotas: [],
1327
+ features: []
1328
+ };
1329
+ const { quotas: targetQuotas, features: targetFeatures } = targetPhase ? categorizeRateCards(targetPhase.rateCards, {
1330
+ currency: targetPlan.currency,
1331
+ units,
1332
+ planBillingCadence: targetPlan.billingCadence
1333
+ }) : {
1334
+ quotas: [],
1335
+ features: []
1336
+ };
1337
+ const quotaChanges = [];
1338
+ const allQuotaKeys = new Set([...currentQuotas.map((q) => q.key), ...targetQuotas.map((q) => q.key)]);
1339
+ for (const key of allQuotaKeys) {
1340
+ const current = currentQuotas.find((q) => q.key === key);
1341
+ const target = targetQuotas.find((q) => q.key === key);
1342
+ if (current && target) {
1343
+ let change = "same";
1344
+ if (target.limit > current.limit) change = "increase";
1345
+ else if (target.limit < current.limit) change = "decrease";
1346
+ quotaChanges.push({
1347
+ key: key ?? "",
1348
+ name: target.name,
1349
+ currentValue: current.limit,
1350
+ newValue: target.limit,
1351
+ period: target.period,
1352
+ change
1353
+ });
1354
+ } else if (target && !current) quotaChanges.push({
1355
+ key: key ?? "",
1356
+ name: target.name,
1357
+ currentValue: null,
1358
+ newValue: target.limit,
1359
+ period: target.period,
1360
+ change: "added"
1361
+ });
1362
+ else if (current && !target) quotaChanges.push({
1363
+ key: key ?? "",
1364
+ name: current.name,
1365
+ currentValue: current.limit,
1366
+ newValue: null,
1367
+ period: current.period,
1368
+ change: "removed"
1369
+ });
1370
+ }
1371
+ const featureChanges = [];
1372
+ const allFeatureKeys = new Set([...currentFeatures.map((f) => f.key), ...targetFeatures.map((f) => f.key)]);
1373
+ for (const key of allFeatureKeys) {
1374
+ const current = currentFeatures.find((f) => f.key === key);
1375
+ const target = targetFeatures.find((f) => f.key === key);
1376
+ if (current && target) {
1377
+ let change = "same";
1378
+ if (current.value && target.value && current.value !== target.value) change = isUpgrade ? "upgraded" : "downgraded";
1379
+ featureChanges.push({
1380
+ key: key ?? "",
1381
+ name: target.name,
1382
+ currentValue: current.value ?? true,
1383
+ newValue: target.value ?? true,
1384
+ change
1385
+ });
1386
+ } else if (target && !current) featureChanges.push({
1387
+ key: key ?? "",
1388
+ name: target.name,
1389
+ currentValue: null,
1390
+ newValue: target.value ?? true,
1391
+ change: "added"
1392
+ });
1393
+ else if (current && !target) featureChanges.push({
1394
+ key: key ?? "",
1395
+ name: current.name,
1396
+ currentValue: current.value ?? true,
1397
+ newValue: null,
1398
+ change: "removed"
1399
+ });
1400
+ }
1401
+ return {
1402
+ plan: targetPlan,
1403
+ isUpgrade,
1404
+ quotaChanges,
1405
+ featureChanges
1406
+ };
1407
+ };
1408
+ const ChangeIndicator = ({ change }) => {
1409
+ if (change === "increase" || change === "added" || change === "upgraded") return /* @__PURE__ */ jsx(ArrowUpIcon, { className: "w-4 h-4 text-primary shrink-0" });
1410
+ if (change === "decrease" || change === "removed" || change === "downgraded") return /* @__PURE__ */ jsx(ArrowDownIcon, { className: "w-4 h-4 text-amber-600 shrink-0" });
1411
+ return /* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" });
1412
+ };
1413
+ const modeLabelMap = {
1414
+ upgrade: "Upgrade",
1415
+ downgrade: "Downgrade",
1416
+ private: "Switch"
1417
+ };
1418
+ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange }) => {
1419
+ const price = getPriceFromPlan(comparison.plan);
1420
+ const isCustom = comparison.plan.key === "enterprise";
1421
+ const displayPrice = price.monthly;
1422
+ const hasChanges = comparison.quotaChanges.some((q) => q.change !== "same") || comparison.featureChanges.some((f) => f.change !== "same");
1423
+ return /* @__PURE__ */ jsxs("div", {
1424
+ className: "border rounded-lg p-4",
1425
+ children: [/* @__PURE__ */ jsxs("div", {
1426
+ className: "flex items-center justify-between mb-3",
1427
+ children: [/* @__PURE__ */ jsxs("div", {
1428
+ className: "flex items-baseline gap-2",
1429
+ children: [/* @__PURE__ */ jsx("h4", {
1430
+ className: "font-semibold text-foreground",
1431
+ children: comparison.plan.name
1432
+ }), isCustom ? /* @__PURE__ */ jsx("span", {
1433
+ className: "text-primary font-medium",
1434
+ children: "Custom"
1435
+ }) : displayPrice === 0 ? /* @__PURE__ */ jsx("span", {
1436
+ className: "text-primary font-medium",
1437
+ children: "Free"
1438
+ }) : /* @__PURE__ */ jsxs("span", {
1439
+ className: "text-primary font-medium text-lg",
1440
+ children: [
1441
+ formatPrice(displayPrice, comparison.plan.currency),
1442
+ "/",
1443
+ formatDuration(comparison.plan.billingCadence)
1444
+ ]
1445
+ })]
1446
+ }), isCustom ? /* @__PURE__ */ jsx(Button$1, {
1447
+ variant: "default",
1448
+ size: "sm",
1449
+ children: "Contact Sales"
1450
+ }) : /* @__PURE__ */ jsx(Button$1, {
1451
+ variant: mode === "upgrade" ? "default" : "outline",
1452
+ onClick: () => onRequestChange({
1453
+ subscriptionId,
1454
+ plan: comparison.plan,
1455
+ mode
1456
+ }),
1457
+ size: "sm",
1458
+ children: modeLabelMap[mode]
1459
+ })]
1460
+ }), hasChanges && /* @__PURE__ */ jsxs("div", {
1461
+ className: "space-y-1.5",
1462
+ children: [comparison.quotaChanges.filter((q) => q.change !== "same").map((quota) => /* @__PURE__ */ jsxs("div", {
1463
+ className: "flex items-center gap-2 text-sm",
1464
+ children: [
1465
+ /* @__PURE__ */ jsx(ChangeIndicator, { change: quota.change }),
1466
+ /* @__PURE__ */ jsxs("span", {
1467
+ className: "font-medium",
1468
+ children: [quota.name, ":"]
1469
+ }),
1470
+ quota.change === "added" ? /* @__PURE__ */ jsx("span", {
1471
+ className: "text-green-600",
1472
+ children: "Now included"
1473
+ }) : quota.change === "removed" ? /* @__PURE__ */ jsx("span", {
1474
+ className: "text-destructive",
1475
+ children: "No longer included"
1476
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1477
+ /* @__PURE__ */ jsxs("span", {
1478
+ className: "text-muted-foreground",
1479
+ children: [
1480
+ quota.currentValue?.toLocaleString(),
1481
+ "/",
1482
+ quota.period
1483
+ ]
1484
+ }),
1485
+ /* @__PURE__ */ jsx("span", {
1486
+ className: "text-muted-foreground",
1487
+ children: "→"
1488
+ }),
1489
+ /* @__PURE__ */ jsxs("span", {
1490
+ className: cn("font-medium", quota.change === "increase" ? "text-primary" : "text-amber-600"),
1491
+ children: [
1492
+ quota.newValue?.toLocaleString(),
1493
+ "/",
1494
+ quota.period
1495
+ ]
1496
+ })
1497
+ ] })
1498
+ ]
1499
+ }, quota.key)), comparison.featureChanges.filter((f) => f.change !== "same").map((feature) => /* @__PURE__ */ jsx("div", {
1500
+ className: "flex items-center gap-2 text-sm",
1501
+ children: feature.change === "added" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1502
+ /* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" }),
1503
+ /* @__PURE__ */ jsx("span", {
1504
+ className: "text-muted-foreground font-medium",
1505
+ children: feature.name
1506
+ }),
1507
+ /* @__PURE__ */ jsx("span", {
1508
+ className: "text-green-600",
1509
+ children: "—"
1510
+ }),
1511
+ /* @__PURE__ */ jsx("span", {
1512
+ className: "text-green-600",
1513
+ children: "Now included"
1514
+ })
1515
+ ] }) : feature.change === "removed" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1516
+ /* @__PURE__ */ jsx(XIcon, { className: "w-4 h-4 text-destructive shrink-0" }),
1517
+ /* @__PURE__ */ jsx("span", {
1518
+ className: "font-medium",
1519
+ children: feature.name
1520
+ }),
1521
+ /* @__PURE__ */ jsx("span", {
1522
+ className: "text-destructive",
1523
+ children: "—"
1524
+ }),
1525
+ /* @__PURE__ */ jsx("span", {
1526
+ className: "text-destructive",
1527
+ children: "No longer included"
1528
+ })
1529
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1530
+ /* @__PURE__ */ jsx(ChangeIndicator, { change: feature.change }),
1531
+ /* @__PURE__ */ jsxs("span", {
1532
+ className: "",
1533
+ children: [feature.name, ":"]
1534
+ }),
1535
+ /* @__PURE__ */ jsx("span", {
1536
+ className: "text-muted-foreground",
1537
+ children: typeof feature.currentValue === "string" ? feature.currentValue : "Included"
1538
+ }),
1539
+ /* @__PURE__ */ jsx("span", {
1540
+ className: "text-muted-foreground",
1541
+ children: "→"
1542
+ }),
1543
+ /* @__PURE__ */ jsx("span", {
1544
+ className: cn(feature.change === "upgraded" ? "text-green-600" : "text-destructive"),
1545
+ children: typeof feature.newValue === "string" ? feature.newValue : "Included"
1546
+ })
1547
+ ] })
1548
+ }, feature.key))]
1549
+ })]
1550
+ });
1551
+ };
1552
+ const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
1553
+ const deploymentName = useDeploymentName();
1554
+ const context = useZudoku();
1555
+ const { generateUrl } = useUrlUtils();
1556
+ const mutation = useMutation({
1557
+ mutationKey: [`/v3/zudoku-metering/${deploymentName}/stripe/checkout`],
1558
+ meta: {
1559
+ context,
1560
+ request: {
1561
+ method: "POST",
1562
+ body: JSON.stringify({
1563
+ planId: switchTo.plan.id,
1564
+ successURL: generateUrl(`/subscription-change-confirm`, { searchParams: {
1565
+ planId: switchTo.plan.id,
1566
+ subscriptionId: switchTo.subscriptionId
1567
+ } }),
1568
+ cancelURL: generateUrl("/subscriptions", { searchParams: { subscriptionId: switchTo.subscriptionId } })
1569
+ })
1570
+ }
1571
+ },
1572
+ retry: false,
1573
+ onSuccess: (data) => {
1574
+ window.location.href = data.url;
1575
+ }
1576
+ });
1577
+ return /* @__PURE__ */ jsx(AlertDialog, {
1578
+ open: true,
1579
+ onOpenChange: onRequestClose,
1580
+ children: /* @__PURE__ */ jsxs(AlertDialogContent, { children: [/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [
1581
+ /* @__PURE__ */ jsxs(AlertDialogTitle, { children: [
1582
+ "Confirm",
1583
+ " ",
1584
+ switchTo.mode === "private" ? "plan change" : switchTo.mode === "upgrade" ? "upgrade" : "downgrade"
1585
+ ] }),
1586
+ mutation.isError && /* @__PURE__ */ jsx(Alert, {
1587
+ variant: "destructive",
1588
+ children: /* @__PURE__ */ jsx(AlertDescription, {
1589
+ className: "first-letter:uppercase",
1590
+ children: mutation.error.message
1591
+ })
1592
+ }),
1593
+ /* @__PURE__ */ jsx(AlertDialogDescription, { children: switchTo.mode === "private" ? `Are you sure you want to switch to ${switchTo.plan.name}? This will take effect immediately.` : switchTo.mode === "upgrade" ? `Are you sure you want to upgrade to ${switchTo.plan.name}? This will take effect immediately.` : `Are you sure you want to downgrade to ${switchTo.plan.name}? This will take effect at the start of your next billing cycle.` })
1594
+ ] }), /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [/* @__PURE__ */ jsx(AlertDialogCancel, {
1595
+ disabled: mutation.isPending,
1596
+ children: "Cancel"
1597
+ }), /* @__PURE__ */ jsx(ActionButton, {
1598
+ isPending: mutation.isPending,
1599
+ onClick: () => mutation.mutate(),
1600
+ children: modeLabelMap[switchTo.mode]
1601
+ })] })] })
1602
+ });
1603
+ };
1604
+ const SwitchPlanModal = ({ subscription, children }) => {
1605
+ const [open, setOpen] = useState(false);
1606
+ const { data: plansData } = usePlans();
1607
+ const [switchTo, setSwitchTo] = useState(null);
1608
+ const { pricing } = useMonetizationConfig();
1609
+ const currentPlan = plansData?.items.find((p) => p.id === subscription.plan.id);
1610
+ const { upgrades, downgrades, privatePlans } = useMemo(() => {
1611
+ if (!plansData?.items || !currentPlan) return {
1612
+ upgrades: [],
1613
+ downgrades: [],
1614
+ privatePlans: []
1615
+ };
1616
+ const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
1617
+ const currentIndex = plansData.items.findIndex((p) => p.id === currentPlan.id);
1618
+ const allComparisons = plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => {
1619
+ return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan), pricing?.units);
1620
+ });
1621
+ return {
1622
+ upgrades: allComparisons.filter((c) => c.isUpgrade && !isPrivatePlan(c.plan)),
1623
+ downgrades: allComparisons.filter((c) => !c.isUpgrade && !isPrivatePlan(c.plan)),
1624
+ privatePlans: allComparisons.filter((c) => isPrivatePlan(c.plan))
1625
+ };
1626
+ }, [
1627
+ plansData?.items,
1628
+ currentPlan,
1629
+ pricing?.units
1630
+ ]);
1631
+ return /* @__PURE__ */ jsxs(Fragment, { children: [switchTo !== null && /* @__PURE__ */ jsx(ConfirmSwitchAlert, {
1632
+ switchTo,
1633
+ onRequestClose: () => setSwitchTo(null)
1634
+ }), /* @__PURE__ */ jsxs(Dialog, {
1635
+ open,
1636
+ onOpenChange: setOpen,
1637
+ children: [/* @__PURE__ */ jsx(DialogTrigger, {
1638
+ asChild: true,
1639
+ children: children ?? /* @__PURE__ */ jsxs(Button$1, {
1640
+ variant: "outline",
1641
+ size: "sm",
1642
+ children: [/* @__PURE__ */ jsx(ArrowLeftRightIcon, {}), " Switch Plan"]
1643
+ })
1644
+ }), /* @__PURE__ */ jsx(DialogContent, { children: /* @__PURE__ */ jsxs("div", {
1645
+ className: "sm:max-w-2xl max-h-[70vh] overflow-y-auto ",
1646
+ children: [/* @__PURE__ */ jsx(DialogHeader, {
1647
+ className: "text-center",
1648
+ children: /* @__PURE__ */ jsx(DialogTitle, {
1649
+ className: "text-xl font-semibold",
1650
+ children: "Change Your Plan"
1651
+ })
1652
+ }), /* @__PURE__ */ jsxs("div", {
1653
+ className: "mt-4 space-y-6",
1654
+ children: [
1655
+ currentPlan && /* @__PURE__ */ jsx(Item, {
1656
+ variant: "outline",
1657
+ children: /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, { children: "Current Plan" }), /* @__PURE__ */ jsx(ItemDescription, {
1658
+ className: "text-lg font-bold",
1659
+ children: currentPlan.name
1660
+ })] })
1661
+ }),
1662
+ upgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
1663
+ className: "flex items-center justify-between mb-3",
1664
+ children: [/* @__PURE__ */ jsxs("div", {
1665
+ className: "flex items-center gap-2",
1666
+ children: [/* @__PURE__ */ jsx(ArrowUpIcon, { className: "size-5 text-muted-foreground" }), /* @__PURE__ */ jsx("span", {
1667
+ className: "font-medium text-primary",
1668
+ children: "Upgrade Options"
1669
+ })]
1670
+ }), /* @__PURE__ */ jsx("span", {
1671
+ className: "text-sm text-muted-foreground",
1672
+ children: "Takes effect immediately"
1673
+ })]
1674
+ }), /* @__PURE__ */ jsx("div", {
1675
+ className: "space-y-3",
1676
+ children: upgrades.map((comparison) => /* @__PURE__ */ jsx(PlanComparisonItem, {
1677
+ comparison,
1678
+ subscriptionId: subscription.id,
1679
+ mode: "upgrade",
1680
+ onRequestChange: setSwitchTo
1681
+ }, comparison.plan.id))
1682
+ })] }),
1683
+ downgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
1684
+ className: "flex items-center justify-between mb-3",
1685
+ children: [/* @__PURE__ */ jsxs("div", {
1686
+ className: "flex items-center gap-2",
1687
+ children: [/* @__PURE__ */ jsx(ArrowDownIcon, { className: "size-5 text-primary" }), /* @__PURE__ */ jsx("span", {
1688
+ className: "font-medium text-foreground",
1689
+ children: "Downgrade Options"
1690
+ })]
1691
+ }), /* @__PURE__ */ jsx("span", {
1692
+ className: "text-sm text-muted-foreground",
1693
+ children: "Takes effect next billing cycle"
1694
+ })]
1695
+ }), /* @__PURE__ */ jsx("div", {
1696
+ className: "space-y-3",
1697
+ children: downgrades.map((comparison) => /* @__PURE__ */ jsx(PlanComparisonItem, {
1698
+ comparison,
1699
+ subscriptionId: subscription.id,
1700
+ mode: "downgrade",
1701
+ onRequestChange: setSwitchTo
1702
+ }, comparison.plan.id))
1703
+ })] }),
1704
+ privatePlans.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
1705
+ className: "flex items-center justify-between mb-3",
1706
+ children: [/* @__PURE__ */ jsxs("div", {
1707
+ className: "flex items-center gap-2",
1708
+ children: [/* @__PURE__ */ jsx(ArrowLeftRightIcon, { className: "size-5 text-muted-foreground" }), /* @__PURE__ */ jsx("span", {
1709
+ className: "font-medium text-foreground",
1710
+ children: "Private Plan Option"
1711
+ })]
1712
+ }), /* @__PURE__ */ jsx("span", {
1713
+ className: "text-sm text-muted-foreground",
1714
+ children: "Takes effect immediately"
1715
+ })]
1716
+ }), /* @__PURE__ */ jsx("div", {
1717
+ className: "space-y-3",
1718
+ children: privatePlans.map((comparison) => /* @__PURE__ */ jsx(PlanComparisonItem, {
1719
+ comparison,
1720
+ subscriptionId: subscription.id,
1721
+ mode: "private",
1722
+ onRequestChange: setSwitchTo
1723
+ }, comparison.plan.id))
1724
+ })] })
1725
+ ]
1726
+ })]
1727
+ }) })]
1728
+ })] });
1729
+ };
1730
+
1731
+ //#endregion
1732
+ //#region src/pages/subscriptions/ManageSubscription.tsx
1733
+ const ManageSubscription = ({ subscription, planName }) => {
1734
+ const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
1735
+ return /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CancelSubscriptionDialog, {
1736
+ open: cancelDialogOpen,
1737
+ onOpenChange: setCancelDialogOpen,
1738
+ planName,
1739
+ subscriptionId: subscription.id,
1740
+ billingPeriodEnd: subscription.alignment.currentAlignedBillingPeriod.to
1741
+ }), /* @__PURE__ */ jsx(CardContent, {
1742
+ className: "p-6",
1743
+ children: /* @__PURE__ */ jsxs("div", {
1744
+ className: "flex gap-4",
1745
+ children: [/* @__PURE__ */ jsx("div", {
1746
+ className: "flex items-center justify-center w-12 h-12 rounded-full bg-primary/10 shrink-0",
1747
+ children: /* @__PURE__ */ jsx(Settings, { className: "w-6 h-6 text-primary" })
1748
+ }), /* @__PURE__ */ jsxs("div", {
1749
+ className: "flex-1",
1750
+ id: "manage",
1751
+ children: [
1752
+ /* @__PURE__ */ jsx("h2", {
1753
+ className: "text-lg font-semibold text-foreground mb-1",
1754
+ children: "Manage Subscription"
1755
+ }),
1756
+ /* @__PURE__ */ jsx("p", {
1757
+ className: "text-sm text-muted-foreground mb-4",
1758
+ children: "Switch to a different plan or cancel your current subscription."
1759
+ }),
1760
+ /* @__PURE__ */ jsxs("div", {
1761
+ className: "flex flex-wrap gap-3",
1762
+ children: [
1763
+ subscription.status === "canceled" && /* @__PURE__ */ jsx(Button$1, {
1764
+ variant: "outline",
1765
+ size: "sm",
1766
+ asChild: true,
1767
+ children: /* @__PURE__ */ jsxs(Link, {
1768
+ to: "/pricing",
1769
+ children: [/* @__PURE__ */ jsx(RefreshCcw, { className: "w-4 h-4 mr-2" }), "New subscription"]
1770
+ })
1771
+ }),
1772
+ subscription.status === "active" && /* @__PURE__ */ jsx(SwitchPlanModal, { subscription }),
1773
+ /* @__PURE__ */ jsxs(Tooltip, {
1774
+ delayDuration: 0,
1775
+ children: [/* @__PURE__ */ jsx(TooltipTrigger, {
1776
+ asChild: true,
1777
+ children: /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button$1, {
1778
+ variant: "outline",
1779
+ size: "sm",
1780
+ onClick: () => setCancelDialogOpen(true),
1781
+ title: "You can only cancel your subscription if it is not active.",
1782
+ disabled: subscription.status !== "active",
1783
+ children: "Cancel subscription"
1784
+ }) })
1785
+ }), subscription.status === "canceled" && /* @__PURE__ */ jsx(TooltipContent, { children: "Your subscription is already cancelled." })]
1786
+ }),
1787
+ /* @__PURE__ */ jsx(Button$1, {
1788
+ asChild: true,
1789
+ size: "sm",
1790
+ variant: "secondary",
1791
+ children: /* @__PURE__ */ jsx(Link, {
1792
+ to: "/manage-payment",
1793
+ children: /* @__PURE__ */ jsxs("div", {
1794
+ className: "flex items-center gap-2",
1795
+ children: [/* @__PURE__ */ jsx(CreditCardIcon, {}), "Manage payment details"]
1796
+ })
1797
+ })
1798
+ })
1799
+ ]
1800
+ }),
1801
+ /* @__PURE__ */ jsx(Separator, { className: "my-4" }),
1802
+ /* @__PURE__ */ jsx("span", {
1803
+ className: "text-sm text-muted-foreground",
1804
+ children: "Your payment is securely managed by Stripe."
1805
+ })
1806
+ ]
1807
+ })]
1808
+ })
1809
+ })] });
1810
+ };
1811
+
1812
+ //#endregion
1813
+ //#region src/pages/subscriptions/Usage.tsx
1814
+ const isMeteredEntitlement = (entitlement) => {
1815
+ return "balance" in entitlement;
1816
+ };
1817
+ const UsageItem = ({ meter, item, subscription }) => {
1818
+ const cadence = item?.billingCadence ?? subscription?.billingCadence;
1819
+ const billingPeriod = cadence ? formatDurationAdjective(cadence) : "monthly";
1820
+ const isSoftLimit = item?.included?.entitlement?.isSoftLimit ?? true;
1821
+ const rate = (item?.price?.tiers?.find((t) => !t.upToAmount) ?? item?.price?.tiers?.at(-1))?.unitPrice?.amount;
1822
+ const hasOverage = meter.overage > 0;
1823
+ const limit = meter.balance + meter.usage - meter.overage;
1824
+ const isAtLimit = !isSoftLimit && meter.usage >= limit;
1825
+ const dangerZone = hasOverage || isAtLimit;
1826
+ return /* @__PURE__ */ jsxs(Card, {
1827
+ className: cn(dangerZone && "border-destructive bg-destructive/5"),
1828
+ children: [/* @__PURE__ */ jsxs(CardHeader, { children: [
1829
+ hasOverage && isSoftLimit && /* @__PURE__ */ jsxs(Alert, {
1830
+ variant: "destructive",
1831
+ className: "mb-4",
1832
+ children: [
1833
+ /* @__PURE__ */ jsx(AlertTriangleIcon, { className: "size-4 text-red-600 shrink-0" }),
1834
+ /* @__PURE__ */ jsxs(AlertTitle, { children: [
1835
+ "You've exceeded your ",
1836
+ billingPeriod,
1837
+ " quota"
1838
+ ] }),
1839
+ /* @__PURE__ */ jsxs(AlertDescription, { children: [
1840
+ "Additional usage is being charged at the overage rate",
1841
+ rate ? ` ($${Number(rate).toFixed(2)}/call)` : "",
1842
+ ". Upgrade to a higher plan for more usage."
1843
+ ] }),
1844
+ subscription && /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(SwitchPlanModal, {
1845
+ subscription,
1846
+ children: /* @__PURE__ */ jsxs(Button, {
1847
+ variant: "destructive",
1848
+ size: "xs",
1849
+ children: [/* @__PURE__ */ jsx(ArrowUpIcon, {}), "Upgrade"]
1850
+ })
1851
+ }) })
1852
+ ]
1853
+ }),
1854
+ isAtLimit && !isSoftLimit && /* @__PURE__ */ jsxs(Alert, {
1855
+ variant: "destructive",
1856
+ className: "mb-4",
1857
+ children: [
1858
+ /* @__PURE__ */ jsx(AlertTriangleIcon, { className: "size-4 text-red-600 shrink-0" }),
1859
+ /* @__PURE__ */ jsxs(AlertTitle, { children: [
1860
+ "You've reached your ",
1861
+ billingPeriod,
1862
+ " limit"
1863
+ ] }),
1864
+ /* @__PURE__ */ jsx(AlertDescription, { children: "Requests beyond your quota are blocked. Upgrade to a higher plan for more usage." }),
1865
+ subscription && /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(SwitchPlanModal, {
1866
+ subscription,
1867
+ children: /* @__PURE__ */ jsxs(Button, {
1868
+ variant: "destructive",
1869
+ size: "xs",
1870
+ children: [/* @__PURE__ */ jsx(ArrowUpIcon, {}), "Upgrade"]
1871
+ })
1872
+ }) })
1873
+ ]
1874
+ }),
1875
+ /* @__PURE__ */ jsxs(CardTitle, { children: [
1876
+ item?.name ?? "Limit",
1877
+ " ",
1878
+ item?.price?.amount
1879
+ ] })
1880
+ ] }), /* @__PURE__ */ jsxs(CardContent, {
1881
+ className: "space-y-2",
1882
+ children: [
1883
+ /* @__PURE__ */ jsxs("div", {
1884
+ className: "flex items-center justify-between text-sm",
1885
+ children: [/* @__PURE__ */ jsx("div", {
1886
+ className: "flex flex-col gap-2 mb-2",
1887
+ children: /* @__PURE__ */ jsxs("span", {
1888
+ className: cn(dangerZone && "text-red-600 font-medium"),
1889
+ children: [
1890
+ meter.usage.toLocaleString(),
1891
+ " used",
1892
+ hasOverage && isSoftLimit && /* @__PURE__ */ jsxs("span", {
1893
+ className: "ml-1 text-xs",
1894
+ children: [
1895
+ "(+",
1896
+ meter.overage.toLocaleString(),
1897
+ " overage)"
1898
+ ]
1899
+ })
1900
+ ]
1901
+ })
1902
+ }), /* @__PURE__ */ jsxs("span", {
1903
+ className: "text-foreground font-medium",
1904
+ children: [limit.toLocaleString(), " limit"]
1905
+ })]
1906
+ }),
1907
+ /* @__PURE__ */ jsx(Progress, {
1908
+ value: Math.min(100, limit > 0 ? meter.usage / limit * 100 : 100),
1909
+ className: cn("mb-3 h-2", dangerZone && "bg-destructive")
1910
+ }),
1911
+ /* @__PURE__ */ jsxs("p", {
1912
+ className: "text-xs text-muted-foreground",
1913
+ children: [meter.balance.toLocaleString(), " remaining this billing period"]
1914
+ })
1915
+ ]
1916
+ })]
1917
+ });
1918
+ };
1919
+ const Usage = ({ usage, isFetching, currentItems, subscription, isPendingFirstPayment }) => {
1920
+ const hasUsage = Object.values(usage.entitlements).some((value) => isMeteredEntitlement(value));
1921
+ return /* @__PURE__ */ jsxs("div", {
1922
+ className: "space-y-4",
1923
+ children: [
1924
+ /* @__PURE__ */ jsx(Heading, {
1925
+ level: 3,
1926
+ children: "Usage"
1927
+ }),
1928
+ (isPendingFirstPayment || usage.paymentStatus.status === "pending") && /* @__PURE__ */ jsxs(Alert, {
1929
+ fit: "loose",
1930
+ children: [
1931
+ /* @__PURE__ */ jsx(Loader2Icon, { className: "size-5 animate-spin mr-1 ml-1 self-center" }),
1932
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Your payment is being processed" }),
1933
+ /* @__PURE__ */ jsx(AlertDescription, { children: "Your API keys may take a minute to load. Please wait while we set up your subscription." })
1934
+ ]
1935
+ }),
1936
+ (usage.paymentStatus.status === "failed" || usage.paymentStatus.status === "uncollectible") && /* @__PURE__ */ jsxs(Alert, {
1937
+ variant: "destructive",
1938
+ fit: "loose",
1939
+ children: [
1940
+ /* @__PURE__ */ jsx(AlertTriangleIcon, { className: "size-4 shrink-0" }),
1941
+ /* @__PURE__ */ jsx(AlertTitle, { children: usage.paymentStatus.status === "failed" ? "Payment failed" : "Payment unsuccessful" }),
1942
+ /* @__PURE__ */ jsx(AlertDescription, { children: "Your last payment was unsuccessful. Please update your billing information to continue using your subscription." }),
1943
+ /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(Button, {
1944
+ variant: "destructive",
1945
+ size: "xs",
1946
+ asChild: true,
1947
+ children: /* @__PURE__ */ jsx(Link, {
1948
+ to: "/manage-payment",
1949
+ target: "_blank",
1950
+ children: "Manage billing"
1951
+ })
1952
+ }) })
1953
+ ]
1954
+ }),
1955
+ hasUsage ? Object.entries(usage.entitlements).flatMap(([key, value]) => isMeteredEntitlement(value) ? /* @__PURE__ */ jsx(UsageItem, {
1956
+ meter: { ...value },
1957
+ subscription,
1958
+ item: currentItems?.find((item) => item.featureKey === key)
1959
+ }, key) : []) : !isFetching && !subscription?.annotations?.["subscription.previous.id"] ? /* @__PURE__ */ jsxs(Alert, {
1960
+ variant: "warning",
1961
+ children: [
1962
+ /* @__PURE__ */ jsx(Grid2x2XIcon, {}),
1963
+ /* @__PURE__ */ jsx(AlertTitle, { children: "No usage data available" }),
1964
+ /* @__PURE__ */ jsx(AlertDescription, { children: "This subscription does not have any usage data." })
1965
+ ]
1966
+ }) : null
1967
+ ]
1968
+ });
1969
+ };
1970
+
1971
+ //#endregion
1972
+ //#region src/pages/subscriptions/ActiveSubscription.tsx
1973
+ const ActiveSubscription = ({ subscription, deploymentName }) => {
1974
+ const zudoku = useZudoku();
1975
+ const planSwitched = useLocation().state?.planSwitched;
1976
+ const usageQuery = useSuspenseQuery({
1977
+ queryKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions/${subscription.id}/usage`],
1978
+ refetchInterval: (query) => query.state.data?.paymentStatus.status === "pending" ? 1500 : 30 * 1e3,
1979
+ refetchOnWindowFocus: true,
1980
+ meta: { context: zudoku }
1981
+ });
1982
+ const isPendingFirstPayment = usageQuery.data.paymentStatus.isFirstPayment === true && usageQuery.data.paymentStatus.status !== "paid" && usageQuery.data.paymentStatus.status !== "not_required";
1983
+ const activePhase = subscription?.phases.find((p) => new Date(p.activeFrom) <= /* @__PURE__ */ new Date() && (!p.activeTo || new Date(p.activeTo) >= /* @__PURE__ */ new Date()));
1984
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1985
+ planSwitched && /* @__PURE__ */ jsxs(DismissibleAlert, {
1986
+ variant: "info",
1987
+ children: [
1988
+ /* @__PURE__ */ jsx(CheckCheckIcon, { className: "size-4" }),
1989
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Plan changed" }),
1990
+ /* @__PURE__ */ jsx(AlertDescription, { children: planSwitched.newPlanName ? `You have successfully switched to ${planSwitched.newPlanName}.` : "Your plan has been successfully changed." }),
1991
+ /* @__PURE__ */ jsx(DismissibleAlertAction, {})
1992
+ ]
1993
+ }),
1994
+ /* @__PURE__ */ jsx(Usage, {
1995
+ currentItems: activePhase?.items,
1996
+ usage: usageQuery.data,
1997
+ isFetching: usageQuery.isFetching,
1998
+ subscription,
1999
+ isPendingFirstPayment
2000
+ }),
2001
+ subscription?.consumer?.apiKeys && /* @__PURE__ */ jsx(ApiKeysList, {
2002
+ isPendingFirstPayment,
2003
+ deploymentName,
2004
+ consumerId: subscription.consumer.id,
2005
+ apiKeys: subscription.consumer.apiKeys
2006
+ }),
2007
+ activePhase && /* @__PURE__ */ jsx(ManageSubscription, {
2008
+ subscription,
2009
+ planName: subscription.name
2010
+ })
2011
+ ] });
2012
+ };
2013
+
2014
+ //#endregion
2015
+ //#region src/pages/subscriptions/SubscriptionsList.tsx
2016
+ const formatDate = (dateString) => {
2017
+ return new Date(dateString).toLocaleDateString("en-US", {
2018
+ month: "short",
2019
+ day: "numeric",
2020
+ year: "numeric"
2021
+ });
2022
+ };
2023
+ const SubscriptionsList = ({ subscriptions, activeSubscriptionId }) => {
2024
+ const groupedSubscriptions = useMemo(() => {
2025
+ return Object.groupBy(subscriptions.sort((a, b) => {
2026
+ if (a.activeTo && !b.activeTo) return 1;
2027
+ if (!a.activeTo && b.activeTo) return -1;
2028
+ return new Date(b.activeTo ?? b.activeFrom).getTime() - new Date(a.activeTo ?? a.activeFrom).getTime();
2029
+ }), (s) => s.activeTo && new Date(s.activeTo) < /* @__PURE__ */ new Date() ? "expired" : "running");
2030
+ }, [subscriptions]);
2031
+ const [showAllSubscriptions, setShowAllSubscriptions] = useState(groupedSubscriptions.expired?.some((s) => s.id === activeSubscriptionId));
2032
+ return /* @__PURE__ */ jsxs("div", {
2033
+ className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3",
2034
+ children: [
2035
+ groupedSubscriptions.running?.map((subscription) => /* @__PURE__ */ jsx(SubscriptionItem, {
2036
+ subscription,
2037
+ isSelected: activeSubscriptionId === subscription.id,
2038
+ isExpired: Boolean(subscription.activeTo && new Date(subscription.activeTo) < /* @__PURE__ */ new Date())
2039
+ }, subscription.id)),
2040
+ showAllSubscriptions && groupedSubscriptions.expired?.map((subscription) => /* @__PURE__ */ jsx(SubscriptionItem, {
2041
+ subscription,
2042
+ isSelected: activeSubscriptionId === subscription.id,
2043
+ isExpired: Boolean(subscription.activeTo && new Date(subscription.activeTo) < /* @__PURE__ */ new Date())
2044
+ }, subscription.id)),
2045
+ Boolean(groupedSubscriptions.expired?.length) && /* @__PURE__ */ jsx("div", {
2046
+ className: "self-center justify-self-center md:justify-self-start",
2047
+ children: /* @__PURE__ */ jsxs(Button, {
2048
+ variant: "ghost",
2049
+ size: "sm",
2050
+ onClick: () => setShowAllSubscriptions(!showAllSubscriptions),
2051
+ children: [showAllSubscriptions ? "Hide" : "Show", " expired subscriptions"]
2052
+ })
2053
+ })
2054
+ ]
2055
+ });
2056
+ };
2057
+ const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
2058
+ const willExpire = subscription.activeTo && new Date(subscription.activeTo) > /* @__PURE__ */ new Date();
2059
+ return /* @__PURE__ */ jsx(Link$1, {
2060
+ to: `/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`,
2061
+ children: /* @__PURE__ */ jsx(Item, {
2062
+ size: "sm",
2063
+ variant: "outline",
2064
+ className: cn(isSelected ? "border-primary bg-primary/5" : isExpired ? "opacity-50 hover:opacity-100 transition-opacity" : "border-muted bg-muted/5"),
2065
+ children: /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsxs(ItemTitle, { children: [/* @__PURE__ */ jsx("span", { children: subscription.name }), subscription.activeTo ? isExpired ? /* @__PURE__ */ jsx(Badge, {
2066
+ variant: "outline",
2067
+ className: "capitalize",
2068
+ children: "Expired"
2069
+ }) : /* @__PURE__ */ jsx(Badge, {
2070
+ variant: "muted",
2071
+ className: "capitalize",
2072
+ children: "Expiring"
2073
+ }) : /* @__PURE__ */ jsx(Badge, {
2074
+ className: "capitalize",
2075
+ children: formatDurationInterval(subscription.billingCadence)
2076
+ })] }), /* @__PURE__ */ jsxs(ItemDescription, {
2077
+ className: cn(willExpire && "text-destructive"),
2078
+ children: [
2079
+ subscription.activeTo ? willExpire ? "Expires on" : "Expired on" : new Date(subscription.activeFrom) > /* @__PURE__ */ new Date() ? "Starts on" : "Started on",
2080
+ " ",
2081
+ formatDate(subscription.activeTo ?? subscription.activeFrom)
2082
+ ]
2083
+ })] })
2084
+ }, subscription.id)
2085
+ });
2086
+ };
2087
+
2088
+ //#endregion
2089
+ //#region src/pages/SubscriptionsPage.tsx
2090
+ const SubscriptionsPage = () => {
2091
+ const deploymentName = useDeploymentName();
2092
+ const { data } = useSubscriptions(deploymentName);
2093
+ const [searchParams] = useSearchParams();
2094
+ const subscriptionId = searchParams.get("subscriptionId");
2095
+ const subscriptions = data?.items ?? [];
2096
+ const activeSubscription = useMemo(() => {
2097
+ if (subscriptions.length === 0) return null;
2098
+ if (subscriptionId) return subscriptions.find((s) => s.id === subscriptionId) ?? subscriptions[0];
2099
+ return subscriptions[0];
2100
+ }, [subscriptions, subscriptionId]);
2101
+ return /* @__PURE__ */ jsxs("div", {
2102
+ className: "w-full pt-(--padding-content-top) pb-(--padding-content-bottom)",
2103
+ children: [/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "My Subscriptions" }) }), /* @__PURE__ */ jsxs("div", {
2104
+ className: "max-w-4xl space-y-8",
2105
+ children: [
2106
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Heading, {
2107
+ level: 2,
2108
+ children: "My Subscriptions"
2109
+ }), /* @__PURE__ */ jsx("p", {
2110
+ className: "text-muted-foreground",
2111
+ children: "Manage your subscriptions, usage, and API keys"
2112
+ })] }),
2113
+ /* @__PURE__ */ jsx(SubscriptionsList, {
2114
+ subscriptions,
2115
+ activeSubscriptionId: activeSubscription?.id
2116
+ }),
2117
+ activeSubscription && /* @__PURE__ */ jsx(ActiveSubscription, {
2118
+ subscription: activeSubscription,
2119
+ deploymentName
2120
+ }, activeSubscription.id),
2121
+ subscriptions.length === 0 && /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, {
2122
+ className: "p-12 text-center",
2123
+ children: /* @__PURE__ */ jsx("p", {
2124
+ className: "text-muted-foreground",
2125
+ children: "No active subscriptions found."
2126
+ })
2127
+ }) })
2128
+ ]
2129
+ })]
2130
+ });
2131
+ };
2132
+
2133
+ //#endregion
2134
+ //#region src/ZuploMonetizationPlugin.tsx
2135
+ const PRICING_PATH = "/pricing";
2136
+ const zuploMonetizationPlugin = createPlugin((options = {}) => ({
2137
+ transformConfig: ({ config, merge }) => merge({
2138
+ apiKeys: { enabled: false },
2139
+ header: { navigation: [...config.header?.navigation ?? [], {
2140
+ label: "Pricing",
2141
+ to: PRICING_PATH
2142
+ }] }
2143
+ }),
2144
+ getIdentities: async (context) => {
2145
+ const deploymentName = context.env.ZUPLO_PUBLIC_DEPLOYMENT_NAME;
2146
+ if (!deploymentName) throw new Error("ZUPLO_PUBLIC_DEPLOYMENT_NAME is not set");
2147
+ return (await queryClient.fetchQuery({
2148
+ queryKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions`],
2149
+ meta: { context }
2150
+ })).items.flatMap((sub) => sub.status !== "active" ? [] : sub.consumer.apiKeys.flatMap((apiKey) => apiKey.expiresOn && new Date(apiKey.expiresOn) < /* @__PURE__ */ new Date() ? [] : {
2151
+ label: `${sub.name} (****${apiKey.key.slice(-5)})`,
2152
+ id: apiKey.id,
2153
+ authorizeRequest: async (request) => {
2154
+ request.headers.set("Authorization", `Bearer ${apiKey.key}`);
2155
+ return request;
2156
+ },
2157
+ authorizationFields: { headers: ["Authorization"] }
2158
+ }));
2159
+ },
2160
+ getProfileMenuItems: () => [{
2161
+ label: "My Subscriptions",
2162
+ path: "/subscriptions",
2163
+ icon: StarsIcon
2164
+ }, {
2165
+ label: "Manage payment details",
2166
+ path: "/manage-payment",
2167
+ target: "_blank",
2168
+ icon: CreditCardIcon
2169
+ }],
2170
+ getRoutes: () => {
2171
+ return [{
2172
+ element: /* @__PURE__ */ jsx(ZuploMonetizationWrapper, { options }),
2173
+ handle: { layout: "none" },
2174
+ children: [
2175
+ {
2176
+ path: "/checkout",
2177
+ element: /* @__PURE__ */ jsx(CheckoutPage, {})
2178
+ },
2179
+ {
2180
+ path: "/checkout-confirm",
2181
+ element: /* @__PURE__ */ jsx(CheckoutConfirmPage, {})
2182
+ },
2183
+ {
2184
+ path: "/subscription-change-confirm",
2185
+ element: /* @__PURE__ */ jsx(SubscriptionChangeConfirmPage, {})
2186
+ },
2187
+ {
2188
+ path: "/manage-payment",
2189
+ element: /* @__PURE__ */ jsx(ManagePaymentPage, {})
2190
+ },
2191
+ {
2192
+ path: PRICING_PATH,
2193
+ handle: { layout: "default" },
2194
+ element: /* @__PURE__ */ jsx(PricingPage, {})
2195
+ },
2196
+ {
2197
+ handle: { layout: "default" },
2198
+ path: "/subscriptions",
2199
+ element: /* @__PURE__ */ jsx(SubscriptionsPage, {})
2200
+ }
2201
+ ]
2202
+ }];
2203
+ },
2204
+ getProtectedRoutes: () => {
2205
+ return [
2206
+ "/checkout",
2207
+ "/checkout-confirm",
2208
+ "/subscription-change-confirm",
2209
+ "/subscriptions",
2210
+ "/manage-payment"
2211
+ ];
2212
+ }
2213
+ }));
2214
+
2215
+ //#endregion
2216
+ export { zuploMonetizationPlugin };