@zuplo/zudoku-plugin-monetization 0.0.22 → 0.0.24
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 +1 -0
- package/dist/index.mjs +189 -40
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -6,6 +6,7 @@ type ZudokuMonetizationPluginOptions = {
|
|
|
6
6
|
subtitle?: string;
|
|
7
7
|
title?: string;
|
|
8
8
|
units?: Record<string, string>;
|
|
9
|
+
showYearlyPrice?: boolean;
|
|
9
10
|
};
|
|
10
11
|
};
|
|
11
12
|
declare const zuploMonetizationPlugin: (options?: ZudokuMonetizationPluginOptions | undefined) => zudoku.ZudokuPlugin;
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cn, createPlugin, joinUrl } 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
|
-
import { Button, ClientOnly, Head, Heading, Link } from "zudoku/components";
|
|
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
6
|
import { Link as Link$1, Outlet, useLocation, useNavigate, useParams, useSearchParams } from "zudoku/router";
|
|
@@ -184,6 +184,16 @@ const categorizeRateCards = (rateCards, currency, units) => {
|
|
|
184
184
|
};
|
|
185
185
|
};
|
|
186
186
|
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/utils/formatBillingCycle.ts
|
|
189
|
+
const formatBillingCycle = (duration) => {
|
|
190
|
+
if (duration === "month") return "monthly";
|
|
191
|
+
if (duration === "year") return "annually";
|
|
192
|
+
if (duration === "week") return "weekly";
|
|
193
|
+
if (duration === "day") return "daily";
|
|
194
|
+
return `every ${duration}`;
|
|
195
|
+
};
|
|
196
|
+
|
|
187
197
|
//#endregion
|
|
188
198
|
//#region src/utils/getPriceFromPlan.ts
|
|
189
199
|
const getPriceFromPlan = (plan) => {
|
|
@@ -217,7 +227,13 @@ const queryClient = new QueryClient({ defaultOptions: {
|
|
|
217
227
|
}
|
|
218
228
|
});
|
|
219
229
|
const response = await fetch(q.meta?.context ? await q.meta.context.signRequest(request) : request);
|
|
220
|
-
if (!response.ok)
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
if (response.headers.get("content-type")?.includes("application/problem+json")) {
|
|
232
|
+
const data = await response.json();
|
|
233
|
+
throw new Error(data.detail ?? data.title);
|
|
234
|
+
}
|
|
235
|
+
throw new Error("Failed to fetch request");
|
|
236
|
+
}
|
|
221
237
|
return response.json();
|
|
222
238
|
}
|
|
223
239
|
},
|
|
@@ -268,14 +284,6 @@ const ZuploMonetizationWrapper = () => {
|
|
|
268
284
|
|
|
269
285
|
//#endregion
|
|
270
286
|
//#region src/pages/CheckoutConfirmPage.tsx
|
|
271
|
-
const formatBillingCycle = (duration) => {
|
|
272
|
-
if (duration === "month") return "monthly";
|
|
273
|
-
if (duration === "year") return "annually";
|
|
274
|
-
if (duration === "week") return "weekly";
|
|
275
|
-
if (duration === "day") return "daily";
|
|
276
|
-
if (duration.includes(" ")) return `every ${duration}`;
|
|
277
|
-
return `every ${duration}`;
|
|
278
|
-
};
|
|
279
287
|
const CheckoutConfirmPage = () => {
|
|
280
288
|
const [search] = useSearchParams();
|
|
281
289
|
const planId = search.get("plan");
|
|
@@ -603,7 +611,7 @@ const PhaseSection = ({ phase, currency, showName, excludeKeys, units }) => {
|
|
|
603
611
|
]
|
|
604
612
|
});
|
|
605
613
|
};
|
|
606
|
-
const PricingCard = ({ plan, isPopular = false, isSubscribed = false, units }) => {
|
|
614
|
+
const PricingCard = ({ plan, isPopular = false, isSubscribed = false, showYearlyPrice = true, units }) => {
|
|
607
615
|
if (plan.phases.length === 0) return null;
|
|
608
616
|
const price = getPriceFromPlan(plan);
|
|
609
617
|
const isFree = price.monthly === 0;
|
|
@@ -640,7 +648,7 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false, units }) =
|
|
|
640
648
|
}), !isFree && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
641
649
|
className: "text-muted-foreground text-sm",
|
|
642
650
|
children: "/mo"
|
|
643
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
651
|
+
}), showYearlyPrice && /* @__PURE__ */ jsxs("div", {
|
|
644
652
|
className: "w-full text-sm text-muted-foreground mt-1",
|
|
645
653
|
children: [formatPrice(price.yearly, plan.currency), "/year"]
|
|
646
654
|
})] })] })
|
|
@@ -685,7 +693,7 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false, units }) =
|
|
|
685
693
|
|
|
686
694
|
//#endregion
|
|
687
695
|
//#region src/pages/PricingPage.tsx
|
|
688
|
-
const PricingPage = ({ subtitle = "See our pricing options and choose the one that best suits your needs.", title = "Pricing", units }) => {
|
|
696
|
+
const PricingPage = ({ subtitle = "See our pricing options and choose the one that best suits your needs.", title = "Pricing", units, showYearlyPrice = true }) => {
|
|
689
697
|
const zudoku = useZudoku();
|
|
690
698
|
const deploymentName = useDeploymentName();
|
|
691
699
|
const auth = useAuth();
|
|
@@ -719,14 +727,160 @@ const PricingPage = ({ subtitle = "See our pricing options and choose the one th
|
|
|
719
727
|
children: pricingTable.items.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
|
|
720
728
|
plan,
|
|
721
729
|
units,
|
|
730
|
+
showYearlyPrice,
|
|
722
731
|
isPopular: plan.metadata?.zuplo_most_popular === "true",
|
|
723
732
|
isSubscribed: subscriptions.items.some((subscription) => ["active", "canceled"].includes(subscription.status))
|
|
724
733
|
}, plan.id))
|
|
725
|
-
})
|
|
734
|
+
}),
|
|
735
|
+
/* @__PURE__ */ jsx(Slot.Target, { name: "pricing-page-after" })
|
|
726
736
|
]
|
|
727
737
|
});
|
|
728
738
|
};
|
|
729
739
|
|
|
740
|
+
//#endregion
|
|
741
|
+
//#region src/pages/SubscriptionChangeConfirmPage.tsx
|
|
742
|
+
const SubscriptionChangeConfirmPage = () => {
|
|
743
|
+
const [search] = useSearchParams();
|
|
744
|
+
const planId = search.get("planId");
|
|
745
|
+
const subscriptionId = search.get("subscriptionId");
|
|
746
|
+
const zudoku = useZudoku();
|
|
747
|
+
const deploymentName = useDeploymentName();
|
|
748
|
+
const navigate = useNavigate();
|
|
749
|
+
const { data: plans } = usePlans();
|
|
750
|
+
const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
|
|
751
|
+
if (!planId) throw new Error("Parameter `planId` missing");
|
|
752
|
+
if (!subscriptionId) throw new Error("Parameter `subscriptionId` missing");
|
|
753
|
+
const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
|
|
754
|
+
const { quotas, features } = categorizeRateCards(rateCards ?? [], selectedPlan?.currency);
|
|
755
|
+
const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
|
|
756
|
+
const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
|
|
757
|
+
const changeMutation = useMutation({
|
|
758
|
+
mutationKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions/${subscriptionId}/change`],
|
|
759
|
+
meta: {
|
|
760
|
+
context: zudoku,
|
|
761
|
+
request: {
|
|
762
|
+
method: "POST",
|
|
763
|
+
body: JSON.stringify({ planId })
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
onSuccess: async (subscription) => {
|
|
767
|
+
await queryClient.invalidateQueries();
|
|
768
|
+
navigate(`/subscriptions/${subscription.id}`, { state: { planSwitched: { newPlanName: selectedPlan?.name } } });
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
return /* @__PURE__ */ jsx("div", {
|
|
772
|
+
className: "w-full bg-muted min-h-screen flex items-center justify-center px-4 py-12 gap-4",
|
|
773
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
774
|
+
className: "max-w-2xl w-full",
|
|
775
|
+
children: [changeMutation.isError && /* @__PURE__ */ jsxs(Alert, {
|
|
776
|
+
className: "mb-4",
|
|
777
|
+
variant: "destructive",
|
|
778
|
+
children: [/* @__PURE__ */ jsx(AlertTitle, { children: "Error" }), /* @__PURE__ */ jsx(AlertDescription, { children: changeMutation.error.message })]
|
|
779
|
+
}), /* @__PURE__ */ jsxs(Card, {
|
|
780
|
+
className: "p-8 w-full max-w-7xl",
|
|
781
|
+
children: [
|
|
782
|
+
/* @__PURE__ */ jsx("div", {
|
|
783
|
+
className: "flex justify-center mb-6",
|
|
784
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
785
|
+
className: "rounded-full bg-primary/10 p-3",
|
|
786
|
+
children: /* @__PURE__ */ jsx(CheckIcon, { className: "size-9 text-primary" })
|
|
787
|
+
})
|
|
788
|
+
}),
|
|
789
|
+
/* @__PURE__ */ jsxs("div", {
|
|
790
|
+
className: "text-center mb-8",
|
|
791
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
792
|
+
className: "text-2xl font-bold text-card-foreground mb-3",
|
|
793
|
+
children: "Confirm plan change"
|
|
794
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
795
|
+
className: "text-muted-foreground text-base",
|
|
796
|
+
children: "Please confirm the details below to change your subscription."
|
|
797
|
+
})]
|
|
798
|
+
}),
|
|
799
|
+
selectedPlan && /* @__PURE__ */ jsxs(Card, {
|
|
800
|
+
className: "bg-muted/50",
|
|
801
|
+
children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, {
|
|
802
|
+
className: "flex justify-between items-start",
|
|
803
|
+
children: [
|
|
804
|
+
/* @__PURE__ */ jsxs("div", {
|
|
805
|
+
className: "flex items-center gap-3",
|
|
806
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
807
|
+
className: "flex flex-col text-2xl font-bold bg-primary text-primary-foreground items-center justify-center rounded size-12",
|
|
808
|
+
children: selectedPlan.name.at(0)?.toUpperCase()
|
|
809
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
810
|
+
className: "flex flex-col",
|
|
811
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
812
|
+
className: "text-lg font-bold",
|
|
813
|
+
children: selectedPlan.name
|
|
814
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
815
|
+
className: "text-sm font-normal text-muted-foreground",
|
|
816
|
+
children: selectedPlan.description || "New plan"
|
|
817
|
+
})]
|
|
818
|
+
})]
|
|
819
|
+
}),
|
|
820
|
+
price && price.monthly > 0 && /* @__PURE__ */ jsxs("div", {
|
|
821
|
+
className: "text-right",
|
|
822
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
823
|
+
className: "text-2xl font-bold",
|
|
824
|
+
children: formatPrice(price.monthly, selectedPlan?.currency)
|
|
825
|
+
}), billingCycle && /* @__PURE__ */ jsxs("div", {
|
|
826
|
+
className: "text-sm text-muted-foreground font-normal",
|
|
827
|
+
children: ["Billed ", formatBillingCycle(billingCycle)]
|
|
828
|
+
})]
|
|
829
|
+
}),
|
|
830
|
+
price && price.monthly === 0 && /* @__PURE__ */ jsx("div", {
|
|
831
|
+
className: "text-2xl text-muted-foreground font-bold",
|
|
832
|
+
children: "Free"
|
|
833
|
+
})
|
|
834
|
+
]
|
|
835
|
+
}) }), /* @__PURE__ */ jsxs(CardContent, { children: [
|
|
836
|
+
/* @__PURE__ */ jsx(Separator, {}),
|
|
837
|
+
/* @__PURE__ */ jsx("div", {
|
|
838
|
+
className: "text-sm font-medium mb-3 mt-3",
|
|
839
|
+
children: "What's included:"
|
|
840
|
+
}),
|
|
841
|
+
/* @__PURE__ */ jsxs("div", {
|
|
842
|
+
className: "grid grid-cols-2 gap-2 text-muted-foreground",
|
|
843
|
+
children: [quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, {
|
|
844
|
+
quota,
|
|
845
|
+
className: "text-muted-foreground"
|
|
846
|
+
}, quota.key)), features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, {
|
|
847
|
+
feature,
|
|
848
|
+
className: "text-muted-foreground"
|
|
849
|
+
}, feature.key))]
|
|
850
|
+
})
|
|
851
|
+
] })]
|
|
852
|
+
}),
|
|
853
|
+
/* @__PURE__ */ jsxs("div", {
|
|
854
|
+
className: "space-y-3 mt-4",
|
|
855
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
856
|
+
className: "w-full",
|
|
857
|
+
onClick: () => changeMutation.mutate(),
|
|
858
|
+
disabled: changeMutation.isPending,
|
|
859
|
+
children: changeMutation.isPending ? "Changing plan..." : "Confirm & Change Plan"
|
|
860
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
861
|
+
variant: "ghost",
|
|
862
|
+
className: "w-full",
|
|
863
|
+
disabled: changeMutation.isPending,
|
|
864
|
+
asChild: !changeMutation.isPending,
|
|
865
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
866
|
+
to: `/subscriptions/${subscriptionId}`,
|
|
867
|
+
children: "Cancel"
|
|
868
|
+
})
|
|
869
|
+
})]
|
|
870
|
+
}),
|
|
871
|
+
/* @__PURE__ */ jsx("div", {
|
|
872
|
+
className: "mt-6 pt-6 border-t text-center",
|
|
873
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
874
|
+
className: "text-xs text-muted-foreground",
|
|
875
|
+
children: "By confirming, you agree to our Terms of Service and Privacy Policy."
|
|
876
|
+
})
|
|
877
|
+
})
|
|
878
|
+
]
|
|
879
|
+
})]
|
|
880
|
+
})
|
|
881
|
+
});
|
|
882
|
+
};
|
|
883
|
+
|
|
730
884
|
//#endregion
|
|
731
885
|
//#region src/hooks/useSubscriptions.ts
|
|
732
886
|
const useSubscriptions = (environmentName) => {
|
|
@@ -1360,29 +1514,26 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
|
|
|
1360
1514
|
const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
|
|
1361
1515
|
const deploymentName = useDeploymentName();
|
|
1362
1516
|
const context = useZudoku();
|
|
1363
|
-
const
|
|
1364
|
-
const
|
|
1517
|
+
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);
|
|
1365
1521
|
const mutation = useMutation({
|
|
1366
|
-
mutationKey: [`/v3/zudoku-metering/${deploymentName}/
|
|
1522
|
+
mutationKey: [`/v3/zudoku-metering/${deploymentName}/stripe/checkout`],
|
|
1367
1523
|
meta: {
|
|
1368
1524
|
context,
|
|
1369
1525
|
request: {
|
|
1370
1526
|
method: "POST",
|
|
1371
|
-
body: JSON.stringify({
|
|
1527
|
+
body: JSON.stringify({
|
|
1528
|
+
planId: switchTo.plan.id,
|
|
1529
|
+
successURL: successUrl.toString(),
|
|
1530
|
+
cancelURL: generateUrl(`/subscriptions/${switchTo.subscriptionId}`)
|
|
1531
|
+
})
|
|
1372
1532
|
}
|
|
1373
1533
|
},
|
|
1374
1534
|
retry: false,
|
|
1375
|
-
onSuccess:
|
|
1376
|
-
|
|
1377
|
-
navigate(`/subscriptions/${subscription.id}`, { state: { planSwitched: {
|
|
1378
|
-
mode: switchTo.mode,
|
|
1379
|
-
newPlanName: switchTo.plan.name
|
|
1380
|
-
} } });
|
|
1381
|
-
onRequestClose();
|
|
1382
|
-
window.scrollTo({
|
|
1383
|
-
top: 0,
|
|
1384
|
-
behavior: "smooth"
|
|
1385
|
-
});
|
|
1535
|
+
onSuccess: (data) => {
|
|
1536
|
+
window.location.href = data.url;
|
|
1386
1537
|
}
|
|
1387
1538
|
});
|
|
1388
1539
|
return /* @__PURE__ */ jsx(AlertDialog, {
|
|
@@ -1782,16 +1933,8 @@ const ActiveSubscription = ({ subscription, deploymentName }) => {
|
|
|
1782
1933
|
variant: "info",
|
|
1783
1934
|
children: [
|
|
1784
1935
|
/* @__PURE__ */ jsx(CheckCheckIcon, { className: "size-4" }),
|
|
1785
|
-
/* @__PURE__ */
|
|
1786
|
-
|
|
1787
|
-
" ",
|
|
1788
|
-
planSwitched.mode === "upgrade" ? "upgraded" : planSwitched.mode === "downgrade" ? "downgraded" : "changed"
|
|
1789
|
-
] }),
|
|
1790
|
-
/* @__PURE__ */ jsxs(AlertDescription, { children: [
|
|
1791
|
-
"You have successfully switched to ",
|
|
1792
|
-
planSwitched.newPlanName,
|
|
1793
|
-
"."
|
|
1794
|
-
] }),
|
|
1936
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: "Plan changed" }),
|
|
1937
|
+
/* @__PURE__ */ jsx(AlertDescription, { children: planSwitched.newPlanName ? `You have successfully switched to ${planSwitched.newPlanName}.` : "Your plan has been successfully changed." }),
|
|
1795
1938
|
/* @__PURE__ */ jsx(DismissibleAlertAction, {})
|
|
1796
1939
|
]
|
|
1797
1940
|
}),
|
|
@@ -1983,6 +2126,10 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
|
|
|
1983
2126
|
path: "/checkout-confirm",
|
|
1984
2127
|
element: /* @__PURE__ */ jsx(CheckoutConfirmPage, {})
|
|
1985
2128
|
},
|
|
2129
|
+
{
|
|
2130
|
+
path: "/subscription-change-confirm",
|
|
2131
|
+
element: /* @__PURE__ */ jsx(SubscriptionChangeConfirmPage, {})
|
|
2132
|
+
},
|
|
1986
2133
|
{
|
|
1987
2134
|
path: "/manage-payment",
|
|
1988
2135
|
element: /* @__PURE__ */ jsx(ManagePaymentPage, {})
|
|
@@ -1993,7 +2140,8 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
|
|
|
1993
2140
|
element: /* @__PURE__ */ jsx(PricingPage, {
|
|
1994
2141
|
subtitle: options?.pricing?.subtitle,
|
|
1995
2142
|
title: options?.pricing?.title,
|
|
1996
|
-
units: options?.pricing?.units
|
|
2143
|
+
units: options?.pricing?.units,
|
|
2144
|
+
showYearlyPrice: options?.pricing?.showYearlyPrice
|
|
1997
2145
|
})
|
|
1998
2146
|
},
|
|
1999
2147
|
{
|
|
@@ -2008,6 +2156,7 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
|
|
|
2008
2156
|
return [
|
|
2009
2157
|
"/checkout/*",
|
|
2010
2158
|
"/checkout-confirm",
|
|
2159
|
+
"/subscription-change-confirm",
|
|
2011
2160
|
"/subscriptions/*",
|
|
2012
2161
|
"/manage-payment"
|
|
2013
2162
|
];
|
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.24",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/zuplo/zudoku",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@testing-library/react": "16.3.2",
|
|
28
28
|
"@types/react": "19.2.14",
|
|
29
29
|
"@types/react-dom": "19.2.3",
|
|
30
|
-
"happy-dom": "20.
|
|
30
|
+
"happy-dom": "20.8.3",
|
|
31
31
|
"react": "19.2.4",
|
|
32
32
|
"react-dom": "19.2.4",
|
|
33
33
|
"tsdown": "0.20.3",
|
|
34
|
-
"zudoku": "0.
|
|
34
|
+
"zudoku": "0.71.3"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": ">=19.2.0",
|