@zuplo/zudoku-plugin-monetization 0.0.28 → 0.0.29

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
@@ -1,5 +1,5 @@
1
- import * as zudoku from "zudoku";
2
1
  import "react";
2
+ import * as zudoku from "zudoku";
3
3
 
4
4
  //#region src/MonetizationContext.d.ts
5
5
  interface MonetizationConfig {
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { Suspense, createContext, use, useEffect, useMemo, useState } from "react";
1
2
  import { cn, createPlugin, joinUrl, throwIfProblemJson } from "zudoku";
2
3
  import { AlertTriangleIcon, ArrowDownIcon, ArrowLeftRightIcon, ArrowUpIcon, CalendarIcon, CheckCheckIcon, CheckIcon, CircleSlashIcon, ClockIcon, CreditCardIcon, Grid2x2XIcon, InfoIcon, Loader2Icon, LockIcon, MoreVerticalIcon, RefreshCcw, RefreshCwIcon, Settings, ShieldIcon, StarsIcon, Trash2Icon, XIcon } from "zudoku/icons";
3
4
  import { Button, ClientOnly, Head, Heading, Link, Slot } from "zudoku/components";
@@ -8,9 +9,9 @@ import { Alert, AlertAction, AlertDescription, AlertTitle } from "zudoku/ui/Aler
8
9
  import { Card, CardContent, CardHeader, CardTitle } from "zudoku/ui/Card";
9
10
  import { Separator } from "zudoku/ui/Separator";
10
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
- import { createContext, use, useEffect, useMemo, useState } from "react";
12
12
  import { parse } from "tinyduration";
13
13
  import { Button as Button$1 } from "zudoku/ui/Button";
14
+ import { Skeleton } from "zudoku/ui/Skeleton";
14
15
  import { DismissibleAlert, DismissibleAlertAction } from "zudoku/ui/DismissibleAlert";
15
16
  import { ActionButton } from "zudoku/ui/ActionButton";
16
17
  import { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "zudoku/ui/Item";
@@ -795,6 +796,37 @@ const PricingPage = () => {
795
796
  });
796
797
  };
797
798
 
799
+ //#endregion
800
+ //#region src/pages/PricingPageSkeleton.tsx
801
+ const PricingPageSkeleton = () => /* @__PURE__ */ jsxs("div", {
802
+ className: "w-full px-4 pt-(--padding-content-top) pb-(--padding-content-bottom)",
803
+ children: [/* @__PURE__ */ jsxs("div", {
804
+ className: "text-center space-y-4 mb-12",
805
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-48 mx-auto" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-96 mx-auto" })]
806
+ }), /* @__PURE__ */ jsx("div", {
807
+ className: "w-full grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(300px,max-content))] justify-center gap-6",
808
+ children: [
809
+ 1,
810
+ 2,
811
+ 3
812
+ ].map((i) => /* @__PURE__ */ jsxs(Card, {
813
+ className: "w-[300px]",
814
+ children: [/* @__PURE__ */ jsxs(CardHeader, {
815
+ className: "space-y-3",
816
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-6 w-24" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-32" })]
817
+ }), /* @__PURE__ */ jsxs(CardContent, {
818
+ className: "space-y-3",
819
+ children: [
820
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }),
821
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }),
822
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/4" }),
823
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full mt-4" })
824
+ ]
825
+ })]
826
+ }, i))
827
+ })]
828
+ });
829
+
798
830
  //#endregion
799
831
  //#region src/pages/SubscriptionChangeConfirmPage.tsx
800
832
  const SubscriptionChangeConfirmPage = () => {
@@ -969,7 +1001,15 @@ const useSubscriptions = (environmentName) => {
969
1001
  const zudoku = useZudoku();
970
1002
  return useSuspenseQuery({
971
1003
  queryKey: [`/v3/zudoku-metering/${environmentName}/subscriptions`],
972
- meta: { context: zudoku }
1004
+ meta: { context: zudoku },
1005
+ select: (data) => ({
1006
+ ...data,
1007
+ items: [...data.items].sort((a, b) => {
1008
+ if (a.status === "active" && b.status !== "active") return -1;
1009
+ if (a.status !== "active" && b.status === "active") return 1;
1010
+ return 0;
1011
+ })
1012
+ })
973
1013
  });
974
1014
  };
975
1015
 
@@ -1461,6 +1501,23 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
1461
1501
 
1462
1502
  //#endregion
1463
1503
  //#region src/pages/subscriptions/SwitchPlanModal.tsx
1504
+ const getAllKeysAcrossPhases = (plan, units) => {
1505
+ const quotaKeys = /* @__PURE__ */ new Set();
1506
+ const featureKeys = /* @__PURE__ */ new Set();
1507
+ for (const phase of plan.phases) {
1508
+ const { quotas, features } = categorizeRateCards(phase.rateCards, {
1509
+ currency: plan.currency,
1510
+ units,
1511
+ planBillingCadence: plan.billingCadence
1512
+ });
1513
+ for (const q of quotas) quotaKeys.add(q.key);
1514
+ for (const f of features) featureKeys.add(f.key);
1515
+ }
1516
+ return {
1517
+ quotaKeys,
1518
+ featureKeys
1519
+ };
1520
+ };
1464
1521
  const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units) => {
1465
1522
  const isUpgrade = targetIndex > currentIndex;
1466
1523
  const currentPhase = currentPlan?.phases.at(-1);
@@ -1481,6 +1538,11 @@ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units)
1481
1538
  quotas: [],
