@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 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) throw new Error("Failed to fetch request");
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 queryClient = useQueryClient();
1364
- const navigate = useNavigate();
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}/subscriptions/${switchTo.subscriptionId}/change`],
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({ planId: switchTo.plan.id })
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: async (subscription) => {
1376
- await queryClient.invalidateQueries();
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__ */ jsxs(AlertTitle, { children: [
1786
- "Plan",
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.22",
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.7.0",
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.70.4"
34
+ "zudoku": "0.71.3"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=19.2.0",