@zuplo/zudoku-plugin-monetization 0.0.2
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/LICENSE.md +18 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +931 -0
- package/package.json +36 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) Zuplo, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction,
|
|
7
|
+
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
8
|
+
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
|
15
|
+
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
|
17
|
+
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ZudokuConfig, ZudokuPlugin } from "zudoku";
|
|
2
|
+
|
|
3
|
+
//#region src/ZuploMonetizationPlugin.d.ts
|
|
4
|
+
type ZudokuMonetizationPluginOptions = {
|
|
5
|
+
environmentName: string;
|
|
6
|
+
};
|
|
7
|
+
declare const enableMonetization: (config: ZudokuConfig, options: ZudokuMonetizationPluginOptions) => ZudokuConfig;
|
|
8
|
+
declare const zuploMonetizationPlugin: (options: ZudokuMonetizationPluginOptions) => ZudokuPlugin;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { enableMonetization, zuploMonetizationPlugin };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
import { ArrowLeftIcon, CheckIcon, ClockIcon, LockIcon, RefreshCwIcon, ShieldIcon, StarsIcon, Trash2Icon } from "zudoku/icons";
|
|
2
|
+
import { Link, Outlet, useNavigate, useParams, useSearchParams } from "zudoku/router";
|
|
3
|
+
import { Button, Heading } from "zudoku/components";
|
|
4
|
+
import { useAuth, useZudoku } from "zudoku/hooks";
|
|
5
|
+
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient, useSuspenseQuery } from "zudoku/react-query";
|
|
6
|
+
import { Alert, AlertDescription, AlertTitle } from "zudoku/ui/Alert";
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "zudoku/ui/Card";
|
|
8
|
+
import { Separator } from "zudoku/ui/Separator";
|
|
9
|
+
import { cn, joinUrl } from "zudoku";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { parse } from "tinyduration";
|
|
12
|
+
import { useMemo } from "react";
|
|
13
|
+
import { Button as Button$1 } from "zudoku/ui/Button";
|
|
14
|
+
import { Secret } from "zudoku/ui/Secret";
|
|
15
|
+
import { Item, ItemContent, ItemTitle } from "zudoku/ui/Item";
|
|
16
|
+
import { Progress } from "zudoku/ui/Progress";
|
|
17
|
+
|
|
18
|
+
//#region src/components/FeatureItem.tsx
|
|
19
|
+
const FeatureItem = ({ feature, className }) => {
|
|
20
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
21
|
+
className: cn("flex items-start gap-2", className),
|
|
22
|
+
children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("div", {
|
|
23
|
+
className: "text-sm",
|
|
24
|
+
children: feature.value ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
25
|
+
/* @__PURE__ */ jsxs("span", {
|
|
26
|
+
className: "font-medium",
|
|
27
|
+
children: [feature.name, ":"]
|
|
28
|
+
}),
|
|
29
|
+
" ",
|
|
30
|
+
feature.value
|
|
31
|
+
] }) : feature.name
|
|
32
|
+
})]
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/components/QuotaItem.tsx
|
|
38
|
+
const QuotaItem = ({ quota, className }) => {
|
|
39
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
40
|
+
className: cn("flex items-start gap-2", className),
|
|
41
|
+
children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsxs("div", {
|
|
42
|
+
className: "text-sm",
|
|
43
|
+
children: [
|
|
44
|
+
/* @__PURE__ */ jsxs("span", {
|
|
45
|
+
className: "font-medium",
|
|
46
|
+
children: [quota.name, ":"]
|
|
47
|
+
}),
|
|
48
|
+
" ",
|
|
49
|
+
quota.limit.toLocaleString(),
|
|
50
|
+
" / ",
|
|
51
|
+
quota.period,
|
|
52
|
+
quota.overagePrice && /* @__PURE__ */ jsxs("div", {
|
|
53
|
+
className: "text-xs text-muted-foreground mt-0.5",
|
|
54
|
+
children: [
|
|
55
|
+
"+",
|
|
56
|
+
quota.overagePrice,
|
|
57
|
+
" after quota"
|
|
58
|
+
]
|
|
59
|
+
})
|
|
60
|
+
]
|
|
61
|
+
})]
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/hooks/usePlans.ts
|
|
67
|
+
const usePlans = (environmentName) => {
|
|
68
|
+
return useSuspenseQuery({ queryKey: [`/v3/zudoku-metering/${environmentName}/pricing-page`] });
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/utils/formatDuration.ts
|
|
73
|
+
const formatDuration = (iso) => {
|
|
74
|
+
try {
|
|
75
|
+
const d = parse(iso);
|
|
76
|
+
if (d.months === 1) return "month";
|
|
77
|
+
if (d.months && d.months > 1) return `${d.months} months`;
|
|
78
|
+
if (d.years === 1) return "year";
|
|
79
|
+
if (d.years && d.years > 1) return `${d.years} years`;
|
|
80
|
+
if (d.weeks === 1) return "week";
|
|
81
|
+
if (d.weeks && d.weeks > 1) return `${d.weeks} weeks`;
|
|
82
|
+
if (d.days === 1) return "day";
|
|
83
|
+
if (d.days && d.days > 1) return `${d.days} days`;
|
|
84
|
+
return iso;
|
|
85
|
+
} catch {
|
|
86
|
+
return iso;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/utils/categorizeRateCards.ts
|
|
92
|
+
const categorizeRateCards = (rateCards) => {
|
|
93
|
+
const quotas = [];
|
|
94
|
+
const features = [];
|
|
95
|
+
for (const rc of rateCards) {
|
|
96
|
+
const et = rc.entitlementTemplate;
|
|
97
|
+
if (!et) continue;
|
|
98
|
+
if (et.type === "metered" && et.issueAfterReset != null) {
|
|
99
|
+
let overagePrice;
|
|
100
|
+
if (rc.price?.type === "tiered" && rc.price.tiers) {
|
|
101
|
+
const overageTier = rc.price.tiers.find((t) => t.unitPrice?.amount);
|
|
102
|
+
if (overageTier?.unitPrice) overagePrice = `$${parseFloat(overageTier.unitPrice.amount).toFixed(2)}/unit`;
|
|
103
|
+
}
|
|
104
|
+
quotas.push({
|
|
105
|
+
key: rc.featureKey,
|
|
106
|
+
name: rc.name,
|
|
107
|
+
limit: et.issueAfterReset,
|
|
108
|
+
period: et.usagePeriod ? formatDuration(et.usagePeriod) : "Month",
|
|
109
|
+
overagePrice
|
|
110
|
+
});
|
|
111
|
+
} else if (et.type === "boolean") features.push({
|
|
112
|
+
key: rc.featureKey,
|
|
113
|
+
name: rc.name
|
|
114
|
+
});
|
|
115
|
+
else if (et.type === "static" && et.config) try {
|
|
116
|
+
const config = JSON.parse(et.config);
|
|
117
|
+
features.push({
|
|
118
|
+
key: rc.featureKey,
|
|
119
|
+
name: rc.name,
|
|
120
|
+
value: String(config.value)
|
|
121
|
+
});
|
|
122
|
+
} catch {
|
|
123
|
+
features.push({
|
|
124
|
+
key: rc.featureKey,
|
|
125
|
+
name: rc.name
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
quotas,
|
|
131
|
+
features
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/utils/getPriceFromPlan.ts
|
|
137
|
+
const getPriceFromPlan = (plan) => {
|
|
138
|
+
const defaultPhase = plan.phases.at(-1);
|
|
139
|
+
if (!defaultPhase) return {
|
|
140
|
+
monthly: 0,
|
|
141
|
+
yearly: 0
|
|
142
|
+
};
|
|
143
|
+
const flatFeeCard = defaultPhase.rateCards.find((rc) => rc.type === "flat_fee" && rc.price?.type === "flat");
|
|
144
|
+
const monthlyAmount = flatFeeCard?.price.amount ? parseInt(flatFeeCard.price.amount, 10) : 0;
|
|
145
|
+
return {
|
|
146
|
+
monthly: monthlyAmount,
|
|
147
|
+
yearly: monthlyAmount * 12
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/pages/CheckoutConfimPage.tsx
|
|
153
|
+
const formatBillingCycle = (duration) => {
|
|
154
|
+
if (duration === "month") return "monthly";
|
|
155
|
+
if (duration === "year") return "annually";
|
|
156
|
+
if (duration === "week") return "weekly";
|
|
157
|
+
if (duration === "day") return "daily";
|
|
158
|
+
if (duration.includes(" ")) return `every ${duration}`;
|
|
159
|
+
return `every ${duration}`;
|
|
160
|
+
};
|
|
161
|
+
const CheckoutConfirmPage = ({ environmentName }) => {
|
|
162
|
+
const [search] = useSearchParams();
|
|
163
|
+
const planId = search.get("plan");
|
|
164
|
+
const auth = useAuth();
|
|
165
|
+
const zudoku = useZudoku();
|
|
166
|
+
const navigate = useNavigate();
|
|
167
|
+
const { data: plans } = usePlans(environmentName);
|
|
168
|
+
const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
|
|
169
|
+
const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
|
|
170
|
+
const { quotas, features } = categorizeRateCards(rateCards ?? []);
|
|
171
|
+
const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
|
|
172
|
+
const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
|
|
173
|
+
const createSubscriptionMutation = useMutation({
|
|
174
|
+
mutationFn: async () => {
|
|
175
|
+
if (!auth.profile?.email) throw new Error("No email found for user. Make sure your Authentication Provider exposes the email address.");
|
|
176
|
+
const signedRequest = await zudoku.signRequest(new Request(`https://api.zuploedge.com/v3/zudoku-metering/${environmentName}/subscriptions`, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: { "Content-Type": "application/json" },
|
|
179
|
+
body: JSON.stringify({ planId })
|
|
180
|
+
}));
|
|
181
|
+
const response = await fetch(signedRequest);
|
|
182
|
+
const subscription = await response.json();
|
|
183
|
+
if (!response.ok) throw new Error(subscription.message);
|
|
184
|
+
return subscription.id;
|
|
185
|
+
},
|
|
186
|
+
onSuccess: (subscriptionId) => {
|
|
187
|
+
navigate(`/subscriptions/${subscriptionId}`);
|
|
188
|
+
},
|
|
189
|
+
onError: (error) => {
|
|
190
|
+
console.error("Error creating subscription:", error);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return /* @__PURE__ */ jsx("div", {
|
|
194
|
+
className: "w-full bg-muted min-h-screen flex items-center justify-center px-4 py-12 gap-4",
|
|
195
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
196
|
+
className: "max-w-2xl w-full",
|
|
197
|
+
children: [
|
|
198
|
+
/* @__PURE__ */ jsxs("div", {
|
|
199
|
+
className: "flex gap-2 text-muted-foreground text-sm items-center pt-4 pb-4",
|
|
200
|
+
children: [/* @__PURE__ */ jsx(ArrowLeftIcon, { className: "size-4" }), "Your payment is secured by Stripe"]
|
|
201
|
+
}),
|
|
202
|
+
" ",
|
|
203
|
+
createSubscriptionMutation.isError && /* @__PURE__ */ jsxs(Alert, {
|
|
204
|
+
className: "mb-4",
|
|
205
|
+
children: [/* @__PURE__ */ jsx(AlertTitle, { children: "Error" }), /* @__PURE__ */ jsx(AlertDescription, { children: createSubscriptionMutation.error.message })]
|
|
206
|
+
}),
|
|
207
|
+
/* @__PURE__ */ jsxs(Card, {
|
|
208
|
+
className: "p-8 w-full max-w-7xl",
|
|
209
|
+
children: [
|
|
210
|
+
/* @__PURE__ */ jsx("div", {
|
|
211
|
+
className: "flex justify-center mb-6",
|
|
212
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
213
|
+
className: "rounded-full bg-primary/10 p-3",
|
|
214
|
+
children: /* @__PURE__ */ jsx(CheckIcon, { className: "size-9 text-primary" })
|
|
215
|
+
})
|
|
216
|
+
}),
|
|
217
|
+
/* @__PURE__ */ jsxs("div", {
|
|
218
|
+
className: "text-center mb-8",
|
|
219
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
220
|
+
className: "text-2xl font-bold text-card-foreground mb-3",
|
|
221
|
+
children: "Review you subscription"
|
|
222
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
223
|
+
className: "text-muted-foreground text-base",
|
|
224
|
+
children: "Please confirm the details below before completing your purchase."
|
|
225
|
+
})]
|
|
226
|
+
}),
|
|
227
|
+
selectedPlan && /* @__PURE__ */ jsxs(Card, {
|
|
228
|
+
className: "bg-muted/50",
|
|
229
|
+
children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, {
|
|
230
|
+
className: "flex justify-between items-start",
|
|
231
|
+
children: [
|
|
232
|
+
/* @__PURE__ */ jsxs("div", {
|
|
233
|
+
className: "flex items-center gap-3",
|
|
234
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
235
|
+
className: "flex flex-col text-2xl font-bold bg-primary text-primary-foreground items-center justify-center rounded size-12",
|
|
236
|
+
children: selectedPlan.name.at(0)?.toUpperCase()
|
|
237
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
238
|
+
className: "flex flex-col",
|
|
239
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
240
|
+
className: "text-lg font-bold",
|
|
241
|
+
children: selectedPlan.name
|
|
242
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
243
|
+
className: "text-sm font-normal text-muted-foreground",
|
|
244
|
+
children: selectedPlan.description || "Selected plan"
|
|
245
|
+
})]
|
|
246
|
+
})]
|
|
247
|
+
}),
|
|
248
|
+
price && price.monthly > 0 && /* @__PURE__ */ jsxs("div", {
|
|
249
|
+
className: "text-right",
|
|
250
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
251
|
+
className: "text-2xl font-bold",
|
|
252
|
+
children: ["$", price.monthly.toLocaleString()]
|
|
253
|
+
}), billingCycle && /* @__PURE__ */ jsxs("div", {
|
|
254
|
+
className: "text-sm text-muted-foreground font-normal",
|
|
255
|
+
children: ["Billed ", formatBillingCycle(billingCycle)]
|
|
256
|
+
})]
|
|
257
|
+
}),
|
|
258
|
+
price && price.monthly === 0 && /* @__PURE__ */ jsx("div", {
|
|
259
|
+
className: "text-2xl text-muted-foreground font-bold",
|
|
260
|
+
children: "Free"
|
|
261
|
+
})
|
|
262
|
+
]
|
|
263
|
+
}) }), /* @__PURE__ */ jsxs(CardContent, { children: [
|
|
264
|
+
/* @__PURE__ */ jsx(Separator, {}),
|
|
265
|
+
/* @__PURE__ */ jsx("div", {
|
|
266
|
+
className: "text-sm font-medium mb-3 mt-3",
|
|
267
|
+
children: "What's included:"
|
|
268
|
+
}),
|
|
269
|
+
/* @__PURE__ */ jsxs("div", {
|
|
270
|
+
className: "grid grid-cols-2 gap-2 text-muted-foreground",
|
|
271
|
+
children: [quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, {
|
|
272
|
+
quota,
|
|
273
|
+
className: "text-muted-foreground"
|
|
274
|
+
}, quota.key)), features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, {
|
|
275
|
+
feature,
|
|
276
|
+
className: "text-muted-foreground"
|
|
277
|
+
}, feature.key))]
|
|
278
|
+
})
|
|
279
|
+
] })]
|
|
280
|
+
}),
|
|
281
|
+
/* @__PURE__ */ jsxs("div", {
|
|
282
|
+
className: "space-y-3 mt-4",
|
|
283
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
284
|
+
className: "w-full",
|
|
285
|
+
onClick: () => createSubscriptionMutation.mutate(),
|
|
286
|
+
disabled: createSubscriptionMutation.isPending,
|
|
287
|
+
children: createSubscriptionMutation.isPending ? "Processing Payment..." : "Confirm & Subscribe"
|
|
288
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
289
|
+
variant: "outline",
|
|
290
|
+
className: "w-full",
|
|
291
|
+
asChild: true
|
|
292
|
+
})]
|
|
293
|
+
}),
|
|
294
|
+
/* @__PURE__ */ jsx("div", {
|
|
295
|
+
className: "mt-6 pt-6 border-t text-center",
|
|
296
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
297
|
+
className: "text-xs text-muted-foreground",
|
|
298
|
+
children: "By confirming, you agree to our Terms of Service and Privacy Policy. You can cancel anytime."
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
]
|
|
302
|
+
}),
|
|
303
|
+
/* @__PURE__ */ jsxs("div", {
|
|
304
|
+
className: "flex items-center gap-2 text-muted-foreground text-xs item-center justify-center pt-4",
|
|
305
|
+
children: [/* @__PURE__ */ jsx(LockIcon, { className: "size-3" }), "Your payment is secured by Stripe"]
|
|
306
|
+
})
|
|
307
|
+
]
|
|
308
|
+
})
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
var CheckoutConfimPage_default = CheckoutConfirmPage;
|
|
312
|
+
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/pages/CheckoutFailedPage.tsx
|
|
315
|
+
const CheckoutFailedPage = () => {
|
|
316
|
+
return /* @__PURE__ */ jsx("div", { children: "CheckoutFailedPage" });
|
|
317
|
+
};
|
|
318
|
+
var CheckoutFailedPage_default = CheckoutFailedPage;
|
|
319
|
+
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/hooks/useUrlUtils.ts
|
|
322
|
+
const useUrlUtils = () => {
|
|
323
|
+
const basePath = useZudoku().options.basePath;
|
|
324
|
+
return { generateUrl: (path) => {
|
|
325
|
+
if (!window.location.origin) throw new Error("Only works in browser environment");
|
|
326
|
+
return joinUrl(window.location.origin, basePath, path);
|
|
327
|
+
} };
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/pages/CheckoutPage.tsx
|
|
332
|
+
const CheckoutPage = ({ environmentName }) => {
|
|
333
|
+
const [search] = useSearchParams();
|
|
334
|
+
const zudoku = useZudoku();
|
|
335
|
+
const planId = search.get("plan");
|
|
336
|
+
const auth = useAuth();
|
|
337
|
+
const { generateUrl } = useUrlUtils();
|
|
338
|
+
const { data: _data } = useQuery({
|
|
339
|
+
queryKey: ["plan", planId],
|
|
340
|
+
queryFn: async () => {
|
|
341
|
+
if (!auth.profile?.email) throw new Error("No email found for user. Make sure your Authentication Provider exposes the email address.");
|
|
342
|
+
const request = await zudoku.signRequest(new Request(`https://api.zuploedge.com/v3/zudoku-metering/${environmentName}/stripe/checkout`, {
|
|
343
|
+
method: "POST",
|
|
344
|
+
headers: { "Content-Type": "application/json" },
|
|
345
|
+
body: JSON.stringify({
|
|
346
|
+
email: auth.profile?.email,
|
|
347
|
+
planId,
|
|
348
|
+
successURL: generateUrl(`/checkout-confirm`) + `?${planId ? `plan=${planId}` : ""}`,
|
|
349
|
+
cancelURL: generateUrl(`/checkout-failed`) + `?${planId ? `plan=${planId}` : ""}`
|
|
350
|
+
})
|
|
351
|
+
}));
|
|
352
|
+
console.log({
|
|
353
|
+
email: auth.profile?.email,
|
|
354
|
+
planId,
|
|
355
|
+
successURL: `${generateUrl(`/checkout-confirm`)}?${planId ? `plan=${planId}` : ""}`,
|
|
356
|
+
cancelURL: `${generateUrl(`/checkout-failed`)}?${planId ? `plan=${planId}` : ""}`
|
|
357
|
+
});
|
|
358
|
+
const checkoutRequest = await fetch(request).then((res) => res.json());
|
|
359
|
+
if (checkoutRequest.url) window.location.href = checkoutRequest.url;
|
|
360
|
+
return checkoutRequest;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
return /* @__PURE__ */ jsx("div", {
|
|
364
|
+
className: "flex min-h-screen items-center justify-center bg-muted",
|
|
365
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
366
|
+
className: "flex max-w-md flex-col items-center space-y-6 text-center",
|
|
367
|
+
children: [
|
|
368
|
+
/* @__PURE__ */ jsx("div", {
|
|
369
|
+
className: "relative",
|
|
370
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
371
|
+
className: "flex h-24 w-24 items-center justify-center rounded-full bg-foreground/10",
|
|
372
|
+
children: /* @__PURE__ */ jsx(ShieldIcon, { className: "w-12 h-12 text-foreground" })
|
|
373
|
+
})
|
|
374
|
+
}),
|
|
375
|
+
/* @__PURE__ */ jsxs("div", {
|
|
376
|
+
className: "space-y-2",
|
|
377
|
+
children: [
|
|
378
|
+
/* @__PURE__ */ jsx("h2", {
|
|
379
|
+
className: "text-2xl font-bold text-card-foreground",
|
|
380
|
+
children: "Establishing encrypted connection..."
|
|
381
|
+
}),
|
|
382
|
+
/* @__PURE__ */ jsx("p", {
|
|
383
|
+
className: "text-muted-foreground",
|
|
384
|
+
children: "Setting up your secure checkout experience"
|
|
385
|
+
}),
|
|
386
|
+
/* @__PURE__ */ jsx("p", {
|
|
387
|
+
className: "text-sm text-muted-foreground",
|
|
388
|
+
children: "Powered by Stripe for maximum security"
|
|
389
|
+
})
|
|
390
|
+
]
|
|
391
|
+
}),
|
|
392
|
+
/* @__PURE__ */ jsxs("div", {
|
|
393
|
+
className: "flex space-x-2",
|
|
394
|
+
children: [
|
|
395
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.3s]" }),
|
|
396
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.15s]" }),
|
|
397
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-3 animate-pulse rounded-full bg-primary" })
|
|
398
|
+
]
|
|
399
|
+
})
|
|
400
|
+
]
|
|
401
|
+
})
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
var CheckoutPage_default = CheckoutPage;
|
|
405
|
+
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/pages/PricingPage.tsx
|
|
408
|
+
const PricingCard = ({ plan, isPopular = false }) => {
|
|
409
|
+
const defaultPhase = plan.phases.at(-1);
|
|
410
|
+
if (!defaultPhase) return null;
|
|
411
|
+
const { quotas, features } = categorizeRateCards(defaultPhase.rateCards);
|
|
412
|
+
const price = getPriceFromPlan(plan);
|
|
413
|
+
const isFree = price.monthly === 0;
|
|
414
|
+
const isCustom = plan.key === "enterprise";
|
|
415
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
416
|
+
className: cn("relative rounded-lg border p-6 flex flex-col min-w-72", isPopular && "border-primary border-2"),
|
|
417
|
+
children: [
|
|
418
|
+
isPopular && /* @__PURE__ */ jsx("div", {
|
|
419
|
+
className: "absolute top-0 -translate-y-1/2 left-1/2 -translate-x-1/2 whitespace-nowrap",
|
|
420
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
421
|
+
className: "bg-primary text-primary-foreground text-xs font-semibold px-3 py-1 rounded-full uppercase",
|
|
422
|
+
children: "Most Popular"
|
|
423
|
+
})
|
|
424
|
+
}),
|
|
425
|
+
/* @__PURE__ */ jsxs("div", {
|
|
426
|
+
className: "mb-4 pb-4 border-b",
|
|
427
|
+
children: [
|
|
428
|
+
/* @__PURE__ */ jsx("h3", {
|
|
429
|
+
className: "text-base font-semibold text-muted-foreground mb-2",
|
|
430
|
+
children: plan.name
|
|
431
|
+
}),
|
|
432
|
+
/* @__PURE__ */ jsx("div", {
|
|
433
|
+
className: "flex items-baseline gap-1 flex-wrap",
|
|
434
|
+
children: isCustom ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
435
|
+
className: "text-3xl font-bold text-card-foreground",
|
|
436
|
+
children: "Custom"
|
|
437
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
438
|
+
className: "text-sm text-muted-foreground mt-1",
|
|
439
|
+
children: "Contact Sales"
|
|
440
|
+
})] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
441
|
+
className: "text-3xl font-bold text-card-foreground",
|
|
442
|
+
children: isFree ? "Free" : `$${price.monthly.toLocaleString()}`
|
|
443
|
+
}), !isFree && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
444
|
+
className: "text-muted-foreground text-sm",
|
|
445
|
+
children: "/mo"
|
|
446
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
447
|
+
className: "w-full text-sm text-muted-foreground mt-1",
|
|
448
|
+
children: [
|
|
449
|
+
"$",
|
|
450
|
+
price.yearly.toLocaleString(),
|
|
451
|
+
"/year"
|
|
452
|
+
]
|
|
453
|
+
})] })] })
|
|
454
|
+
}),
|
|
455
|
+
isFree && /* @__PURE__ */ jsx("div", {
|
|
456
|
+
className: "text-sm text-muted-foreground mt-1",
|
|
457
|
+
children: "No CC required"
|
|
458
|
+
})
|
|
459
|
+
]
|
|
460
|
+
}),
|
|
461
|
+
/* @__PURE__ */ jsxs("div", {
|
|
462
|
+
className: "space-y-4 mb-6 flex-grow",
|
|
463
|
+
children: [quotas.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
464
|
+
className: "space-y-2",
|
|
465
|
+
children: quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, { quota }, quota.key))
|
|
466
|
+
}), features.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
467
|
+
className: "space-y-2",
|
|
468
|
+
children: features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, { feature }, feature.key))
|
|
469
|
+
})]
|
|
470
|
+
}),
|
|
471
|
+
/* @__PURE__ */ jsx(Button, {
|
|
472
|
+
variant: isPopular ? "default" : "secondary",
|
|
473
|
+
asChild: true,
|
|
474
|
+
children: /* @__PURE__ */ jsx(Link, {
|
|
475
|
+
to: `/checkout?plan=${plan.id}`,
|
|
476
|
+
children: "Subscribe"
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
]
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
const PricingPage = ({ environmentName }) => {
|
|
483
|
+
const { data: pricingTableData } = usePlans(environmentName);
|
|
484
|
+
const planOrder = [
|
|
485
|
+
"developer",
|
|
486
|
+
"startup",
|
|
487
|
+
"pro",
|
|
488
|
+
"business",
|
|
489
|
+
"enterprise"
|
|
490
|
+
];
|
|
491
|
+
const sortedPlans = [...pricingTableData.items].sort((a, b) => {
|
|
492
|
+
return planOrder.indexOf(a.key) - planOrder.indexOf(b.key);
|
|
493
|
+
});
|
|
494
|
+
const getGridCols = (count) => {
|
|
495
|
+
if (count === 1) return "lg:grid-cols-1";
|
|
496
|
+
if (count === 2) return "lg:grid-cols-2";
|
|
497
|
+
if (count === 3) return "lg:grid-cols-3";
|
|
498
|
+
if (count === 4) return "lg:grid-cols-4";
|
|
499
|
+
return "lg:grid-cols-5";
|
|
500
|
+
};
|
|
501
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
502
|
+
className: "w-full px-4 py-12",
|
|
503
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
504
|
+
className: "text-center mb-12",
|
|
505
|
+
children: [/* @__PURE__ */ jsx(Heading, {
|
|
506
|
+
level: 1,
|
|
507
|
+
children: "Pricing"
|
|
508
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
509
|
+
className: "text-lg text-gray-600 dark:text-gray-400",
|
|
510
|
+
children: "Global live music data, flexible plans for every scale"
|
|
511
|
+
})]
|
|
512
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
513
|
+
className: "flex justify-center",
|
|
514
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
515
|
+
className: cn("w-full md:w-auto grid grid-cols-1 md:grid-cols-2 gap-6 md:max-w-fit", getGridCols(sortedPlans.length)),
|
|
516
|
+
children: sortedPlans.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
|
|
517
|
+
plan,
|
|
518
|
+
isPopular: plan.key === "pro"
|
|
519
|
+
}, plan.id))
|
|
520
|
+
})
|
|
521
|
+
})]
|
|
522
|
+
});
|
|
523
|
+
};
|
|
524
|
+
var PricingPage_default = PricingPage;
|
|
525
|
+
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/hooks/useSubscriptions.ts
|
|
528
|
+
const useSubscriptions = (environmentName) => {
|
|
529
|
+
return useSuspenseQuery({
|
|
530
|
+
meta: { context: useZudoku() },
|
|
531
|
+
queryKey: [`/v3/zudoku-metering/${environmentName}/subscriptions`]
|
|
532
|
+
});
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/ZuploMonetizationWrapper.tsx
|
|
537
|
+
const BASE_URL = "https://api.zuploedge.com";
|
|
538
|
+
const createMutationFn = (url, context, init) => {
|
|
539
|
+
return async () => {
|
|
540
|
+
const request = new Request(`${BASE_URL}${url}`, {
|
|
541
|
+
method: "POST",
|
|
542
|
+
...init,
|
|
543
|
+
headers: {
|
|
544
|
+
"Content-Type": "application/json",
|
|
545
|
+
...init?.headers
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
const response = await fetch(context ? await context.signRequest(request) : request);
|
|
549
|
+
if (!response.ok) {
|
|
550
|
+
const errorText = await response.text();
|
|
551
|
+
throw new Error(`Request failed: ${response.status} ${errorText}`);
|
|
552
|
+
}
|
|
553
|
+
return response.json();
|
|
554
|
+
};
|
|
555
|
+
};
|
|
556
|
+
const client = new QueryClient({ defaultOptions: {
|
|
557
|
+
queries: {
|
|
558
|
+
retry: false,
|
|
559
|
+
queryFn: async (q) => {
|
|
560
|
+
if (!Array.isArray(q.queryKey)) throw new Error("Query key must be an array");
|
|
561
|
+
if (q.queryKey.length === 0) throw new Error("Query key must be a non-empty array");
|
|
562
|
+
const url = q.queryKey[0];
|
|
563
|
+
if (!url || typeof url !== "string") throw new Error("URL is required");
|
|
564
|
+
const init = q.queryKey[1] ?? {};
|
|
565
|
+
const request = new Request(`${BASE_URL}${url}`, {
|
|
566
|
+
...init,
|
|
567
|
+
headers: {
|
|
568
|
+
"Content-Type": "application/json",
|
|
569
|
+
...init.headers
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
const response = await fetch(q.meta?.context ? await q.meta.context.signRequest(request) : request);
|
|
573
|
+
if (!response.ok) throw new Error("Failed to fetch request");
|
|
574
|
+
return response.json();
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
mutations: { retry: false }
|
|
578
|
+
} });
|
|
579
|
+
const ZuploMonetizationWrapper = () => {
|
|
580
|
+
return /* @__PURE__ */ jsx(QueryClientProvider, {
|
|
581
|
+
client,
|
|
582
|
+
children: /* @__PURE__ */ jsx(Outlet, {})
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
var ZuploMonetizationWrapper_default = ZuploMonetizationWrapper;
|
|
586
|
+
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region src/pages/subscriptions/ApiKey.tsx
|
|
589
|
+
const ApiKey = ({ apiKey, createdAt, lastUsed, expiresAt, isActive = true, label, apiKeyId }) => {
|
|
590
|
+
const formatDate = (dateString) => {
|
|
591
|
+
if (!dateString) return "";
|
|
592
|
+
return new Date(dateString).toLocaleDateString("en-US", {
|
|
593
|
+
month: "short",
|
|
594
|
+
day: "numeric",
|
|
595
|
+
year: "numeric"
|
|
596
|
+
});
|
|
597
|
+
};
|
|
598
|
+
const getTimeAgo = (dateString) => {
|
|
599
|
+
if (!dateString) return "Never";
|
|
600
|
+
const date = new Date(dateString);
|
|
601
|
+
const now = /* @__PURE__ */ new Date();
|
|
602
|
+
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1e3 * 60));
|
|
603
|
+
if (diffInMinutes < 1) return "Just now";
|
|
604
|
+
if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`;
|
|
605
|
+
const diffInHours = Math.floor(diffInMinutes / 60);
|
|
606
|
+
if (diffInHours < 24) return `${diffInHours} hours ago`;
|
|
607
|
+
const diffInDays = Math.floor(diffInHours / 24);
|
|
608
|
+
if (diffInDays === 1) return "1 day ago";
|
|
609
|
+
if (diffInDays < 30) return `${diffInDays} days ago`;
|
|
610
|
+
return formatDate(dateString);
|
|
611
|
+
};
|
|
612
|
+
const isExpiring = expiresAt && new Date(expiresAt) < new Date(Date.now() + 720 * 60 * 60 * 1e3);
|
|
613
|
+
const isExpired = expiresAt && new Date(expiresAt) < /* @__PURE__ */ new Date();
|
|
614
|
+
const handleDelete = () => {
|
|
615
|
+
console.log("Delete API Key", apiKeyId);
|
|
616
|
+
};
|
|
617
|
+
return /* @__PURE__ */ jsx("div", {
|
|
618
|
+
className: isExpiring && !isExpired ? "border-l-4 border-yellow-500 pl-4" : "",
|
|
619
|
+
children: /* @__PURE__ */ jsx(Card, {
|
|
620
|
+
className: isExpiring && !isExpired ? "border-yellow-200 bg-yellow-50" : "",
|
|
621
|
+
children: /* @__PURE__ */ jsx(CardContent, {
|
|
622
|
+
className: "p-6",
|
|
623
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
624
|
+
className: "space-y-4",
|
|
625
|
+
children: [
|
|
626
|
+
/* @__PURE__ */ jsx("div", {
|
|
627
|
+
className: "flex items-center justify-between",
|
|
628
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
629
|
+
className: "flex items-center gap-2",
|
|
630
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
631
|
+
className: "font-semibold text-base",
|
|
632
|
+
children: label || "API Key"
|
|
633
|
+
}), isActive ? /* @__PURE__ */ jsx("span", {
|
|
634
|
+
className: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800",
|
|
635
|
+
children: "Active"
|
|
636
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
637
|
+
className: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800",
|
|
638
|
+
children: "Expiring"
|
|
639
|
+
})]
|
|
640
|
+
})
|
|
641
|
+
}),
|
|
642
|
+
/* @__PURE__ */ jsxs("div", {
|
|
643
|
+
className: "flex items-center gap-1.5 text-sm text-muted-foreground",
|
|
644
|
+
children: [
|
|
645
|
+
/* @__PURE__ */ jsxs("div", {
|
|
646
|
+
className: "flex items-center gap-1.5",
|
|
647
|
+
children: [/* @__PURE__ */ jsx(ClockIcon, { className: "size-3.5" }), /* @__PURE__ */ jsxs("span", { children: ["Created ", formatDate(createdAt)] })]
|
|
648
|
+
}),
|
|
649
|
+
/* @__PURE__ */ jsx("span", { children: "•" }),
|
|
650
|
+
/* @__PURE__ */ jsxs("span", { children: ["Last used ", getTimeAgo(lastUsed)] }),
|
|
651
|
+
expiresAt,
|
|
652
|
+
expiresAt && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { children: "•" }), /* @__PURE__ */ jsxs("span", {
|
|
653
|
+
className: isExpired ? "text-red-700 font-medium" : isExpiring ? "text-yellow-700 font-medium" : "",
|
|
654
|
+
children: ["Expires ", formatDate(expiresAt)]
|
|
655
|
+
})] })
|
|
656
|
+
]
|
|
657
|
+
}),
|
|
658
|
+
/* @__PURE__ */ jsxs("div", {
|
|
659
|
+
className: "flex items-center gap-2 rounded-md font-mono text-sm",
|
|
660
|
+
children: [/* @__PURE__ */ jsx(Secret, {
|
|
661
|
+
secret: apiKey,
|
|
662
|
+
status: isActive ? "active" : "expiring"
|
|
663
|
+
}), !isActive && /* @__PURE__ */ jsx("button", {
|
|
664
|
+
onClick: handleDelete,
|
|
665
|
+
className: "text-red-500 hover:text-red-700 p-1",
|
|
666
|
+
type: "button",
|
|
667
|
+
"aria-label": "Delete API key",
|
|
668
|
+
children: /* @__PURE__ */ jsx(Trash2Icon, { className: "size-4" })
|
|
669
|
+
})]
|
|
670
|
+
})
|
|
671
|
+
]
|
|
672
|
+
})
|
|
673
|
+
})
|
|
674
|
+
})
|
|
675
|
+
});
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region src/pages/subscriptions/ApiKeysList.tsx
|
|
680
|
+
const ApiKeysList = ({ apiKeys, deploymentName, consumerId }) => {
|
|
681
|
+
const queryClient = useQueryClient();
|
|
682
|
+
const context = useZudoku();
|
|
683
|
+
const rollKeyMutation = useMutation({
|
|
684
|
+
mutationFn: createMutationFn(`/v2/client/${deploymentName}/consumers/${consumerId}/roll-key`, context, {
|
|
685
|
+
method: "POST",
|
|
686
|
+
body: JSON.stringify({})
|
|
687
|
+
}),
|
|
688
|
+
onSuccess: () => {
|
|
689
|
+
queryClient.invalidateQueries({ queryKey: [`/${deploymentName}/consumers/${consumerId}`] });
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
if (!apiKeys || apiKeys.length === 0) return null;
|
|
693
|
+
const sortedKeys = [...apiKeys].sort((a, b) => {
|
|
694
|
+
const aExpired = new Date(a.expiresAt) < /* @__PURE__ */ new Date();
|
|
695
|
+
if (aExpired !== new Date(b.expiresAt) < /* @__PURE__ */ new Date()) return aExpired ? 1 : -1;
|
|
696
|
+
return new Date(b.createdOn).getTime() - new Date(a.createdOn).getTime();
|
|
697
|
+
});
|
|
698
|
+
const activeKey = sortedKeys.find((k) => !k.expiresAt);
|
|
699
|
+
const expiringKeys = sortedKeys.filter((k) => !k.expiresAt);
|
|
700
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
701
|
+
className: "space-y-4",
|
|
702
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
703
|
+
className: "flex items-center justify-between",
|
|
704
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
705
|
+
className: "text-lg font-semibold",
|
|
706
|
+
children: "API Keys"
|
|
707
|
+
}), /* @__PURE__ */ jsxs(Button$1, {
|
|
708
|
+
onClick: () => rollKeyMutation.mutate(),
|
|
709
|
+
disabled: rollKeyMutation.isPending,
|
|
710
|
+
children: [/* @__PURE__ */ jsx(RefreshCwIcon, { className: `size-4 ${rollKeyMutation.isPending ? "animate-spin" : ""}` }), rollKeyMutation.isPending ? "Rolling..." : "Roll API Key"]
|
|
711
|
+
})]
|
|
712
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
713
|
+
className: "space-y-4",
|
|
714
|
+
children: [activeKey && /* @__PURE__ */ jsx(ApiKey, {
|
|
715
|
+
deploymentName,
|
|
716
|
+
consumerId,
|
|
717
|
+
apiKeyId: activeKey.id,
|
|
718
|
+
apiKey: activeKey.key,
|
|
719
|
+
createdAt: activeKey.createdOn,
|
|
720
|
+
lastUsed: activeKey.updatedOn,
|
|
721
|
+
expiresAt: activeKey.expiresAt,
|
|
722
|
+
isActive: true,
|
|
723
|
+
label: "Current Key"
|
|
724
|
+
}, activeKey.id), expiringKeys.map((apiKey) => /* @__PURE__ */ jsx(ApiKey, {
|
|
725
|
+
deploymentName,
|
|
726
|
+
consumerId,
|
|
727
|
+
apiKeyId: apiKey.id,
|
|
728
|
+
apiKey: apiKey.key,
|
|
729
|
+
createdAt: apiKey.createdOn,
|
|
730
|
+
lastUsed: apiKey.updatedOn,
|
|
731
|
+
expiresAt: apiKey.expiresAt,
|
|
732
|
+
isActive: false,
|
|
733
|
+
label: "Previous Key"
|
|
734
|
+
}, apiKey.id))]
|
|
735
|
+
})]
|
|
736
|
+
});
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region src/pages/subscriptions/SubscriptionsList.tsx
|
|
741
|
+
const SubscriptionsList = ({ subscriptions, activeSubscriptionId }) => {
|
|
742
|
+
return /* @__PURE__ */ jsx("div", {
|
|
743
|
+
className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3",
|
|
744
|
+
children: subscriptions.map((subscription) => {
|
|
745
|
+
const isActive = activeSubscriptionId === subscription.id;
|
|
746
|
+
return /* @__PURE__ */ jsx(Link, {
|
|
747
|
+
to: `/subscriptions/${subscription.id}`,
|
|
748
|
+
children: /* @__PURE__ */ jsx(Item, {
|
|
749
|
+
size: "sm",
|
|
750
|
+
variant: "outline",
|
|
751
|
+
className: cn(isActive && "border-primary bg-primary/5 shadow-md"),
|
|
752
|
+
children: /* @__PURE__ */ jsx(ItemContent, { children: /* @__PURE__ */ jsx(ItemTitle, { children: subscription.name }) })
|
|
753
|
+
}, subscription.id)
|
|
754
|
+
}, subscription.id);
|
|
755
|
+
})
|
|
756
|
+
});
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
//#endregion
|
|
760
|
+
//#region src/pages/subscriptions/Usage.tsx
|
|
761
|
+
const isMeteredEntitlement = (entitlement) => {
|
|
762
|
+
return "balance" in entitlement;
|
|
763
|
+
};
|
|
764
|
+
const Usage = ({ subscriptionId, environmentName }) => {
|
|
765
|
+
const zudoku = useZudoku();
|
|
766
|
+
const { data: plans } = usePlans(environmentName);
|
|
767
|
+
const { data: usage } = useSuspenseQuery({
|
|
768
|
+
queryKey: [`/v3/zudoku-metering/${environmentName}/subscriptions/${subscriptionId}/usage`],
|
|
769
|
+
meta: { context: zudoku }
|
|
770
|
+
});
|
|
771
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Heading, {
|
|
772
|
+
level: 3,
|
|
773
|
+
className: "mb-4",
|
|
774
|
+
children: "Usage"
|
|
775
|
+
}), /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, {
|
|
776
|
+
className: "p-6",
|
|
777
|
+
children: Object.entries(usage.entitlements).filter((entry) => isMeteredEntitlement(entry[1])).map(([key, metric], index) => /* @__PURE__ */ jsxs("div", { children: [index > 0 && /* @__PURE__ */ jsx("div", { className: "my-4 border-t" }), /* @__PURE__ */ jsxs("div", {
|
|
778
|
+
className: "space-y-2",
|
|
779
|
+
children: [
|
|
780
|
+
/* @__PURE__ */ jsxs("div", {
|
|
781
|
+
className: "flex items-center justify-between text-sm",
|
|
782
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
783
|
+
className: "flex flex-col gap-2",
|
|
784
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
785
|
+
className: "text-base font-medium capitalize",
|
|
786
|
+
children: key
|
|
787
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
788
|
+
className: "text-muted-foreground",
|
|
789
|
+
children: [metric.usage.toLocaleString(), " used"]
|
|
790
|
+
})]
|
|
791
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
792
|
+
className: "text-muted-foreground",
|
|
793
|
+
children: [metric.balance.toLocaleString(), " limit"]
|
|
794
|
+
})]
|
|
795
|
+
}),
|
|
796
|
+
/* @__PURE__ */ jsx(Progress, {
|
|
797
|
+
value: metric.usage / metric.balance * 100,
|
|
798
|
+
className: "h-2"
|
|
799
|
+
}),
|
|
800
|
+
/* @__PURE__ */ jsxs("p", {
|
|
801
|
+
className: "text-sm text-muted-foreground",
|
|
802
|
+
children: [metric.balance - metric.usage, " calls remaining this month"]
|
|
803
|
+
})
|
|
804
|
+
]
|
|
805
|
+
})] }, key))
|
|
806
|
+
}) })] });
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region src/pages/SubscriptionsPage.tsx
|
|
811
|
+
const SubscriptionsPage = ({ environmentName }) => {
|
|
812
|
+
const { data } = useSubscriptions(environmentName);
|
|
813
|
+
const { subscriptionId } = useParams();
|
|
814
|
+
const subscriptions = data?.items ?? [];
|
|
815
|
+
const activeSubscription = useMemo(() => {
|
|
816
|
+
if (subscriptions.length === 0) return null;
|
|
817
|
+
if (subscriptionId) return subscriptions.find((s) => s.id === subscriptionId) ?? subscriptions[0];
|
|
818
|
+
return subscriptions[0];
|
|
819
|
+
}, [subscriptions, subscriptionId]);
|
|
820
|
+
return /* @__PURE__ */ jsx("div", {
|
|
821
|
+
className: "w-full py-12",
|
|
822
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
823
|
+
className: "max-w-4xl space-y-8",
|
|
824
|
+
children: [
|
|
825
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
|
|
826
|
+
className: "text-2xl font-bold text-foreground mb-2",
|
|
827
|
+
children: "My Subscriptions"
|
|
828
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
829
|
+
className: "text-base text-muted-foreground",
|
|
830
|
+
children: "Manage your subscriptions, usage, and API keys"
|
|
831
|
+
})] }),
|
|
832
|
+
/* @__PURE__ */ jsx(SubscriptionsList, {
|
|
833
|
+
subscriptions,
|
|
834
|
+
activeSubscriptionId: activeSubscription?.id
|
|
835
|
+
}),
|
|
836
|
+
activeSubscription && /* @__PURE__ */ jsx(Usage, {
|
|
837
|
+
subscriptionId: activeSubscription?.id,
|
|
838
|
+
environmentName
|
|
839
|
+
}),
|
|
840
|
+
activeSubscription?.consumer?.apiKeys && /* @__PURE__ */ jsx(ApiKeysList, {
|
|
841
|
+
deploymentName: environmentName,
|
|
842
|
+
consumerId: activeSubscription.consumer.id,
|
|
843
|
+
apiKeys: activeSubscription.consumer.apiKeys
|
|
844
|
+
}),
|
|
845
|
+
subscriptions.length === 0 && /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, {
|
|
846
|
+
className: "p-12 text-center",
|
|
847
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
848
|
+
className: "text-muted-foreground",
|
|
849
|
+
children: "No active subscriptions found."
|
|
850
|
+
})
|
|
851
|
+
}) })
|
|
852
|
+
]
|
|
853
|
+
})
|
|
854
|
+
});
|
|
855
|
+
};
|
|
856
|
+
var SubscriptionsPage_default = SubscriptionsPage;
|
|
857
|
+
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/ZuploMonetizationPlugin.tsx
|
|
860
|
+
const PRICING_PATH = "/pricing";
|
|
861
|
+
const enableMonetization = (config, options) => {
|
|
862
|
+
return {
|
|
863
|
+
...config,
|
|
864
|
+
plugins: [...config.plugins ?? [], zuploMonetizationPlugin(options)],
|
|
865
|
+
slots: { "head-navigation-start": () => {
|
|
866
|
+
return /* @__PURE__ */ jsx(Link, {
|
|
867
|
+
to: PRICING_PATH,
|
|
868
|
+
children: "Pricing"
|
|
869
|
+
});
|
|
870
|
+
} }
|
|
871
|
+
};
|
|
872
|
+
};
|
|
873
|
+
const zuploMonetizationPlugin = (options) => {
|
|
874
|
+
return {
|
|
875
|
+
getIdentities: async (context) => {
|
|
876
|
+
return (await client.fetchQuery({
|
|
877
|
+
queryKey: [`/v3/zudoku-metering/${options.environmentName}/subscriptions`],
|
|
878
|
+
meta: { context }
|
|
879
|
+
})).items.flatMap((item) => item.consumer.apiKeys.map((apiKey) => ({
|
|
880
|
+
label: item.name,
|
|
881
|
+
id: apiKey.id,
|
|
882
|
+
authorizeRequest: async (request) => {
|
|
883
|
+
return new Request(request, { headers: { Authorization: `Bearer ${apiKey.key}` } });
|
|
884
|
+
},
|
|
885
|
+
authorizationFields: { headers: ["Authorization"] }
|
|
886
|
+
})));
|
|
887
|
+
},
|
|
888
|
+
getProfileMenuItems: () => [{
|
|
889
|
+
label: "My Subscriptions",
|
|
890
|
+
path: "/subscriptions",
|
|
891
|
+
icon: StarsIcon
|
|
892
|
+
}],
|
|
893
|
+
getRoutes: () => [{
|
|
894
|
+
Component: ZuploMonetizationWrapper_default,
|
|
895
|
+
handle: { layout: "none" },
|
|
896
|
+
children: [{
|
|
897
|
+
path: "/checkout",
|
|
898
|
+
element: /* @__PURE__ */ jsx(CheckoutPage_default, { environmentName: options.environmentName })
|
|
899
|
+
}, {
|
|
900
|
+
path: "/checkout-confirm",
|
|
901
|
+
element: /* @__PURE__ */ jsx(CheckoutConfimPage_default, { environmentName: options.environmentName })
|
|
902
|
+
}]
|
|
903
|
+
}, {
|
|
904
|
+
Component: ZuploMonetizationWrapper_default,
|
|
905
|
+
children: [
|
|
906
|
+
{
|
|
907
|
+
path: "/pricing",
|
|
908
|
+
element: /* @__PURE__ */ jsx(PricingPage_default, { environmentName: options.environmentName })
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
path: "/checkout-failed",
|
|
912
|
+
element: /* @__PURE__ */ jsx(CheckoutFailedPage_default, {})
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
path: "/subscriptions/:subscriptionId?",
|
|
916
|
+
element: /* @__PURE__ */ jsx(SubscriptionsPage_default, { environmentName: options.environmentName })
|
|
917
|
+
}
|
|
918
|
+
]
|
|
919
|
+
}],
|
|
920
|
+
getProtectedRoutes: () => {
|
|
921
|
+
return [
|
|
922
|
+
"/checkout",
|
|
923
|
+
"/checkout-success",
|
|
924
|
+
"/checkout-failed"
|
|
925
|
+
];
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
//#endregion
|
|
931
|
+
export { enableMonetization, zuploMonetizationPlugin };
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zuplo/zudoku-plugin-monetization",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.mjs",
|
|
6
|
+
"types": "./dist/index.d.mts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.mjs",
|
|
10
|
+
"types": "./dist/index.d.mts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"tinyduration": "3.4.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/react": "19.2.7",
|
|
21
|
+
"@types/react-dom": "19.2.3",
|
|
22
|
+
"react": "19.2.3",
|
|
23
|
+
"react-dom": "19.2.3",
|
|
24
|
+
"tsdown": "0.20.0-beta.3",
|
|
25
|
+
"zudoku": "0.67.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=19.2.0",
|
|
29
|
+
"react-dom": ">=19.2.0",
|
|
30
|
+
"zudoku": ">=0.67.0"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsdown",
|
|
34
|
+
"dev": "tsdown --watch"
|
|
35
|
+
}
|
|
36
|
+
}
|