1482
1539
  features: []
1483
1540
  };
1541
+ const currentAllKeys = currentPlan ? getAllKeysAcrossPhases(currentPlan, units) : {
1542
+ quotaKeys: /* @__PURE__ */ new Set(),
1543
+ featureKeys: /* @__PURE__ */ new Set()
1544
+ };
1545
+ const targetAllKeys = getAllKeysAcrossPhases(targetPlan, units);
1484
1546
  const quotaChanges = [];
1485
1547
  const allQuotaKeys = new Set([...currentQuotas.map((q) => q.key), ...targetQuotas.map((q) => q.key)]);
1486
1548
  for (const key of allQuotaKeys) {
@@ -1498,22 +1560,37 @@ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units)
1498
1560
  period: target.period,
1499
1561
  change
1500
1562
  });
1501
- } else if (target && !current) quotaChanges.push({
1502
- key: key ?? "",
1503
- name: target.name,
1504
- currentValue: null,
1505
- newValue: target.limit,
1506
- period: target.period,
1507
- change: "added"
1508
- });
1509
- else if (current && !target) quotaChanges.push({
1510
- key: key ?? "",
1511
- name: current.name,
1512
- currentValue: current.limit,
1513
- newValue: null,
1514
- period: current.period,
1515
- change: "removed"
1516
- });
1563
+ } else if (target && !current) {
1564
+ if (currentAllKeys.featureKeys.has(key)) {
1565
+ quotaChanges.push({
1566
+ key: key ?? "",
1567
+ name: target.name,
1568
+ currentValue: null,
1569
+ newValue: target.limit,
1570
+ period: target.period,
1571
+ change: "same"
1572
+ });
1573
+ continue;
1574
+ }
1575
+ quotaChanges.push({
1576
+ key: key ?? "",
1577
+ name: target.name,
1578
+ currentValue: null,
1579
+ newValue: target.limit,
1580
+ period: target.period,
1581
+ change: "added"
1582
+ });
1583
+ } else if (current && !target) {
1584
+ if (targetAllKeys.featureKeys.has(key)) continue;
1585
+ quotaChanges.push({
1586
+ key: key ?? "",
1587
+ name: current.name,
1588
+ currentValue: current.limit,
1589
+ newValue: null,
1590
+ period: current.period,
1591
+ change: "removed"
1592
+ });
1593
+ }
1517
1594
  }
1518
1595
  const featureChanges = [];
1519
1596
  const allFeatureKeys = new Set([...currentFeatures.map((f) => f.key), ...targetFeatures.map((f) => f.key)]);
@@ -1530,20 +1607,34 @@ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units)
1530
1607
  newValue: target.value ?? true,
1531
1608
  change
1532
1609
  });
1533
- } else if (target && !current) featureChanges.push({
1534
- key: key ?? "",
1535
- name: target.name,
1536
- currentValue: null,
1537
- newValue: target.value ?? true,
1538
- change: "added"
1539
- });
1540
- else if (current && !target) featureChanges.push({
1541
- key: key ?? "",
1542
- name: current.name,
1543
- currentValue: current.value ?? true,
1544
- newValue: null,
1545
- change: "removed"
1546
- });
1610
+ } else if (target && !current) {
1611
+ if (currentAllKeys.quotaKeys.has(key)) {
1612
+ featureChanges.push({
1613
+ key: key ?? "",
1614
+ name: target.name,
1615
+ currentValue: true,
1616
+ newValue: target.value ?? true,
1617
+ change: "same"
1618
+ });
1619
+ continue;
1620
+ }
1621
+ featureChanges.push({
1622
+ key: key ?? "",
1623
+ name: target.name,
1624
+ currentValue: null,
1625
+ newValue: target.value ?? true,
1626
+ change: "added"
1627
+ });
1628
+ } else if (current && !target) {
1629
+ if (targetAllKeys.quotaKeys.has(key)) continue;
1630
+ featureChanges.push({
1631
+ key: key ?? "",
1632
+ name: current.name,
1633
+ currentValue: current.value ?? true,
1634
+ newValue: null,
1635
+ change: "removed"
1636
+ });
1637
+ }
1547
1638
  }
