@zuplo/zudoku-plugin-monetization 0.0.23 → 0.0.25

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.
Files changed (2) hide show
  1. package/dist/index.mjs +193 -55
  2. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
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, useParams, useSearchParams } from "zudoku/router";
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";
@@ -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,6 +227,7 @@ 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);
230
+ await throwIfProblemJson(response);
220
231
  if (!response.ok) throw new Error("Failed to fetch request");
221
232
  return response.json();
222
233
  }
@@ -246,11 +257,8 @@ const queryClient = new QueryClient({ defaultOptions: {
246
257
  }
247
258
  });
248
259
  const response = await fetch(m.meta?.context ? await m.meta.context.signRequest(request) : request);
260
+ await throwIfProblemJson(response);
249
261
  if (!response.ok) {
250
- if (response.headers.get("content-type")?.includes("application/problem+json")) {
251
- const data = await response.json();
252
- throw new Error(data.detail ?? data.title);
253
- }
254
262
  const errorText = await response.text();
255
263
  throw new Error(`Request failed: ${response.status} ${errorText}`);
256
264
  }
@@ -268,17 +276,9 @@ const ZuploMonetizationWrapper = () => {
268
276
 
269
277
  //#endregion
270
278
  //#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
279
  const CheckoutConfirmPage = () => {
280
280
  const [search] = useSearchParams();
281
- const planId = search.get("plan");
281
+ const planId = search.get("planId");
282
282
  const zudoku = useZudoku();
283
283
  const deploymentName = useDeploymentName();
284
284
  const navigate = useNavigate();
@@ -300,7 +300,7 @@ const CheckoutConfirmPage = () => {
300
300
  },
301
301
  onSuccess: async (subscription) => {
302
302
  await queryClient.invalidateQueries();
303
- navigate(`/subscriptions/${subscription.id}`);
303
+ navigate(`/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`);
304
304
  }
305
305
  });
