@zuplo/zudoku-plugin-monetization 0.0.24 → 0.0.26
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 +8 -5
- package/dist/index.mjs +140 -91
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import * as zudoku from "zudoku";
|
|
2
|
+
import "react";
|
|
2
3
|
|
|
3
|
-
//#region src/
|
|
4
|
-
|
|
4
|
+
//#region src/MonetizationContext.d.ts
|
|
5
|
+
interface MonetizationConfig {
|
|
5
6
|
pricing?: {
|
|
6
7
|
subtitle?: string;
|
|
7
8
|
title?: string;
|
|
8
|
-
units?: Record<string, string>;
|
|
9
9
|
showYearlyPrice?: boolean;
|
|
10
|
+
units?: Record<string, string>;
|
|
10
11
|
};
|
|
11
|
-
}
|
|
12
|
-
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/ZuploMonetizationPlugin.d.ts
|
|
15
|
+
declare const zuploMonetizationPlugin: (options?: MonetizationConfig | undefined) => zudoku.ZudokuPlugin;
|
|
13
16
|
//#endregion
|
|
14
17
|
export { zuploMonetizationPlugin };
|
package/dist/index.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { cn, createPlugin, joinUrl } from "zudoku";
|
|
1
|
+
import { cn, createPlugin, joinUrl, throwIfProblemJson } from "zudoku";
|
|
2
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 "zudoku/icons";
|
|
3
3
|
import { Button, ClientOnly, Head, Heading, Link, Slot } from "zudoku/components";
|
|
4
4
|
import { useAuth, useZudoku } from "zudoku/hooks";
|
|
5
5
|
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient, useSuspenseQuery } from "zudoku/react-query";
|
|
6
|
-
import { Link as Link$1, Outlet, useLocation, useNavigate,
|
|
6
|
+
import { Link as Link$1, Outlet, useLocation, useNavigate, useSearchParams } from "zudoku/router";
|
|
7
7
|
import { Alert, AlertAction, AlertDescription, AlertTitle } from "zudoku/ui/Alert";
|
|
8
8
|
import { Card, CardContent, CardHeader, CardTitle } from "zudoku/ui/Card";
|
|
9
9
|
import { Separator } from "zudoku/ui/Separator";
|
|
10
10
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { createContext, use, useEffect, useMemo, useState } from "react";
|
|
11
12
|
import { parse } from "tinyduration";
|
|
12
13
|
import { Button as Button$1 } from "zudoku/ui/Button";
|
|
13
|
-
import { useEffect, useMemo, useState } from "react";
|
|
14
14
|
import { DismissibleAlert, DismissibleAlertAction } from "zudoku/ui/DismissibleAlert";
|
|
15
15
|
import { ActionButton } from "zudoku/ui/ActionButton";
|
|
16
16
|
import { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "zudoku/ui/Item";
|
|
@@ -90,6 +90,11 @@ const usePlans = () => {
|
|
|
90
90
|
});
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/MonetizationContext.tsx
|
|
95
|
+
const MonetizationContext = createContext({});
|
|
96
|
+
const useMonetizationConfig = () => use(MonetizationContext);
|
|
97
|
+
|
|
93
98
|
//#endregion
|
|
94
99
|
//#region src/utils/formatDuration.ts
|
|
95
100
|
const formatDuration = (iso) => {
|
|
@@ -124,6 +129,24 @@ const formatDurationInterval = (iso) => {
|
|
|
124
129
|
return iso;
|
|
125
130
|
}
|
|
126
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
|
+
};
|
|
127
150
|
|
|
128
151
|
//#endregion
|
|
129
152
|
//#region src/utils/formatPrice.ts
|
|
@@ -137,7 +160,8 @@ const formatPrice = (amount, currency) => new Intl.NumberFormat("en-US", {
|
|
|
137
160
|
|
|
138
161
|
//#endregion
|
|
139
162
|
//#region src/utils/categorizeRateCards.ts
|
|
140
|
-
const categorizeRateCards = (rateCards,
|
|
163
|
+
const categorizeRateCards = (rateCards, options) => {
|
|
164
|
+
const { currency, units, planBillingCadence } = options ?? {};
|
|
141
165
|
const quotas = [];
|
|
142
166
|
const features = [];
|
|
143
167
|
for (const rc of rateCards) {
|
|
@@ -157,7 +181,7 @@ const categorizeRateCards = (rateCards, currency, units) => {
|
|
|
157
181
|
key: rc.featureKey ?? rc.key,
|
|
158
182
|
name: rc.name,
|
|
159
183
|
limit: et.issueAfterReset,
|
|
160
|
-
period:
|
|
184
|
+
period: rc.billingCadence ? formatDuration(rc.billingCadence) : planBillingCadence ? formatDuration(planBillingCadence) : "month",
|
|
161
185
|
overagePrice
|
|
162
186
|
});
|
|
163
187
|
} else if (et.type === "boolean") features.push({
|
|
@@ -227,13 +251,8 @@ const queryClient = new QueryClient({ defaultOptions: {
|
|
|
227
251
|
}
|
|
228
252
|
});
|
|
229
253
|
const response = await fetch(q.meta?.context ? await q.meta.context.signRequest(request) : request);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const data = await response.json();
|
|
233
|
-
throw new Error(data.detail ?? data.title);
|
|
234
|
-
}
|
|
235
|
-
throw new Error("Failed to fetch request");
|
|
236
|
-
}
|
|
254
|
+
await throwIfProblemJson(response);
|
|
255
|
+
if (!response.ok) throw new Error("Failed to fetch request");
|
|
237
256
|
return response.json();
|
|
238
257
|
}
|
|
239
258
|
},
|
|
@@ -262,11 +281,8 @@ const queryClient = new QueryClient({ defaultOptions: {
|
|
|
262
281
|
}
|
|
263
282
|
});
|
|
264
283
|
const response = await fetch(m.meta?.context ? await m.meta.context.signRequest(request) : request);
|
|
284
|
+
await throwIfProblemJson(response);
|
|
265
285
|
if (!response.ok) {
|
|
266
|
-
if (response.headers.get("content-type")?.includes("application/problem+json")) {
|
|
267
|
-
const data = await response.json();
|
|
268
|
-
throw new Error(data.detail ?? data.title);
|
|
269
|
-
}
|
|
270
286
|
const errorText = await response.text();
|
|
271
287
|
throw new Error(`Request failed: ${response.status} ${errorText}`);
|
|
272
288
|
}
|
|
@@ -275,26 +291,32 @@ const queryClient = new QueryClient({ defaultOptions: {
|
|
|
275
291
|
}
|
|
276
292
|
}
|
|
277
293
|
} });
|
|
278
|
-
const ZuploMonetizationWrapper = () => {
|
|
279
|
-
|
|
280
|
-
|
|
294
|
+
const ZuploMonetizationWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(QueryClientProvider, {
|
|
295
|
+
client: queryClient,
|
|
296
|
+
children: /* @__PURE__ */ jsx(MonetizationContext, {
|
|
297
|
+
value: options,
|
|
281
298
|
children: /* @__PURE__ */ jsx(ClientOnly, { children: /* @__PURE__ */ jsx(Outlet, {}) })
|
|
282
|
-
})
|
|
283
|
-
};
|
|
299
|
+
})
|
|
300
|
+
});
|
|
284
301
|
|
|
285
302
|
//#endregion
|
|
286
303
|
//#region src/pages/CheckoutConfirmPage.tsx
|
|
287
304
|
const CheckoutConfirmPage = () => {
|
|
288
305
|
const [search] = useSearchParams();
|
|
289
|
-
const planId = search.get("
|
|
306
|
+
const planId = search.get("planId");
|
|
290
307
|
const zudoku = useZudoku();
|
|
291
308
|
const deploymentName = useDeploymentName();
|
|
292
309
|
const navigate = useNavigate();
|
|
293
310
|
const { data: plans } = usePlans();
|
|
311
|
+
const { pricing } = useMonetizationConfig();
|
|
294
312
|
const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
|
|
295
313
|
if (!planId) throw new Error("Parameter `planId` missing");
|
|
296
314
|
const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
|
|
297
|
-
const { quotas, features } = categorizeRateCards(rateCards ?? [],
|
|
315
|
+
const { quotas, features } = categorizeRateCards(rateCards ?? [], {
|
|
316
|
+
currency: selectedPlan?.currency,
|
|
317
|
+
units: pricing?.units,
|
|
318
|
+
planBillingCadence: selectedPlan?.billingCadence
|
|
319
|
+
});
|
|
298
320
|
const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
|
|
299
321
|
const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
|
|
300
322
|
const createSubscriptionMutation = useMutation({
|
|
@@ -308,7 +330,7 @@ const CheckoutConfirmPage = () => {
|
|
|
308
330
|
},
|
|
309
331
|
onSuccess: async (subscription) => {
|
|
310
332
|
await queryClient.invalidateQueries();
|
|
311
|
-
navigate(`/subscriptions
|
|
333
|
+
navigate(`/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`);
|
|
312
334
|
}
|
|
313
335
|
});
|
|
314
336
|
return /* @__PURE__ */ jsx("div", {
|
|
@@ -484,23 +506,22 @@ const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
|
|
|
484
506
|
//#region src/hooks/useUrlUtils.ts
|
|
485
507
|
const useUrlUtils = () => {
|
|
486
508
|
const basePath = useZudoku().options.basePath;
|
|
487
|
-
return { generateUrl: (path) => {
|
|
509
|
+
return { generateUrl: (path, { searchParams } = {}) => {
|
|
488
510
|
if (!window.location.origin) throw new Error("Only works in browser environment");
|
|
489
|
-
return joinUrl(window.location.origin, basePath, path);
|
|
511
|
+
return joinUrl(window.location.origin, basePath, path, searchParams ? `?${new URLSearchParams(searchParams)}` : void 0);
|
|
490
512
|
} };
|
|
491
513
|
};
|
|
492
514
|
|
|
493
515
|
//#endregion
|
|
494
516
|
//#region src/pages/CheckoutPage.tsx
|
|
495
517
|
const CheckoutPage = () => {
|
|
496
|
-
const
|
|
518
|
+
const [searchParams] = useSearchParams();
|
|
519
|
+
const planId = searchParams.get("planId");
|
|
497
520
|
const zudoku = useZudoku();
|
|
498
521
|
const auth = useAuth();
|
|
499
522
|
const { generateUrl } = useUrlUtils();
|
|
500
523
|
const deploymentName = useDeploymentName();
|
|
501
524
|
if (!planId) throw new Error(`missing planId in URL`);
|
|
502
|
-
const successUrl = new URL(generateUrl("/checkout-confirm"));
|
|
503
|
-
successUrl.searchParams.set("plan", planId);
|
|
504
525
|
const checkoutLink = useQuery({
|
|
505
526
|
queryKey: [
|
|
506
527
|
`/v3/zudoku-metering/${deploymentName}/stripe/checkout`,
|
|
@@ -513,7 +534,7 @@ const CheckoutPage = () => {
|
|
|
513
534
|
method: "POST",
|
|
514
535
|
body: JSON.stringify({
|
|
515
536
|
planId,
|
|
516
|
-
successURL:
|
|
537
|
+
successURL: generateUrl("/checkout-confirm", { searchParams: { planId } }),
|
|
517
538
|
cancelURL: generateUrl("/pricing")
|
|
518
539
|
})
|
|
519
540
|
}
|
|
@@ -587,11 +608,14 @@ const ManagePaymentPage = () => {
|
|
|
587
608
|
|
|
588
609
|
//#endregion
|
|
589
610
|
//#region src/pages/pricing/PricingCard.tsx
|
|
590
|
-
const PhaseSection = ({ phase, currency, showName,
|
|
591
|
-
const {
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
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;
|
|
595
619
|
return /* @__PURE__ */ jsxs("div", {
|
|
596
620
|
className: "space-y-2",
|
|
597
621
|
children: [
|
|
@@ -606,17 +630,19 @@ const PhaseSection = ({ phase, currency, showName, excludeKeys, units }) => {
|
|
|
606
630
|
]
|
|
607
631
|
})]
|
|
608
632
|
}),
|
|
609
|
-
|
|
610
|
-
|
|
633
|
+
quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, { quota }, quota.key)),
|
|
634
|
+
features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, { feature }, feature.key))
|
|
611
635
|
]
|
|
612
636
|
});
|
|
613
637
|
};
|
|
614
|
-
const PricingCard = ({ plan, isPopular = false, isSubscribed = false
|
|
638
|
+
const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
|
|
639
|
+
const { pricing } = useMonetizationConfig();
|
|
615
640
|
if (plan.phases.length === 0) return null;
|
|
616
641
|
const price = getPriceFromPlan(plan);
|
|
617
642
|
const isFree = price.monthly === 0;
|
|
618
643
|
const isCustom = plan.metadata?.isCustom === true;
|
|
619
644
|
const hasMultiplePhases = plan.phases.length > 1;
|
|
645
|
+
const billingInterval = formatDuration(plan.billingCadence);
|
|
620
646
|
return /* @__PURE__ */ jsxs("div", {
|
|
621
647
|
className: cn("relative rounded-lg border p-6 flex flex-col", isPopular && "border-primary border-2"),
|
|
622
648
|
children: [
|
|
@@ -645,10 +671,10 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false, showYearly
|
|
|
645
671
|
})] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
646
672
|
className: "text-3xl font-bold text-card-foreground",
|
|
647
673
|
children: isFree ? "Free" : formatPrice(price.monthly, plan.currency)
|
|
648
|
-
}), !isFree && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */
|
|
674
|
+
}), !isFree && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
649
675
|
className: "text-muted-foreground text-sm",
|
|
650
|
-
children: "/
|
|
651
|
-
}), showYearlyPrice && /* @__PURE__ */ jsxs("div", {
|
|
676
|
+
children: ["/", billingInterval]
|
|
677
|
+
}), pricing?.showYearlyPrice !== false && price.yearly > 0 && /* @__PURE__ */ jsxs("div", {
|
|
652
678
|
className: "w-full text-sm text-muted-foreground mt-1",
|
|
653
679
|
children: [formatPrice(price.yearly, plan.currency), "/year"]
|
|
654
680
|
})] })] })
|
|
@@ -661,16 +687,12 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false, showYearly
|
|
|
661
687
|
}),
|
|
662
688
|
/* @__PURE__ */ jsx("div", {
|
|
663
689
|
className: "space-y-4 mb-6 grow",
|
|
664
|
-
children: plan.phases.map((phase
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
excludeKeys: laterKeys,
|
|
671
|
-
units
|
|
672
|
-
}, phase.key);
|
|
673
|
-
})
|
|
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))
|
|
674
696
|
}),
|
|
675
697
|
isSubscribed ? /* @__PURE__ */ jsx(Button, {
|
|
676
698
|
variant: isPopular ? "default" : "secondary",
|
|
@@ -683,7 +705,7 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false, showYearly
|
|
|
683
705
|
variant: isPopular ? "default" : "secondary",
|
|
684
706
|
asChild: true,
|
|
685
707
|
children: /* @__PURE__ */ jsx(Link$1, {
|
|
686
|
-
to: `/checkout
|
|
708
|
+
to: `/checkout?planId=${encodeURIComponent(plan.id)}`,
|
|
687
709
|
children: "Subscribe"
|
|
688
710
|
})
|
|
689
711
|
})
|
|
@@ -693,7 +715,8 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false, showYearly
|
|
|
693
715
|
|
|
694
716
|
//#endregion
|
|
695
717
|
//#region src/pages/PricingPage.tsx
|
|
696
|
-
const PricingPage = (
|
|
718
|
+
const PricingPage = () => {
|
|
719
|
+
const { pricing } = useMonetizationConfig();
|
|
697
720
|
const zudoku = useZudoku();
|
|
698
721
|
const deploymentName = useDeploymentName();
|
|
699
722
|
const auth = useAuth();
|
|
@@ -706,28 +729,26 @@ const PricingPage = ({ subtitle = "See our pricing options and choose the one th
|
|
|
706
729
|
return /* @__PURE__ */ jsxs("div", {
|
|
707
730
|
className: "w-full px-4 pt-(--padding-content-top) pb-(--padding-content-bottom)",
|
|
708
731
|
children: [
|
|
709
|
-
/* @__PURE__ */ jsxs(Head, { children: [/* @__PURE__ */ jsx("title", { children: title }), /* @__PURE__ */ jsx("meta", {
|
|
732
|
+
/* @__PURE__ */ jsxs(Head, { children: [/* @__PURE__ */ jsx("title", { children: pricing?.title ?? "Pricing" }), /* @__PURE__ */ jsx("meta", {
|
|
710
733
|
name: "description",
|
|
711
|
-
content: subtitle
|
|
734
|
+
content: pricing?.subtitle ?? "See our pricing options and choose the one that best suits your needs."
|
|
712
735
|
})] }),
|
|
713
736
|
/* @__PURE__ */ jsxs("div", {
|
|
714
737
|
className: "text-center space-y-4 mb-12",
|
|
715
738
|
children: [/* @__PURE__ */ jsx(Heading, {
|
|
716
739
|
level: 1,
|
|
717
740
|
"data-testid": "title",
|
|
718
|
-
children: title
|
|
741
|
+
children: pricing?.title ?? "Pricing"
|
|
719
742
|
}), /* @__PURE__ */ jsx("p", {
|
|
720
743
|
className: "text-muted-foreground",
|
|
721
744
|
"data-testid": "subtitle",
|
|
722
|
-
children: subtitle
|
|
745
|
+
children: pricing?.subtitle ?? "See our pricing options and choose the one that best suits your needs."
|
|
723
746
|
})]
|
|
724
747
|
}),
|
|
725
748
|
/* @__PURE__ */ jsx("div", {
|
|
726
749
|
className: "w-full grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(300px,max-content))] justify-center gap-6",
|
|
727
750
|
children: pricingTable.items.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
|
|
728
751
|
plan,
|
|
729
|
-
units,
|
|
730
|
-
showYearlyPrice,
|
|
731
752
|
isPopular: plan.metadata?.zuplo_most_popular === "true",
|
|
732
753
|
isSubscribed: subscriptions.items.some((subscription) => ["active", "canceled"].includes(subscription.status))
|
|
733
754
|
}, plan.id))
|
|
@@ -747,11 +768,16 @@ const SubscriptionChangeConfirmPage = () => {
|
|
|
747
768
|
const deploymentName = useDeploymentName();
|
|
748
769
|
const navigate = useNavigate();
|
|
749
770
|
const { data: plans } = usePlans();
|
|
771
|
+
const { pricing } = useMonetizationConfig();
|
|
750
772
|
const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
|
|
751
773
|
if (!planId) throw new Error("Parameter `planId` missing");
|
|
752
774
|
if (!subscriptionId) throw new Error("Parameter `subscriptionId` missing");
|
|
753
775
|
const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
|
|
754
|
-
const { quotas, features } = categorizeRateCards(rateCards ?? [],
|
|
776
|
+
const { quotas, features } = categorizeRateCards(rateCards ?? [], {
|
|
777
|
+
currency: selectedPlan?.currency,
|
|
778
|
+
units: pricing?.units,
|
|
779
|
+
planBillingCadence: selectedPlan?.billingCadence
|
|
780
|
+
});
|
|
755
781
|
const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
|
|
756
782
|
const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
|
|
757
783
|
const changeMutation = useMutation({
|
|
@@ -765,7 +791,7 @@ const SubscriptionChangeConfirmPage = () => {
|
|
|
765
791
|
},
|
|
766
792
|
onSuccess: async (subscription) => {
|
|
767
793
|
await queryClient.invalidateQueries();
|
|
768
|
-
navigate(`/subscriptions
|
|
794
|
+
navigate(`/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`, { state: { planSwitched: { newPlanName: selectedPlan?.name } } });
|
|
769
795
|
}
|
|
770
796
|
});
|
|
771
797
|
return /* @__PURE__ */ jsx("div", {
|
|
@@ -863,7 +889,7 @@ const SubscriptionChangeConfirmPage = () => {
|
|
|
863
889
|
disabled: changeMutation.isPending,
|
|
864
890
|
asChild: !changeMutation.isPending,
|
|
865
891
|
children: /* @__PURE__ */ jsx(Link$1, {
|
|
866
|
-
to: `/subscriptions
|
|
892
|
+
to: `/subscriptions?${new URLSearchParams({ subscriptionId: subscriptionId ?? "" })}`,
|
|
867
893
|
children: "Cancel"
|
|
868
894
|
})
|
|
869
895
|
})]
|
|
@@ -1288,15 +1314,23 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
|
|
|
1288
1314
|
|
|
1289
1315
|
//#endregion
|
|
1290
1316
|
//#region src/pages/subscriptions/SwitchPlanModal.tsx
|
|
1291
|
-
const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex) => {
|
|
1317
|
+
const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units) => {
|
|
1292
1318
|
const isUpgrade = targetIndex > currentIndex;
|
|
1293
1319
|
const currentPhase = currentPlan?.phases.at(-1);
|
|
1294
1320
|
const targetPhase = targetPlan.phases.at(-1);
|
|
1295
|
-
const { quotas: currentQuotas, features: currentFeatures } = currentPhase ? categorizeRateCards(currentPhase.rateCards,
|
|
1321
|
+
const { quotas: currentQuotas, features: currentFeatures } = currentPhase ? categorizeRateCards(currentPhase.rateCards, {
|
|
1322
|
+
currency: currentPlan?.currency,
|
|
1323
|
+
units,
|
|
1324
|
+
planBillingCadence: currentPlan?.billingCadence
|
|
1325
|
+
}) : {
|
|
1296
1326
|
quotas: [],
|
|
1297
1327
|
features: []
|
|
1298
1328
|
};
|
|
1299
|
-
const { quotas: targetQuotas, features: targetFeatures } = targetPhase ? categorizeRateCards(targetPhase.rateCards,
|
|
1329
|
+
const { quotas: targetQuotas, features: targetFeatures } = targetPhase ? categorizeRateCards(targetPhase.rateCards, {
|
|
1330
|
+
currency: targetPlan.currency,
|
|
1331
|
+
units,
|
|
1332
|
+
planBillingCadence: targetPlan.billingCadence
|
|
1333
|
+
}) : {
|
|
1300
1334
|
quotas: [],
|
|
1301
1335
|
features: []
|
|
1302
1336
|
};
|
|
@@ -1403,7 +1437,11 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
|
|
|
1403
1437
|
children: "Free"
|
|
1404
1438
|
}) : /* @__PURE__ */ jsxs("span", {
|
|
1405
1439
|
className: "text-primary font-medium text-lg",
|
|
1406
|
-
children: [
|
|
1440
|
+
children: [
|
|
1441
|
+
formatPrice(displayPrice, comparison.plan.currency),
|
|
1442
|
+
"/",
|
|
1443
|
+
formatDuration(comparison.plan.billingCadence)
|
|
1444
|
+
]
|
|
1407
1445
|
})]
|
|
1408
1446
|
}), isCustom ? /* @__PURE__ */ jsx(Button$1, {
|
|
1409
1447
|
variant: "default",
|
|
@@ -1515,9 +1553,6 @@ const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
|
|
|
1515
1553
|
const deploymentName = useDeploymentName();
|
|
1516
1554
|
const context = useZudoku();
|
|
1517
1555
|
const { generateUrl } = useUrlUtils();
|
|
1518
|
-
const successUrl = new URL(generateUrl("/subscription-change-confirm"));
|
|
1519
|
-
successUrl.searchParams.set("planId", switchTo.plan.id);
|
|
1520
|
-
successUrl.searchParams.set("subscriptionId", switchTo.subscriptionId);
|
|
1521
1556
|
const mutation = useMutation({
|
|
1522
1557
|
mutationKey: [`/v3/zudoku-metering/${deploymentName}/stripe/checkout`],
|
|
1523
1558
|
meta: {
|
|
@@ -1526,8 +1561,11 @@ const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
|
|
|
1526
1561
|
method: "POST",
|
|
1527
1562
|
body: JSON.stringify({
|
|
1528
1563
|
planId: switchTo.plan.id,
|
|
1529
|
-
successURL:
|
|
1530
|
-
|
|
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 } })
|
|
1531
1569
|
})
|
|
1532
1570
|
}
|
|
1533
1571
|
},
|
|
@@ -1567,6 +1605,7 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1567
1605
|
const [open, setOpen] = useState(false);
|
|
1568
1606
|
const { data: plansData } = usePlans();
|
|
1569
1607
|
const [switchTo, setSwitchTo] = useState(null);
|
|
1608
|
+
const { pricing } = useMonetizationConfig();
|
|
1570
1609
|
const currentPlan = plansData?.items.find((p) => p.id === subscription.plan.id);
|
|
1571
1610
|
const { upgrades, downgrades, privatePlans } = useMemo(() => {
|
|
1572
1611
|
if (!plansData?.items || !currentPlan) return {
|
|
@@ -1577,14 +1616,18 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1577
1616
|
const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
|
|
1578
1617
|
const currentIndex = plansData.items.findIndex((p) => p.id === currentPlan.id);
|
|
1579
1618
|
const allComparisons = plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => {
|
|
1580
|
-
return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan));
|
|
1619
|
+
return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan), pricing?.units);
|
|
1581
1620
|
});
|
|
1582
1621
|
return {
|
|
1583
1622
|
upgrades: allComparisons.filter((c) => c.isUpgrade && !isPrivatePlan(c.plan)),
|
|
1584
1623
|
downgrades: allComparisons.filter((c) => !c.isUpgrade && !isPrivatePlan(c.plan)),
|
|
1585
1624
|
privatePlans: allComparisons.filter((c) => isPrivatePlan(c.plan))
|
|
1586
1625
|
};
|
|
1587
|
-
}, [
|
|
1626
|
+
}, [
|
|
1627
|
+
plansData?.items,
|
|
1628
|
+
currentPlan,
|
|
1629
|
+
pricing?.units
|
|
1630
|
+
]);
|
|
1588
1631
|
return /* @__PURE__ */ jsxs(Fragment, { children: [switchTo !== null && /* @__PURE__ */ jsx(ConfirmSwitchAlert, {
|
|
1589
1632
|
switchTo,
|
|
1590
1633
|
onRequestClose: () => setSwitchTo(null)
|
|
@@ -1772,6 +1815,8 @@ const isMeteredEntitlement = (entitlement) => {
|
|
|
1772
1815
|
return "balance" in entitlement;
|
|
1773
1816
|
};
|
|
1774
1817
|
const UsageItem = ({ meter, item, subscription }) => {
|
|
1818
|
+
const cadence = item?.billingCadence ?? subscription?.billingCadence;
|
|
1819
|
+
const billingPeriod = cadence ? formatDurationAdjective(cadence) : "monthly";
|
|
1775
1820
|
const isSoftLimit = item?.included?.entitlement?.isSoftLimit ?? true;
|
|
1776
1821
|
const rate = (item?.price?.tiers?.find((t) => !t.upToAmount) ?? item?.price?.tiers?.at(-1))?.unitPrice?.amount;
|
|
1777
1822
|
const hasOverage = meter.overage > 0;
|
|
@@ -1786,7 +1831,11 @@ const UsageItem = ({ meter, item, subscription }) => {
|
|
|
1786
1831
|
className: "mb-4",
|
|
1787
1832
|
children: [
|
|
1788
1833
|
/* @__PURE__ */ jsx(AlertTriangleIcon, { className: "size-4 text-red-600 shrink-0" }),
|
|
1789
|
-
/* @__PURE__ */
|
|
1834
|
+
/* @__PURE__ */ jsxs(AlertTitle, { children: [
|
|
1835
|
+
"You've exceeded your ",
|
|
1836
|
+
billingPeriod,
|
|
1837
|
+
" quota"
|
|
1838
|
+
] }),
|
|
1790
1839
|
/* @__PURE__ */ jsxs(AlertDescription, { children: [
|
|
1791
1840
|
"Additional usage is being charged at the overage rate",
|
|
1792
1841
|
rate ? ` ($${Number(rate).toFixed(2)}/call)` : "",
|
|
@@ -1807,7 +1856,11 @@ const UsageItem = ({ meter, item, subscription }) => {
|
|
|
1807
1856
|
className: "mb-4",
|
|
1808
1857
|
children: [
|
|
1809
1858
|
/* @__PURE__ */ jsx(AlertTriangleIcon, { className: "size-4 text-red-600 shrink-0" }),
|
|
1810
|
-
/* @__PURE__ */
|
|
1859
|
+
/* @__PURE__ */ jsxs(AlertTitle, { children: [
|
|
1860
|
+
"You've reached your ",
|
|
1861
|
+
billingPeriod,
|
|
1862
|
+
" limit"
|
|
1863
|
+
] }),
|
|
1811
1864
|
/* @__PURE__ */ jsx(AlertDescription, { children: "Requests beyond your quota are blocked. Upgrade to a higher plan for more usage." }),
|
|
1812
1865
|
subscription && /* @__PURE__ */ jsx(AlertAction, { children: /* @__PURE__ */ jsx(SwitchPlanModal, {
|
|
1813
1866
|
subscription,
|
|
@@ -1857,7 +1910,7 @@ const UsageItem = ({ meter, item, subscription }) => {
|
|
|
1857
1910
|
}),
|
|
1858
1911
|
/* @__PURE__ */ jsxs("p", {
|
|
1859
1912
|
className: "text-xs text-muted-foreground",
|
|
1860
|
-
children: [meter.balance.toLocaleString(), " remaining this
|
|
1913
|
+
children: [meter.balance.toLocaleString(), " remaining this billing period"]
|
|
1861
1914
|
})
|
|
1862
1915
|
]
|
|
1863
1916
|
})]
|
|
@@ -2004,7 +2057,7 @@ const SubscriptionsList = ({ subscriptions, activeSubscriptionId }) => {
|
|
|
2004
2057
|
const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
|
|
2005
2058
|
const willExpire = subscription.activeTo && new Date(subscription.activeTo) > /* @__PURE__ */ new Date();
|
|
2006
2059
|
return /* @__PURE__ */ jsx(Link$1, {
|
|
2007
|
-
to: `/subscriptions
|
|
2060
|
+
to: `/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`,
|
|
2008
2061
|
children: /* @__PURE__ */ jsx(Item, {
|
|
2009
2062
|
size: "sm",
|
|
2010
2063
|
variant: "outline",
|
|
@@ -2029,7 +2082,7 @@ const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
|
|
|
2029
2082
|
]
|
|
2030
2083
|
})] })
|
|
2031
2084
|
}, subscription.id)
|
|
2032
|
-
}
|
|
2085
|
+
});
|
|
2033
2086
|
};
|
|
2034
2087
|
|
|
2035
2088
|
//#endregion
|
|
@@ -2037,7 +2090,8 @@ const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
|
|
|
2037
2090
|
const SubscriptionsPage = () => {
|
|
2038
2091
|
const deploymentName = useDeploymentName();
|
|
2039
2092
|
const { data } = useSubscriptions(deploymentName);
|
|
2040
|
-
const
|
|
2093
|
+
const [searchParams] = useSearchParams();
|
|
2094
|
+
const subscriptionId = searchParams.get("subscriptionId");
|
|
2041
2095
|
const subscriptions = data?.items ?? [];
|
|
2042
2096
|
const activeSubscription = useMemo(() => {
|
|
2043
2097
|
if (subscriptions.length === 0) return null;
|
|
@@ -2079,7 +2133,7 @@ const SubscriptionsPage = () => {
|
|
|
2079
2133
|
//#endregion
|
|
2080
2134
|
//#region src/ZuploMonetizationPlugin.tsx
|
|
2081
2135
|
const PRICING_PATH = "/pricing";
|
|
2082
|
-
const zuploMonetizationPlugin = createPlugin((options) => ({
|
|
2136
|
+
const zuploMonetizationPlugin = createPlugin((options = {}) => ({
|
|
2083
2137
|
transformConfig: ({ config, merge }) => merge({
|
|
2084
2138
|
apiKeys: { enabled: false },
|
|
2085
2139
|
header: { navigation: [...config.header?.navigation ?? [], {
|
|
@@ -2115,11 +2169,11 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
|
|
|
2115
2169
|
}],
|
|
2116
2170
|
getRoutes: () => {
|
|
2117
2171
|
return [{
|
|
2118
|
-
|
|
2172
|
+
element: /* @__PURE__ */ jsx(ZuploMonetizationWrapper, { options }),
|
|
2119
2173
|
handle: { layout: "none" },
|
|
2120
2174
|
children: [
|
|
2121
2175
|
{
|
|
2122
|
-
path: "/checkout
|
|
2176
|
+
path: "/checkout",
|
|
2123
2177
|
element: /* @__PURE__ */ jsx(CheckoutPage, {})
|
|
2124
2178
|
},
|
|
2125
2179
|
{
|
|
@@ -2137,16 +2191,11 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
|
|
|
2137
2191
|
{
|
|
2138
2192
|
path: PRICING_PATH,
|
|
2139
2193
|
handle: { layout: "default" },
|
|
2140
|
-
element: /* @__PURE__ */ jsx(PricingPage, {
|
|
2141
|
-
subtitle: options?.pricing?.subtitle,
|
|
2142
|
-
title: options?.pricing?.title,
|
|
2143
|
-
units: options?.pricing?.units,
|
|
2144
|
-
showYearlyPrice: options?.pricing?.showYearlyPrice
|
|
2145
|
-
})
|
|
2194
|
+
element: /* @__PURE__ */ jsx(PricingPage, {})
|
|
2146
2195
|
},
|
|
2147
2196
|
{
|
|
2148
2197
|
handle: { layout: "default" },
|
|
2149
|
-
path: "/subscriptions
|
|
2198
|
+
path: "/subscriptions",
|
|
2150
2199
|
element: /* @__PURE__ */ jsx(SubscriptionsPage, {})
|
|
2151
2200
|
}
|
|
2152
2201
|
]
|
|
@@ -2154,10 +2203,10 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
|
|
|
2154
2203
|
},
|
|
2155
2204
|
getProtectedRoutes: () => {
|
|
2156
2205
|
return [
|
|
2157
|
-
"/checkout
|
|
2206
|
+
"/checkout",
|
|
2158
2207
|
"/checkout-confirm",
|
|
2159
2208
|
"/subscription-change-confirm",
|
|
2160
|
-
"/subscriptions
|
|
2209
|
+
"/subscriptions",
|
|
2161
2210
|
"/manage-payment"
|
|
2162
2211
|
];
|
|
2163
2212
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zuplo/zudoku-plugin-monetization",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/zuplo/zudoku",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"react": "19.2.4",
|
|
32
32
|
"react-dom": "19.2.4",
|
|
33
33
|
"tsdown": "0.20.3",
|
|
34
|
-
"zudoku": "0.71.
|
|
34
|
+
"zudoku": "0.71.10"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": ">=19.2.0",
|