1548
1639
  return {
1549
1640
  plan: targetPlan,
@@ -1571,7 +1662,7 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange,
1571
1662
  const price = getPriceFromPlan(comparison.plan);
1572
1663
  const isCustom = comparison.plan.key === "enterprise";
1573
1664
  const displayPrice = price.monthly;
1574
- const hasChanges = comparison.quotaChanges.some((q) => q.change !== "same") || comparison.featureChanges.some((f) => f.change !== "same");
1665
+ const hasChanges = comparison.quotaChanges.length > 0 || comparison.featureChanges.length > 0;
1575
1666
  return /* @__PURE__ */ jsxs("div", {
1576
1667
  className: "border rounded-lg p-4",
1577
1668
  children: [/* @__PURE__ */ jsxs("div", {
@@ -1612,7 +1703,7 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange,
1612
1703
  })]
1613
1704
  }), hasChanges && /* @__PURE__ */ jsxs("div", {
1614
1705
  className: "space-y-1.5",
1615
- children: [comparison.quotaChanges.filter((q) => q.change !== "same").map((quota) => /* @__PURE__ */ jsxs("div", {
1706
+ children: [comparison.quotaChanges.map((quota) => /* @__PURE__ */ jsxs("div", {
1616
1707
  className: "flex items-center gap-2 text-sm",
1617
1708
  children: [
1618
1709
  /* @__PURE__ */ jsx(ChangeIndicator, { change: quota.change }),
@@ -1620,7 +1711,14 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange,
1620
1711
  className: "font-medium",
1621
1712
  children: [quota.name, ":"]
1622
1713
  }),
1623
- quota.change === "added" ? /* @__PURE__ */ jsx("span", {
1714
+ quota.change === "same" ? /* @__PURE__ */ jsxs("span", {
1715
+ className: "text-muted-foreground",
1716
+ children: [
1717
+ (quota.newValue ?? quota.currentValue)?.toLocaleString(),
1718
+ "/",
1719
+ quota.period
1720
+ ]
1721
+ }) : quota.change === "added" ? /* @__PURE__ */ jsx("span", {
1624
1722
  className: "text-green-600",
1625
1723
  children: "Now included"
1626
1724
  }) : quota.change === "removed" ? /* @__PURE__ */ jsx("span", {
@@ -1649,9 +1747,12 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange,
1649
1747
  })
1650
1748
  ] })
1651
1749
  ]
1652
- }, quota.key)), comparison.featureChanges.filter((f) => f.change !== "same").map((feature) => /* @__PURE__ */ jsx("div", {
1750
+ }, quota.key)), comparison.featureChanges.map((feature) => /* @__PURE__ */ jsx("div", {
1653
1751
  className: "flex items-center gap-2 text-sm",
1654
- children: feature.change === "added" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1752
+ children: feature.change === "same" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" }), /* @__PURE__ */ jsxs("span", {
1753
+ className: "text-muted-foreground",
1754
+ children: [feature.name, typeof feature.newValue === "string" ? `: ${feature.newValue}` : typeof feature.currentValue === "string" ? `: ${feature.currentValue}` : ""]
1755
+ })] }) : feature.change === "added" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1655
1756
  /* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" }),
1656
1757
  /* @__PURE__ */ jsx("span", {
1657
1758
  className: "text-muted-foreground font-medium",
@@ -1735,7 +1836,7 @@ const SwitchPlanModal = ({ subscription, children }) => {
1735
1836
  window.location.href = data.url;
1736
1837
  }
1737
1838
  });
1738
- const currentPlan = plansData?.items.find((p) => p.id === subscription.plan.id);
1839
+ const currentPlan = plansData?.items.find((p) => p.key === subscription.plan.key);
1739
1840
  const { upgrades, downgrades, privatePlans } = useMemo(() => {
1740
1841
  if (!plansData?.items || !currentPlan) return {
1741
1842
  upgrades: [],
@@ -1961,7 +2062,7 @@ const ManageSubscription = ({ subscription, planName }) => {
1961
2062
  const isMeteredEntitlement = (entitlement) => {
1962
2063
  return "balance" in entitlement;
1963
2064
  };
1964
- const UsageItem = ({ meter, item, subscription }) => {
2065
+ const UsageItem = ({ meter, item, subscription, featureKey }) => {
1965
2066
  const cadence = item?.billingCadence ?? subscription?.billingCadence;
1966
2067
  const billingPeriod = cadence ? formatDurationAdjective(cadence) : "monthly";
1967
2068
  const isSoftLimit = item?.included?.entitlement?.isSoftLimit ?? true;
@@ -2020,7 +2121,7 @@ const UsageItem = ({ meter, item, subscription }) => {
2020
2121
  ]
2021
2122
  }),
2022
2123
  /* @__PURE__ */ jsxs(CardTitle, { children: [
2023
- item?.name ?? "Limit",
2124
+ item?.name ?? featureKey,
2024
2125
  " ",
2025
2126
  item?.price?.amount
2026
2127
  ] })
@@ -2100,6 +2201,7 @@ const Usage = ({ usage, isFetching, currentItems, subscription, isPendingFirstPa
2100
2201
  ]
2101
2202
  }),
2102
2203
  hasUsage ? Object.entries(usage.entitlements).flatMap(([key, value]) => isMeteredEntitlement(value) ? /* @__PURE__ */ jsx(UsageItem, {
2204
+ featureKey: key,
2103
2205
  meter: { ...value },
2104
2206
  subscription,
2105
2207
  item: currentItems?.find((item) => item.featureKey === key)
@@ -2277,6 +2379,40 @@ const SubscriptionsPage = () => {
2277
2379
  });
2278
2380
  };
2279
2381
 
2382
+ //#endregion
2383
+ //#region src/pages/SubscriptionsPageSkeleton.tsx
2384
+ const SubscriptionsPageSkeleton = () => /* @__PURE__ */ jsx("div", {
2385
+ className: "w-full pt-(--padding-content-top) pb-(--padding-content-bottom)",
2386
+ children: /* @__PURE__ */ jsxs("div", {
2387
+ className: "max-w-4xl space-y-8",
2388
+ children: [
2389
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-56 mb-2" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-80" })] }),
2390
+ /* @__PURE__ */ jsx("div", {
2391
+ className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3",
2392
+ children: [1, 2].map((i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-20 rounded-lg" }, i))
2393
+ }),
2394
+ /* @__PURE__ */ jsxs("div", {
2395
+ className: "space-y-4",
2396
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-16" }), /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-32" }) }), /* @__PURE__ */ jsxs(CardContent, {
2397
+ className: "space-y-2",
2398
+ children: [
2399
+ /* @__PURE__ */ jsxs("div", {
2400
+ className: "flex justify-between",
2401
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-20" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-24" })]
2402
+ }),
2403
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-2 w-full" }),
2404
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-48" })
2405
+ ]
2406
+ })] })]
2407
+ }),
2408
+ /* @__PURE__ */ jsxs("div", {
2409
+ className: "space-y-4",
2410
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-20" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-12 rounded-lg" })]
2411
+ })
2412
+ ]
2413
+ })
2414
+ });
2415
+
2280
2416
  //#endregion