306
306
  return /* @__PURE__ */ jsx("div", {
@@ -476,23 +476,22 @@ const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
476
476
  //#region src/hooks/useUrlUtils.ts
477
477
  const useUrlUtils = () => {
478
478
  const basePath = useZudoku().options.basePath;
479
- return { generateUrl: (path) => {
479
+ return { generateUrl: (path, { searchParams } = {}) => {
480
480
  if (!window.location.origin) throw new Error("Only works in browser environment");
481
- return joinUrl(window.location.origin, basePath, path);
481
+ return joinUrl(window.location.origin, basePath, path, searchParams ? `?${new URLSearchParams(searchParams)}` : void 0);
482
482
  } };
483
483
  };
484
484
 
485
485
  //#endregion
486
486
  //#region src/pages/CheckoutPage.tsx
487
487
  const CheckoutPage = () => {
488
- const { planId } = useParams();
488
+ const [searchParams] = useSearchParams();
489
+ const planId = searchParams.get("planId");
489
490
  const zudoku = useZudoku();
490
491
  const auth = useAuth();
491
492
  const { generateUrl } = useUrlUtils();
492
493
  const deploymentName = useDeploymentName();
493
494
  if (!planId) throw new Error(`missing planId in URL`);
494
- const successUrl = new URL(generateUrl("/checkout-confirm"));
495
- successUrl.searchParams.set("plan", planId);
496
495
  const checkoutLink = useQuery({
497
496
  queryKey: [
498
497
  `/v3/zudoku-metering/${deploymentName}/stripe/checkout`,
@@ -505,7 +504,7 @@ const CheckoutPage = () => {
505
504
  method: "POST",
506
505
  body: JSON.stringify({
507
506
  planId,
508
- successURL: successUrl.toString(),
507
+ successURL: generateUrl("/checkout-confirm", { searchParams: { planId } }),
509
508
  cancelURL: generateUrl("/pricing")
510
509
  })
511
510
  }
@@ -675,7 +674,7 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false, showYearly
675
674
  variant: isPopular ? "default" : "secondary",
676
675
  asChild: true,
677
676
  children: /* @__PURE__ */ jsx(Link$1, {
678
- to: `/checkout/${plan.id}`,
677
+ to: `/checkout?planId=${encodeURIComponent(plan.id)}`,
679
678
  children: "Subscribe"
680
679
  })
681
680
  })
@@ -729,6 +728,150 @@ const PricingPage = ({ subtitle = "See our pricing options and choose the one th
729
728
  });
730
729
  };
731
730
 
731
+ //#endregion
732
+ //#region src/pages/SubscriptionChangeConfirmPage.tsx
733
+ const SubscriptionChangeConfirmPage = () => {
734
+ const [search] = useSearchParams();
735
+ const planId = search.get("planId");
736
+ const subscriptionId = search.get("subscriptionId");
737
+ const zudoku = useZudoku();
738
+ const deploymentName = useDeploymentName();
739
+ const navigate = useNavigate();
740
+ const { data: plans } = usePlans();
741
+ const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
742
+ if (!planId) throw new Error("Parameter `planId` missing");
743
+ if (!subscriptionId) throw new Error("Parameter `subscriptionId` missing");
744
+ const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
745
+ const { quotas, features } = categorizeRateCards(rateCards ?? [], selectedPlan?.currency);
746
+ const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
747
+ const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
748
+ const changeMutation = useMutation({
749
+ mutationKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions/${subscriptionId}/change`],
750
+ meta: {
751
+ context: zudoku,
752
+ request: {
753
+ method: "POST",
754
+ body: JSON.stringify({ planId })
755
+ }
756
+ },
757
+ onSuccess: async (subscription) => {
758
+ await queryClient.invalidateQueries();
759
+ navigate(`/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`, { state: { planSwitched: { newPlanName: selectedPlan?.name } } });
760
+ }
761
+ });
762
+ return /* @__PURE__ */ jsx("div", {
763
+ className: "w-full bg-muted min-h-screen flex items-center justify-center px-4 py-12 gap-4",
764
+ children: /* @__PURE__ */ jsxs("div", {
765
+ className: "max-w-2xl w-full",
766
+ children: [changeMutation.isError && /* @__PURE__ */ jsxs(Alert, {
767
+ className: "mb-4",
768
+ variant: "destructive",
769
+ children: [/* @__PURE__ */ jsx(AlertTitle, { children: "Error" }), /* @__PURE__ */ jsx(AlertDescription, { children: changeMutation.error.message })]
770
+ }), /* @__PURE__ */ jsxs(Card, {
771
+ className: "p-8 w-full max-w-7xl",
772
+ children: [
773
+ /* @__PURE__ */ jsx("div", {
774
+ className: "flex justify-center mb-6",
775
+ children: /* @__PURE__ */ jsx("div", {
776
+ className: "rounded-full bg-primary/10 p-3",
777
+ children: /* @__PURE__ */ jsx(CheckIcon, { className: "size-9 text-primary" })
778
+ })
779
+ }),
780
+ /* @__PURE__ */ jsxs("div", {
781
+ className: "text-center mb-8",
782
+ children: [/* @__PURE__ */ jsx("h1", {
783
+ className: "text-2xl font-bold text-card-foreground mb-3",
784
+ children: "Confirm plan change"
785
+ }), /* @__PURE__ */ jsx("p", {
786
+ className: "text-muted-foreground text-base",
787
+ children: "Please confirm the details below to change your subscription."
788
+ })]
789
+ }),
790
+ selectedPlan && /* @__PURE__ */ jsxs(Card, {
791
+ className: "bg-muted/50",
792
+ children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, {
793
+ className: "flex justify-between items-start",
794
+ children: [
795
+ /* @__PURE__ */ jsxs("div", {
796
+ className: "flex items-center gap-3",
797
+ children: [/* @__PURE__ */ jsx("div", {
798
+ className: "flex flex-col text-2xl font-bold bg-primary text-primary-foreground items-center justify-center rounded size-12",
799
+ children: selectedPlan.name.at(0)?.toUpperCase()
800
+ }), /* @__PURE__ */ jsxs("div", {
801
+ className: "flex flex-col",
802
+ children: [/* @__PURE__ */ jsx("span", {
803
+ className: "text-lg font-bold",
804
+ children: selectedPlan.name
805
+ }), /* @__PURE__ */ jsx("span", {
806
+ className: "text-sm font-normal text-muted-foreground",
807
+ children: selectedPlan.description || "New plan"
808
+ })]
809
+ })]
810
+ }),
811
+ price && price.monthly > 0 && /* @__PURE__ */ jsxs("div", {
812
+ className: "text-right",
813
+ children: [/* @__PURE__ */ jsx("div", {
814
+ className: "text-2xl font-bold",
815
+ children: formatPrice(price.monthly, selectedPlan?.currency)
816
+ }), billingCycle && /* @__PURE__ */ jsxs("div", {
817
+ className: "text-sm text-muted-foreground font-normal",
818
+ children: ["Billed ", formatBillingCycle(billingCycle)]
819
+ })]
820
+ }),
821
+ price && price.monthly === 0 && /* @__PURE__ */ jsx("div", {
822
+ className: "text-2xl text-muted-foreground font-bold",
823
+ children: "Free"
824
+ })
825
+ ]
826
+ }) }), /* @__PURE__ */ jsxs(CardContent, { children: [
827
+ /* @__PURE__ */ jsx(Separator, {}),
828
+ /* @__PURE__ */ jsx("div", {
829
+ className: "text-sm font-medium mb-3 mt-3",
830
+ children: "What's included:"
831
+ }),
832
+ /* @__PURE__ */ jsxs("div", {
833
+ className: "grid grid-cols-2 gap-2 text-muted-foreground",
834
+ children: [quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, {
835
+ quota,
836
+ className: "text-muted-foreground"
837
+ }, quota.key)), features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, {
838
+ feature,
839
+ className: "text-muted-foreground"
840
+ }, feature.key))]
841
+ })
842
+ ] })]
843
+ }),
844
+ /* @__PURE__ */ jsxs("div", {
845
+ className: "space-y-3 mt-4",
846
+ children: [/* @__PURE__ */ jsx(Button, {
847
+ className: "w-full",
848
+ onClick: () => changeMutation.mutate(),
849
+ disabled: changeMutation.isPending,
850
+ children: changeMutation.isPending ? "Changing plan..." : "Confirm & Change Plan"
851
+ }), /* @__PURE__ */ jsx(Button, {
852
+ variant: "ghost",
853
+ className: "w-full",
854
+ disabled: changeMutation.isPending,
855
+ asChild: !changeMutation.isPending,
856
+ children: /* @__PURE__ */ jsx(Link$1, {
857
+ to: `/subscriptions?${new URLSearchParams({ subscriptionId: subscriptionId ?? "" })}`,
858
+ children: "Cancel"
859
+ })
860
+ })]
861
+ }),
862
+ /* @__PURE__ */ jsx("div", {
863
+ className: "mt-6 pt-6 border-t text-center",
864
+ children: /* @__PURE__ */ jsx("p", {
865
+ className: "text-xs text-muted-foreground",
866
+ children: "By confirming, you agree to our Terms of Service and Privacy Policy."
867
+ })
868
+ })
869
+ ]
870
+ })]
871
+ })
872
+ });
873
+ };
874
+
732
875
  //#endregion
733
876
  //#region src/hooks/useSubscriptions.ts
734
877
  const useSubscriptions = (environmentName) => {
@@ -1362,29 +1505,26 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
1362
1505
  const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
1363
1506
  const deploymentName = useDeploymentName();
1364
1507
  const context = useZudoku();
1365
- const queryClient = useQueryClient();
1366
- const navigate = useNavigate();
1508
+ const { generateUrl } = useUrlUtils();
1367
1509
  const mutation = useMutation({
1368
- mutationKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions/${switchTo.subscriptionId}/change`],
1510
+ mutationKey: [`/v3/zudoku-metering/${deploymentName}/stripe/checkout`],
1369
1511
  meta: {
1370
1512
  context,
1371
1513
  request: {
1372
1514
  method: "POST",
1373
- body: JSON.stringify({ planId: switchTo.plan.id })
1515
+ body: JSON.stringify({
1516
+ planId: switchTo.plan.id,
1517
+ successURL: generateUrl(`/subscription-change-confirm`, { searchParams: {
1518
+ planId: switchTo.plan.id,
1519
+ subscriptionId: switchTo.subscriptionId
1520
+ } }),
1521
+ cancelURL: generateUrl("/subscriptions", { searchParams: { subscriptionId: switchTo.subscriptionId } })
1522
+ })
1374
1523
  }
1375
1524
  },
1376
1525
  retry: false,
1377
- onSuccess: async (subscription) => {
1378
- await queryClient.invalidateQueries();
1379
- navigate(`/subscriptions/${subscription.id}`, { state: { planSwitched: {
1380
- mode: switchTo.mode,
1381
- newPlanName: switchTo.plan.name
1382
- } } });
1383
- onRequestClose();
1384
- window.scrollTo({
1385
- top: 0,
1386
- behavior: "smooth"
1387
- });
1526
+ onSuccess: (data) => {
1527
+ window.location.href = data.url;
1388
1528
  }
1389
1529
  });
1390
1530
  return /* @__PURE__ */ jsx(AlertDialog, {
@@ -1784,16 +1924,8 @@ const ActiveSubscription = ({ subscription, deploymentName }) => {
1784
1924
  variant: "info",
1785
1925
  children: [
1786
1926
  /* @__PURE__ */ jsx(CheckCheckIcon, { className: "size-4" }),
1787
- /* @__PURE__ */ jsxs(AlertTitle, { children: [
1788
- "Plan",
1789
- " ",
1790
- planSwitched.mode === "upgrade" ? "upgraded" : planSwitched.mode === "downgrade" ? "downgraded" : "changed"
1791
- ] }),
1792
- /* @__PURE__ */ jsxs(AlertDescription, { children: [
1793
- "You have successfully switched to ",
1794
- planSwitched.newPlanName,
1795
- "."
1796
- ] }),
1927
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Plan changed" }),
1928
+ /* @__PURE__ */ jsx(AlertDescription, { children: planSwitched.newPlanName ? `You have successfully switched to ${planSwitched.newPlanName}.` : "Your plan has been successfully changed." }),
1797
1929
  /* @__PURE__ */ jsx(DismissibleAlertAction, {})
1798
1930
  ]
1799
1931
  }),
@@ -1863,7 +1995,7 @@ const SubscriptionsList = ({ subscriptions, activeSubscriptionId }) => {
1863
1995
  const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
1864
1996
  const willExpire = subscription.activeTo && new Date(subscription.activeTo) > /* @__PURE__ */ new Date();
1865
1997
  return /* @__PURE__ */ jsx(Link$1, {
1866
- to: `/subscriptions/${subscription.id}`,
1998
+ to: `/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`,
1867
1999
  children: /* @__PURE__ */ jsx(Item, {
1868
2000
  size: "sm",
1869
2001
  variant: "outline",
@@ -1888,7 +2020,7 @@ const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
1888
2020
  ]
1889
2021
  })] })
1890
2022
  }, subscription.id)
1891
- }, subscription.id);
2023
+ });
1892
2024
  };
