@zuplo/zudoku-plugin-monetization 0.0.27 → 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 +1 -1
- package/dist/index.mjs +300 -137
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
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";
|
|
@@ -78,6 +79,16 @@ const useDeploymentName = () => {
|
|
|
78
79
|
return deploymentName;
|
|
79
80
|
};
|
|
80
81
|
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/hooks/usePurchaseDetails.ts
|
|
84
|
+
const usePurchaseDetails = (planId) => {
|
|
85
|
+
const zudoku = useZudoku();
|
|
86
|
+
return useSuspenseQuery({
|
|
87
|
+
queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/plans/${planId}/purchase-details`],
|
|
88
|
+
meta: { context: zudoku }
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
81
92
|
//#endregion
|
|
82
93
|
//#region src/MonetizationContext.tsx
|
|
83
94
|
const MonetizationContext = createContext({});
|
|
@@ -215,6 +226,24 @@ const getPriceFromPlan = (plan) => {
|
|
|
215
226
|
};
|
|
216
227
|
};
|
|
217
228
|
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/utils/purchaseDetails.ts
|
|
231
|
+
const getPlanFromPurchaseDetails = (response) => {
|
|
232
|
+
return response;
|
|
233
|
+
};
|
|
234
|
+
const getTaxAmountFromPurchaseDetails = (response) => {
|
|
235
|
+
const taxAmount = response?.tax?.taxAmount;
|
|
236
|
+
const numericAmount = typeof taxAmount === "number" ? taxAmount : Number.parseFloat(taxAmount ?? "");
|
|
237
|
+
if (!Number.isFinite(numericAmount)) return;
|
|
238
|
+
return numericAmount;
|
|
239
|
+
};
|
|
240
|
+
const getTaxLabelFromPurchaseDetails = (response) => {
|
|
241
|
+
return (response.tax?.taxes ?? []).some((tax) => tax.taxType?.toLowerCase() === "vat") ? "VAT" : "tax";
|
|
242
|
+
};
|
|
243
|
+
const isTaxInclusiveFromPurchaseDetails = (response) => {
|
|
244
|
+
return response.tax?.taxInclusive === true;
|
|
245
|
+
};
|
|
246
|
+
|
|
218
247
|
//#endregion
|
|
219
248
|
//#region src/ZuploMonetizationWrapper.tsx
|
|
220
249
|
const DEFAULT_GATEWAY_URL = "https://api.zuploedge.com";
|
|
@@ -289,15 +318,6 @@ const ZuploMonetizationWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(Query
|
|
|
289
318
|
|
|
290
319
|
//#endregion
|
|
291
320
|
//#region src/pages/CheckoutConfirmPage.tsx
|
|
292
|
-
const getPlanFromPurchaseDetails = (response) => {
|
|
293
|
-
return "plan" in response ? response.plan : response;
|
|
294
|
-
};
|
|
295
|
-
const getTaxAmountFromPurchaseDetails = (response) => {
|
|
296
|
-
const taxAmount = response?.tax?.amount;
|
|
297
|
-
const numericAmount = typeof taxAmount === "number" ? taxAmount : Number.parseFloat(taxAmount ?? "");
|
|
298
|
-
if (!Number.isFinite(numericAmount)) return;
|
|
299
|
-
return numericAmount;
|
|
300
|
-
};
|
|
301
321
|
const CheckoutConfirmPage = () => {
|
|
302
322
|
const [search] = useSearchParams();
|
|
303
323
|
const planId = search.get("planId");
|
|
@@ -306,12 +326,11 @@ const CheckoutConfirmPage = () => {
|
|
|
306
326
|
const navigate = useNavigate();
|
|
307
327
|
const { pricing } = useMonetizationConfig();
|
|
308
328
|
if (!planId) throw new Error("Parameter `planId` missing");
|
|
309
|
-
const purchaseDetails =
|
|
310
|
-
queryKey: [`/v3/zudoku-metering/${deploymentName}/plans/${planId}/purchase-details`],
|
|
311
|
-
meta: { context: zudoku }
|
|
312
|
-
});
|
|
329
|
+
const purchaseDetails = usePurchaseDetails(planId);
|
|
313
330
|
const selectedPlan = getPlanFromPurchaseDetails(purchaseDetails.data);
|
|
314
331
|
const taxAmount = getTaxAmountFromPurchaseDetails(purchaseDetails.data);
|
|
332
|
+
const taxLabel = getTaxLabelFromPurchaseDetails(purchaseDetails.data);
|
|
333
|
+
const taxInclusive = isTaxInclusiveFromPurchaseDetails(purchaseDetails.data);
|
|
315
334
|
const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
|
|
316
335
|
const { quotas, features } = categorizeRateCards(rateCards ?? [], {
|
|
317
336
|
currency: selectedPlan?.currency,
|
|
@@ -396,13 +415,9 @@ const CheckoutConfirmPage = () => {
|
|
|
396
415
|
className: "text-sm text-muted-foreground font-normal",
|
|
397
416
|
children: ["Billed ", formatBillingCycle(billingCycle)]
|
|
398
417
|
}),
|
|
399
|
-
taxAmount != null && /* @__PURE__ */
|
|
418
|
+
taxAmount != null && /* @__PURE__ */ jsx("div", {
|
|
400
419
|
className: "text-xs text-muted-foreground font-normal mt-1",
|
|
401
|
-
children:
|
|
402
|
-
"+ ",
|
|
403
|
-
formatPrice(taxAmount, selectedPlan?.currency),
|
|
404
|
-
" VAT"
|
|
405
|
-
]
|
|
420
|
+
children: taxInclusive ? `${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel} included` : `+ ${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel}`
|
|
406
421
|
})
|
|
407
422
|
]
|
|
408
423
|
}),
|
|
@@ -781,20 +796,55 @@ const PricingPage = () => {
|
|
|
781
796
|
});
|
|
782
797
|
};
|
|
783
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
|
+
|
|
784
830
|
//#endregion
|
|
785
831
|
//#region src/pages/SubscriptionChangeConfirmPage.tsx
|
|
786
832
|
const SubscriptionChangeConfirmPage = () => {
|
|
787
833
|
const [search] = useSearchParams();
|
|
788
834
|
const planId = search.get("planId");
|
|
789
835
|
const subscriptionId = search.get("subscriptionId");
|
|
836
|
+
const mode = search.get("mode");
|
|
790
837
|
const zudoku = useZudoku();
|
|
791
838
|
const deploymentName = useDeploymentName();
|
|
792
839
|
const navigate = useNavigate();
|
|
793
|
-
const { data: plans } = usePlans();
|
|
794
840
|
const { pricing } = useMonetizationConfig();
|
|
795
|
-
const selectedPlan = plans?.items?.find((plan) => plan.id === planId);
|
|
796
841
|
if (!planId) throw new Error("Parameter `planId` missing");
|
|
797
842
|
if (!subscriptionId) throw new Error("Parameter `subscriptionId` missing");
|
|
843
|
+
const purchaseDetails = usePurchaseDetails(planId);
|
|
844
|
+
const selectedPlan = getPlanFromPurchaseDetails(purchaseDetails.data);
|
|
845
|
+
const taxAmount = getTaxAmountFromPurchaseDetails(purchaseDetails.data);
|
|
846
|
+
const taxLabel = getTaxLabelFromPurchaseDetails(purchaseDetails.data);
|
|
847
|
+
const taxInclusive = isTaxInclusiveFromPurchaseDetails(purchaseDetails.data);
|
|
798
848
|
const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
|
|
799
849
|
const { quotas, features } = categorizeRateCards(rateCards ?? [], {
|
|
800
850
|
currency: selectedPlan?.currency,
|
|
@@ -803,6 +853,7 @@ const SubscriptionChangeConfirmPage = () => {
|
|
|
803
853
|
});
|
|
804
854
|
const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
|
|
805
855
|
const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
|
|
856
|
+
const effectiveChangeMessage = mode === "downgrade" ? "This change will take effect at the start of your next billing cycle." : "This change will take effect immediately.";
|
|
806
857
|
const changeMutation = useMutation({
|
|
807
858
|
mutationKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions/${subscriptionId}/change`],
|
|
808
859
|
meta: {
|
|
@@ -837,13 +888,20 @@ const SubscriptionChangeConfirmPage = () => {
|
|
|
837
888
|
}),
|
|
838
889
|
/* @__PURE__ */ jsxs("div", {
|
|
839
890
|
className: "text-center mb-8",
|
|
840
|
-
children: [
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
891
|
+
children: [
|
|
892
|
+
/* @__PURE__ */ jsx("h1", {
|
|
893
|
+
className: "text-2xl font-bold text-card-foreground mb-3",
|
|
894
|
+
children: "Confirm plan change"
|
|
895
|
+
}),
|
|
896
|
+
/* @__PURE__ */ jsx("p", {
|
|
897
|
+
className: "text-muted-foreground text-base",
|
|
898
|
+
children: effectiveChangeMessage
|
|
899
|
+
}),
|
|
900
|
+
/* @__PURE__ */ jsx("p", {
|
|
901
|
+
className: "text-muted-foreground text-base",
|
|
902
|
+
children: "Please confirm the details below to change your subscription."
|
|
903
|
+
})
|
|
904
|
+
]
|
|
847
905
|
}),
|
|
848
906
|
selectedPlan && /* @__PURE__ */ jsxs(Card, {
|
|
849
907
|
className: "bg-muted/50",
|
|
@@ -868,13 +926,20 @@ const SubscriptionChangeConfirmPage = () => {
|
|
|
868
926
|
}),
|
|
869
927
|
price && price.monthly > 0 && /* @__PURE__ */ jsxs("div", {
|
|
870
928
|
className: "text-right",
|
|
871
|
-
children: [
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
929
|
+
children: [
|
|
930
|
+
/* @__PURE__ */ jsx("div", {
|
|
931
|
+
className: "text-2xl font-bold",
|
|
932
|
+
children: formatPrice(price.monthly, selectedPlan?.currency)
|
|
933
|
+
}),
|
|
934
|
+
billingCycle && /* @__PURE__ */ jsxs("div", {
|
|
935
|
+
className: "text-sm text-muted-foreground font-normal",
|
|
936
|
+
children: ["Billed ", formatBillingCycle(billingCycle)]
|
|
937
|
+
}),
|
|
938
|
+
taxAmount != null && /* @__PURE__ */ jsx("div", {
|
|
939
|
+
className: "text-xs text-muted-foreground font-normal mt-1",
|
|
940
|
+
children: taxInclusive ? `${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel} included` : `+ ${formatPrice(taxAmount, selectedPlan?.currency)} ${taxLabel}`
|
|
941
|
+
})
|
|
942
|
+
]
|
|
878
943
|
}),
|
|
879
944
|
price && price.monthly === 0 && /* @__PURE__ */ jsx("div", {
|
|
880
945
|
className: "text-2xl text-muted-foreground font-bold",
|
|
@@ -936,7 +1001,15 @@ const useSubscriptions = (environmentName) => {
|
|
|
936
1001
|
const zudoku = useZudoku();
|
|
937
1002
|
return useSuspenseQuery({
|
|
938
1003
|
queryKey: [`/v3/zudoku-metering/${environmentName}/subscriptions`],
|
|
939
|
-
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
|
+
})
|
|
940
1013
|
});
|
|
941
1014
|
};
|
|
942
1015
|
|
|
@@ -1428,6 +1501,23 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
|
|
|
1428
1501
|
|
|
1429
1502
|
//#endregion
|
|
1430
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
|
+
};
|
|
1431
1521
|
const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units) => {
|
|
1432
1522
|
const isUpgrade = targetIndex > currentIndex;
|
|
1433
1523
|
const currentPhase = currentPlan?.phases.at(-1);
|
|
@@ -1448,6 +1538,11 @@ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units)
|
|
|
1448
1538
|
quotas: [],
|
|
1449
1539
|
features: []
|
|
1450
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);
|
|
1451
1546
|
const quotaChanges = [];
|
|
1452
1547
|
const allQuotaKeys = new Set([...currentQuotas.map((q) => q.key), ...targetQuotas.map((q) => q.key)]);
|
|
1453
1548
|
for (const key of allQuotaKeys) {
|
|
@@ -1465,22 +1560,37 @@ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units)
|
|
|
1465
1560
|
period: target.period,
|
|
1466
1561
|
change
|
|
1467
1562
|
});
|
|
1468
|
-
} else if (target && !current)
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|
+
}
|
|
1484
1594
|
}
|
|
1485
1595
|
const featureChanges = [];
|
|
1486
1596
|
const allFeatureKeys = new Set([...currentFeatures.map((f) => f.key), ...targetFeatures.map((f) => f.key)]);
|
|
@@ -1497,20 +1607,34 @@ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units)
|
|
|
1497
1607
|
newValue: target.value ?? true,
|
|
1498
1608
|
change
|
|
1499
1609
|
});
|
|
1500
|
-
} else if (target && !current)
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
+
}
|
|
1514
1638
|
}
|
|
1515
1639
|
return {
|
|
1516
1640
|
plan: targetPlan,
|
|
@@ -1529,11 +1653,16 @@ const modeLabelMap = {
|
|
|
1529
1653
|
downgrade: "Downgrade",
|
|
1530
1654
|
private: "Switch"
|
|
1531
1655
|
};
|
|
1532
|
-
const
|
|
1656
|
+
const isSwitchPlanTarget = (value) => {
|
|
1657
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1658
|
+
if (!("subscriptionId" in value) || !("plan" in value) || !("mode" in value)) return false;
|
|
1659
|
+
return true;
|
|
1660
|
+
};
|
|
1661
|
+
const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange, isSwitching }) => {
|
|
1533
1662
|
const price = getPriceFromPlan(comparison.plan);
|
|
1534
1663
|
const isCustom = comparison.plan.key === "enterprise";
|
|
1535
1664
|
const displayPrice = price.monthly;
|
|
1536
|
-
const hasChanges = comparison.quotaChanges.
|
|
1665
|
+
const hasChanges = comparison.quotaChanges.length > 0 || comparison.featureChanges.length > 0;
|
|
1537
1666
|
return /* @__PURE__ */ jsxs("div", {
|
|
1538
1667
|
className: "border rounded-lg p-4",
|
|
1539
1668
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -1569,11 +1698,12 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
|
|
|
1569
1698
|
mode
|
|
1570
1699
|
}),
|
|
1571
1700
|
size: "sm",
|
|
1701
|
+
disabled: isSwitching,
|
|
1572
1702
|
children: modeLabelMap[mode]
|
|
1573
1703
|
})]
|
|
1574
1704
|
}), hasChanges && /* @__PURE__ */ jsxs("div", {
|
|
1575
1705
|
className: "space-y-1.5",
|
|
1576
|
-
children: [comparison.quotaChanges.
|
|
1706
|
+
children: [comparison.quotaChanges.map((quota) => /* @__PURE__ */ jsxs("div", {
|
|
1577
1707
|
className: "flex items-center gap-2 text-sm",
|
|
1578
1708
|
children: [
|
|
1579
1709
|
/* @__PURE__ */ jsx(ChangeIndicator, { change: quota.change }),
|
|
@@ -1581,7 +1711,14 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
|
|
|
1581
1711
|
className: "font-medium",
|
|
1582
1712
|
children: [quota.name, ":"]
|
|
1583
1713
|
}),
|
|
1584
|
-
quota.change === "
|
|
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", {
|
|
1585
1722
|
className: "text-green-600",
|
|
1586
1723
|
children: "Now included"
|
|
1587
1724
|
}) : quota.change === "removed" ? /* @__PURE__ */ jsx("span", {
|
|
@@ -1610,9 +1747,12 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
|
|
|
1610
1747
|
})
|
|
1611
1748
|
] })
|
|
1612
1749
|
]
|
|
1613
|
-
}, quota.key)), comparison.featureChanges.
|
|
1750
|
+
}, quota.key)), comparison.featureChanges.map((feature) => /* @__PURE__ */ jsx("div", {
|
|
1614
1751
|
className: "flex items-center gap-2 text-sm",
|
|
1615
|
-
children: feature.change === "
|
|
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: [
|
|
1616
1756
|
/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" }),
|
|
1617
1757
|
/* @__PURE__ */ jsx("span", {
|
|
1618
1758
|
className: "text-muted-foreground font-medium",
|
|
@@ -1663,24 +1803,32 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange
|
|
|
1663
1803
|
})]
|
|
1664
1804
|
});
|
|
1665
1805
|
};
|
|
1666
|
-
const
|
|
1806
|
+
const SwitchPlanModal = ({ subscription, children }) => {
|
|
1807
|
+
const [open, setOpen] = useState(false);
|
|
1808
|
+
const { data: plansData } = usePlans();
|
|
1809
|
+
const { pricing } = useMonetizationConfig();
|
|
1667
1810
|
const deploymentName = useDeploymentName();
|
|
1668
1811
|
const context = useZudoku();
|
|
1669
1812
|
const { generateUrl } = useUrlUtils();
|
|
1670
|
-
const
|
|
1813
|
+
const switchPlanMutation = useMutation({
|
|
1671
1814
|
mutationKey: [`/v3/zudoku-metering/${deploymentName}/stripe/checkout`],
|
|
1672
1815
|
meta: {
|
|
1673
1816
|
context,
|
|
1674
|
-
request: {
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1817
|
+
request: (variables) => {
|
|
1818
|
+
if (!isSwitchPlanTarget(variables)) throw new Error("Couldn't start the plan change. Please refresh and try again.");
|
|
1819
|
+
const switchTo = variables;
|
|
1820
|
+
return {
|
|
1821
|
+
method: "POST",
|
|
1822
|
+
body: JSON.stringify({
|
|
1679
1823
|
planId: switchTo.plan.id,
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1824
|
+
successURL: generateUrl(`/subscription-change-confirm`, { searchParams: {
|
|
1825
|
+
planId: switchTo.plan.id,
|
|
1826
|
+
subscriptionId: switchTo.subscriptionId,
|
|
1827
|
+
mode: switchTo.mode
|
|
1828
|
+
} }),
|
|
1829
|
+
cancelURL: generateUrl("/subscriptions", { searchParams: { subscriptionId: switchTo.subscriptionId } })
|
|
1830
|
+
})
|
|
1831
|
+
};
|
|
1684
1832
|
}
|
|
1685
1833
|
},
|
|
1686
1834
|
retry: false,
|
|
@@ -1688,39 +1836,7 @@ const ConfirmSwitchAlert = ({ switchTo, onRequestClose }) => {
|
|
|
1688
1836
|
window.location.href = data.url;
|
|
1689
1837
|
}
|
|
1690
1838
|
});
|
|
1691
|
-
|
|
1692
|
-
open: true,
|
|
1693
|
-
onOpenChange: onRequestClose,
|
|
1694
|
-
children: /* @__PURE__ */ jsxs(AlertDialogContent, { children: [/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [
|
|
1695
|
-
/* @__PURE__ */ jsxs(AlertDialogTitle, { children: [
|
|
1696
|
-
"Confirm",
|
|
1697
|
-
" ",
|
|
1698
|
-
switchTo.mode === "private" ? "plan change" : switchTo.mode === "upgrade" ? "upgrade" : "downgrade"
|
|
1699
|
-
] }),
|
|
1700
|
-
mutation.isError && /* @__PURE__ */ jsx(Alert, {
|
|
1701
|
-
variant: "destructive",
|
|
1702
|
-
children: /* @__PURE__ */ jsx(AlertDescription, {
|
|
1703
|
-
className: "first-letter:uppercase",
|
|
1704
|
-
children: mutation.error.message
|
|
1705
|
-
})
|
|
1706
|
-
}),
|
|
1707
|
-
/* @__PURE__ */ jsx(AlertDialogDescription, { children: switchTo.mode === "private" ? `Are you sure you want to switch to ${switchTo.plan.name}? This will take effect immediately.` : switchTo.mode === "upgrade" ? `Are you sure you want to upgrade to ${switchTo.plan.name}? This will take effect immediately.` : `Are you sure you want to downgrade to ${switchTo.plan.name}? This will take effect at the start of your next billing cycle.` })
|
|
1708
|
-
] }), /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [/* @__PURE__ */ jsx(AlertDialogCancel, {
|
|
1709
|
-
disabled: mutation.isPending,
|
|
1710
|
-
children: "Cancel"
|
|
1711
|
-
}), /* @__PURE__ */ jsx(ActionButton, {
|
|
1712
|
-
isPending: mutation.isPending,
|
|
1713
|
-
onClick: () => mutation.mutate(),
|
|
1714
|
-
children: modeLabelMap[switchTo.mode]
|
|
1715
|
-
})] })] })
|
|
1716
|
-
});
|
|
1717
|
-
};
|
|
1718
|
-
const SwitchPlanModal = ({ subscription, children }) => {
|
|
1719
|
-
const [open, setOpen] = useState(false);
|
|
1720
|
-
const { data: plansData } = usePlans();
|
|
1721
|
-
const [switchTo, setSwitchTo] = useState(null);
|
|
1722
|
-
const { pricing } = useMonetizationConfig();
|
|
1723
|
-
const currentPlan = plansData?.items.find((p) => p.id === subscription.plan.id);
|
|
1839
|
+
const currentPlan = plansData?.items.find((p) => p.key === subscription.plan.key);
|
|
1724
1840
|
const { upgrades, downgrades, privatePlans } = useMemo(() => {
|
|
1725
1841
|
if (!plansData?.items || !currentPlan) return {
|
|
1726
1842
|
upgrades: [],
|
|
@@ -1742,10 +1858,7 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1742
1858
|
currentPlan,
|
|
1743
1859
|
pricing?.units
|
|
1744
1860
|
]);
|
|
1745
|
-
return /* @__PURE__ */ jsxs(
|
|
1746
|
-
switchTo,
|
|
1747
|
-
onRequestClose: () => setSwitchTo(null)
|
|
1748
|
-
}), /* @__PURE__ */ jsxs(Dialog, {
|
|
1861
|
+
return /* @__PURE__ */ jsxs(Dialog, {
|
|
1749
1862
|
open,
|
|
1750
1863
|
onOpenChange: setOpen,
|
|
1751
1864
|
children: [/* @__PURE__ */ jsx(DialogTrigger, {
|
|
@@ -1766,6 +1879,13 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1766
1879
|
}), /* @__PURE__ */ jsxs("div", {
|
|
1767
1880
|
className: "mt-4 space-y-6",
|
|
1768
1881
|
children: [
|
|
1882
|
+
switchPlanMutation.isError && /* @__PURE__ */ jsx(Alert, {
|
|
1883
|
+
variant: "destructive",
|
|
1884
|
+
children: /* @__PURE__ */ jsx(AlertDescription, {
|
|
1885
|
+
className: "first-letter:uppercase",
|
|
1886
|
+
children: switchPlanMutation.error.message
|
|
1887
|
+
})
|
|
1888
|
+
}),
|
|
1769
1889
|
currentPlan && /* @__PURE__ */ jsx(Item, {
|
|
1770
1890
|
variant: "outline",
|
|
1771
1891
|
children: /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, { children: "Current Plan" }), /* @__PURE__ */ jsx(ItemDescription, {
|
|
@@ -1791,7 +1911,8 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1791
1911
|
comparison,
|
|
1792
1912
|
subscriptionId: subscription.id,
|
|
1793
1913
|
mode: "upgrade",
|
|
1794
|
-
onRequestChange:
|
|
1914
|
+
onRequestChange: (target) => switchPlanMutation.mutate(target),
|
|
1915
|
+
isSwitching: switchPlanMutation.isPending
|
|
1795
1916
|
}, comparison.plan.id))
|
|
1796
1917
|
})] }),
|
|
1797
1918
|
downgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -1812,7 +1933,8 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1812
1933
|
comparison,
|
|
1813
1934
|
subscriptionId: subscription.id,
|
|
1814
1935
|
mode: "downgrade",
|
|
1815
|
-
onRequestChange:
|
|
1936
|
+
onRequestChange: (target) => switchPlanMutation.mutate(target),
|
|
1937
|
+
isSwitching: switchPlanMutation.isPending
|
|
1816
1938
|
}, comparison.plan.id))
|
|
1817
1939
|
})] }),
|
|
1818
1940
|
privatePlans.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -1833,13 +1955,14 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1833
1955
|
comparison,
|
|
1834
1956
|
subscriptionId: subscription.id,
|
|
1835
1957
|
mode: "private",
|
|
1836
|
-
onRequestChange:
|
|
1958
|
+
onRequestChange: (target) => switchPlanMutation.mutate(target),
|
|
1959
|
+
isSwitching: switchPlanMutation.isPending
|
|
1837
1960
|
}, comparison.plan.id))
|
|
1838
1961
|
})] })
|
|
1839
1962
|
]
|
|
1840
1963
|
})]
|
|
1841
1964
|
}) })]
|
|
1842
|
-
})
|
|
1965
|
+
});
|
|
1843
1966
|
};
|
|
1844
1967
|
|
|
1845
1968
|
//#endregion
|
|
@@ -1939,7 +2062,7 @@ const ManageSubscription = ({ subscription, planName }) => {
|
|
|
1939
2062
|
const isMeteredEntitlement = (entitlement) => {
|
|
1940
2063
|
return "balance" in entitlement;
|
|
1941
2064
|
};
|
|
1942
|
-
const UsageItem = ({ meter, item, subscription }) => {
|
|
2065
|
+
const UsageItem = ({ meter, item, subscription, featureKey }) => {
|
|
1943
2066
|
const cadence = item?.billingCadence ?? subscription?.billingCadence;
|
|
1944
2067
|
const billingPeriod = cadence ? formatDurationAdjective(cadence) : "monthly";
|
|
1945
2068
|
const isSoftLimit = item?.included?.entitlement?.isSoftLimit ?? true;
|
|
@@ -1998,7 +2121,7 @@ const UsageItem = ({ meter, item, subscription }) => {
|
|
|
1998
2121
|
]
|
|
1999
2122
|
}),
|
|
2000
2123
|
/* @__PURE__ */ jsxs(CardTitle, { children: [
|
|
2001
|
-
item?.name ??
|
|
2124
|
+
item?.name ?? featureKey,
|
|
2002
2125
|
" ",
|
|
2003
2126
|
item?.price?.amount
|
|
2004
2127
|
] })
|
|
@@ -2078,6 +2201,7 @@ const Usage = ({ usage, isFetching, currentItems, subscription, isPendingFirstPa
|
|
|
2078
2201
|
]
|
|
2079
2202
|
}),
|
|
2080
2203
|
hasUsage ? Object.entries(usage.entitlements).flatMap(([key, value]) => isMeteredEntitlement(value) ? /* @__PURE__ */ jsx(UsageItem, {
|
|
2204
|
+
featureKey: key,
|
|
2081
2205
|
meter: { ...value },
|
|
2082
2206
|
subscription,
|
|
2083
2207
|
item: currentItems?.find((item) => item.featureKey === key)
|
|
@@ -2255,6 +2379,40 @@ const SubscriptionsPage = () => {
|
|
|
2255
2379
|
});
|
|
2256
2380
|
};
|
|
2257
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
|
+
|
|
2258
2416
|
//#endregion
|
|
2259
2417
|
//#region src/ZuploMonetizationPlugin.tsx
|
|
2260
2418
|
const PRICING_PATH = "/pricing";
|
|
@@ -2312,18 +2470,23 @@ const zuploMonetizationPlugin = createPlugin((options = {}) => ({
|
|
|
2312
2470
|
{
|
|
2313
2471
|
path: "/manage-payment",
|
|
2314
2472
|
element: /* @__PURE__ */ jsx(ManagePaymentPage, {})
|
|
2315
|
-
},
|
|
2316
|
-
{
|
|
2317
|
-
path: PRICING_PATH,
|
|
2318
|
-
handle: { layout: "default" },
|
|
2319
|
-
element: /* @__PURE__ */ jsx(PricingPage, {})
|
|
2320
|
-
},
|
|
2321
|
-
{
|
|
2322
|
-
handle: { layout: "default" },
|
|
2323
|
-
path: "/subscriptions",
|
|
2324
|
-
element: /* @__PURE__ */ jsx(SubscriptionsPage, {})
|
|
2325
2473
|
}
|
|
2326
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
|
+
}]
|
|
2327
2490
|
}];
|
|
2328
2491
|
},
|
|
2329
2492
|
getProtectedRoutes: () => {
|
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.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.
|
|
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.
|
|
34
|
+
"zudoku": "0.73.1"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": ">=19.2.0",
|