2281
2417
  //#region src/ZuploMonetizationPlugin.tsx
2282
2418
  const PRICING_PATH = "/pricing";
@@ -2334,18 +2470,23 @@ const zuploMonetizationPlugin = createPlugin((options = {}) => ({
2334
2470
  {
2335
2471
  path: "/manage-payment",
2336
2472
  element: /* @__PURE__ */ jsx(ManagePaymentPage, {})
2337
- },
2338
- {
2339
- path: PRICING_PATH,
2340
- handle: { layout: "default" },
2341
- element: /* @__PURE__ */ jsx(PricingPage, {})
2342
- },
2343
- {
2344
- handle: { layout: "default" },
2345
- path: "/subscriptions",
2346
- element: /* @__PURE__ */ jsx(SubscriptionsPage, {})
2347
2473
  }
2348
2474
  ]
2475
+ }, {
2476
+ element: /* @__PURE__ */ jsx(ZuploMonetizationWrapper, { options }),
2477
+ children: [{
2478
+ path: PRICING_PATH,
2479
+ element: /* @__PURE__ */ jsx(Suspense, {
2480
+ fallback: /* @__PURE__ */ jsx(PricingPageSkeleton, {}),
2481
+ children: /* @__PURE__ */ jsx(PricingPage, {})
2482
+ })
2483
+ }, {
2484
+ path: "/subscriptions",
2485
+ element: /* @__PURE__ */ jsx(Suspense, {
2486
+ fallback: /* @__PURE__ */ jsx(SubscriptionsPageSkeleton, {}),
2487
+ children: /* @__PURE__ */ jsx(SubscriptionsPage, {})
2488
+ })
2489
+ }]
2349
2490
  }];
2350
2491
  },
2351
2492
  getProtectedRoutes: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuplo/zudoku-plugin-monetization",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
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.8.3",
30
+ "happy-dom": "20.8.9",
31
31
  "react": "19.2.4",
32
32
  "react-dom": "19.2.4",
33
33
  "tsdown": "0.20.3",
34
- "zudoku": "0.73.0"
34
+ "zudoku": "0.73.1"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=19.2.0",