1893
2025
 
1894
2026
  //#endregion
@@ -1896,7 +2028,8 @@ const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
1896
2028
  const SubscriptionsPage = () => {
1897
2029
  const deploymentName = useDeploymentName();
1898
2030
  const { data } = useSubscriptions(deploymentName);
1899
- const { subscriptionId } = useParams();
2031
+ const [searchParams] = useSearchParams();
2032
+ const subscriptionId = searchParams.get("subscriptionId");
1900
2033
  const subscriptions = data?.items ?? [];
1901
2034
  const activeSubscription = useMemo(() => {
1902
2035
  if (subscriptions.length === 0) return null;
@@ -1978,13 +2111,17 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
1978
2111
  handle: { layout: "none" },
1979
2112
  children: [
1980
2113
  {
1981
- path: "/checkout/:planId?",
2114
+ path: "/checkout",
1982
2115
  element: /* @__PURE__ */ jsx(CheckoutPage, {})
1983
2116
  },
1984
2117
  {
1985
2118
  path: "/checkout-confirm",
1986
2119
  element: /* @__PURE__ */ jsx(CheckoutConfirmPage, {})
1987
2120
  },
2121
+ {
2122
+ path: "/subscription-change-confirm",
2123
+ element: /* @__PURE__ */ jsx(SubscriptionChangeConfirmPage, {})
2124
+ },
1988
2125
  {
1989
2126
  path: "/manage-payment",
1990
2127
  element: /* @__PURE__ */ jsx(ManagePaymentPage, {})
@@ -2001,7 +2138,7 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
2001
2138
  },
2002
2139
  {
2003
2140
  handle: { layout: "default" },
2004
- path: "/subscriptions/:subscriptionId?",
2141
+ path: "/subscriptions",
2005
2142
  element: /* @__PURE__ */ jsx(SubscriptionsPage, {})
2006
2143
  }
2007
2144
  ]
@@ -2009,9 +2146,10 @@ const zuploMonetizationPlugin = createPlugin((options) => ({
2009
2146
  },
2010
2147
  getProtectedRoutes: () => {
2011
2148
  return [
2012
- "/checkout/*",
2149
+ "/checkout",
2013
2150
  "/checkout-confirm",
2014
- "/subscriptions/*",
2151
+ "/subscription-change-confirm",
2152
+ "/subscriptions",
2015
2153
  "/manage-payment"
2016
2154
  ];
2017
2155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuplo/zudoku-plugin-monetization",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
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.2"
34
+ "zudoku": "0.71.10"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=19.2.0",