@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.d.mts +17 -0
- package/dist/index.mjs +2216 -0
- package/package.json +55 -0
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 };
|