@monetizekit/react 0.3.0 → 0.4.0

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.cjs CHANGED
@@ -359,13 +359,14 @@ var INTERVAL_SUFFIX = {
359
359
  annually: "/yr",
360
360
  one_time: ""
361
361
  };
362
- function describePlanPrice(plan, locale) {
362
+ function describePlanPrice(plan, locale, billingCycle = "monthly") {
363
363
  const pricing = plan.pricing ?? [];
364
364
  const contactSales = (plan.tags ?? []).includes("contact_sales") || pricing.length === 0;
365
365
  if (contactSales) {
366
366
  return { headline: null, contactSales: true };
367
367
  }
368
- const base = pricing.find((t) => t.type === "flat");
368
+ const flats = pricing.filter((t) => t.type === "flat");
369
+ const base = flats.find((t) => t.interval === billingCycle) ?? flats.find((t) => t.interval === "monthly") ?? flats[0];
369
370
  const hasUsage = pricing.some((t) => t.type === "usage");
370
371
  const currency = base?.currency ?? pricing[0]?.currency ?? "USD";
371
372
  const interval = base?.interval ?? pricing[0]?.interval ?? "monthly";
@@ -377,6 +378,14 @@ function describePlanPrice(plan, locale) {
377
378
  contactSales: false
378
379
  };
379
380
  }
381
+ function annualSavingsPercent(plan) {
382
+ const flats = (plan.pricing ?? []).filter((t) => t.type === "flat");
383
+ const monthly = flats.find((t) => t.interval === "monthly");
384
+ const annual = flats.find((t) => t.interval === "annually");
385
+ if (!monthly || !annual || monthly.amount <= 0) return null;
386
+ const pct = Math.round((1 - annual.amount / (monthly.amount * 12)) * 100);
387
+ return pct > 0 ? pct : null;
388
+ }
380
389
  function describeUsageTerm(term, locale) {
381
390
  const parts = [];
382
391
  if (term.includedUnits && term.includedUnits > 0) {
@@ -498,6 +507,78 @@ function SampleNotice({ children }) {
498
507
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: children ?? SAMPLE_NOTICE_TEXT })
499
508
  ] });
500
509
  }
510
+ var trackStyle = {
511
+ display: "inline-flex",
512
+ alignSelf: "center",
513
+ gap: "0.125rem",
514
+ padding: "0.25rem",
515
+ borderRadius: "var(--mk-radius)",
516
+ background: "color-mix(in srgb, var(--mk-fg) 6%, transparent)",
517
+ fontFamily: "var(--mk-font)"
518
+ };
519
+ function optionStyle(active) {
520
+ return {
521
+ border: "none",
522
+ cursor: "pointer",
523
+ borderRadius: "var(--mk-radius)",
524
+ padding: "0.375rem 0.875rem",
525
+ fontSize: "0.8125rem",
526
+ fontWeight: 600,
527
+ display: "inline-flex",
528
+ alignItems: "center",
529
+ gap: "0.375rem",
530
+ background: active ? "var(--mk-card)" : "transparent",
531
+ color: active ? "var(--mk-card-fg)" : "var(--mk-muted)",
532
+ boxShadow: active ? "var(--mk-shadow)" : "none"
533
+ };
534
+ }
535
+ function BillingCycleToggle({
536
+ value,
537
+ onChange,
538
+ savingsPercent = 0,
539
+ monthlyLabel = "Monthly",
540
+ annuallyLabel = "Yearly"
541
+ }) {
542
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: trackStyle, role: "group", "aria-label": "Billing cycle", "data-mk-component": "billing-toggle", children: [
543
+ /* @__PURE__ */ jsxRuntime.jsx(
544
+ "button",
545
+ {
546
+ type: "button",
547
+ style: optionStyle(value === "monthly"),
548
+ "aria-pressed": value === "monthly",
549
+ onClick: () => onChange("monthly"),
550
+ children: monthlyLabel
551
+ }
552
+ ),
553
+ /* @__PURE__ */ jsxRuntime.jsxs(
554
+ "button",
555
+ {
556
+ type: "button",
557
+ style: optionStyle(value === "annually"),
558
+ "aria-pressed": value === "annually",
559
+ onClick: () => onChange("annually"),
560
+ children: [
561
+ annuallyLabel,
562
+ savingsPercent > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
563
+ "span",
564
+ {
565
+ style: {
566
+ fontSize: "0.6875rem",
567
+ fontWeight: 700,
568
+ color: "var(--mk-success)"
569
+ },
570
+ children: [
571
+ "Save ",
572
+ savingsPercent,
573
+ "%"
574
+ ]
575
+ }
576
+ ) : null
577
+ ]
578
+ }
579
+ )
580
+ ] });
581
+ }
501
582
  var wrapperStyle = {
502
583
  display: "grid",
503
584
  gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))",
@@ -517,6 +598,8 @@ var cardBase = {
517
598
  function PricingTable({
518
599
  plans: plansProp,
519
600
  highlightPlan,
601
+ billingCycle,
602
+ showBillingToggle = false,
520
603
  locale,
521
604
  onSelectPlan,
522
605
  onContactSales,
@@ -526,6 +609,10 @@ function PricingTable({
526
609
  const { client, tokens } = useMonetizeKit();
527
610
  const [plans, setPlans] = react.useState(plansProp ?? null);
528
611
  const [error, setError] = react.useState(null);
612
+ const [cycle, setCycle] = react.useState(billingCycle ?? "monthly");
613
+ react.useEffect(() => {
614
+ if (billingCycle) setCycle(billingCycle);
615
+ }, [billingCycle]);
529
616
  react.useEffect(() => {
530
617
  if (plansProp) {
531
618
  setPlans(plansProp);
@@ -560,8 +647,19 @@ function PricingTable({
560
647
  "data-mk-sample": isSample ? "true" : void 0,
561
648
  children: [
562
649
  isSample ? /* @__PURE__ */ jsxRuntime.jsx(SampleNotice, { children: disclaimer }) : null,
650
+ showBillingToggle ? /* @__PURE__ */ jsxRuntime.jsx(
651
+ BillingCycleToggle,
652
+ {
653
+ value: cycle,
654
+ onChange: setCycle,
655
+ savingsPercent: Math.max(
656
+ 0,
657
+ ...effectivePlans.map((p) => annualSavingsPercent(p) ?? 0)
658
+ )
659
+ }
660
+ ) : null,
563
661
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: wrapperStyle, children: effectivePlans.map((plan) => {
564
- const price = describePlanPrice(plan, locale);
662
+ const price = describePlanPrice(plan, locale, cycle);
565
663
  const highlighted = highlightPlan != null && plan.name.toLowerCase() === highlightPlan.toLowerCase();
566
664
  return /* @__PURE__ */ jsxRuntime.jsxs(
567
665
  "div",
@@ -626,6 +724,169 @@ function PricingTable({
626
724
  }
627
725
  );
628
726
  }
727
+ var UNLIMITED_THRESHOLD = 9999;
728
+ function entitlementCell(ent, locale) {
729
+ if (!ent) return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "var(--mk-muted)" }, children: "\u2014" });
730
+ switch (ent.type) {
731
+ case "boolean":
732
+ return ent.value ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-label": "Included", style: { color: "var(--mk-success)", fontWeight: 700 }, children: "\u2713" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-label": "Not included", style: { color: "var(--mk-muted)" }, children: "\u2014" });
733
+ case "limit": {
734
+ const n = Number(ent.value);
735
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { children: n >= UNLIMITED_THRESHOLD ? "Unlimited" : formatUnits(n, locale) });
736
+ }
737
+ default:
738
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { children: String(ent.value) });
739
+ }
740
+ }
741
+ function deriveGroups(plans) {
742
+ const seen = /* @__PURE__ */ new Map();
743
+ for (const plan of plans) {
744
+ for (const ent of plan.entitlements ?? []) {
745
+ if (!seen.has(ent.featureKey)) seen.set(ent.featureKey, ent.featureDisplayName);
746
+ }
747
+ }
748
+ return [
749
+ {
750
+ title: "Features",
751
+ features: Array.from(seen, ([key, label]) => ({ key, label }))
752
+ }
753
+ ];
754
+ }
755
+ var cellStyle = {
756
+ padding: "0.75rem 1rem",
757
+ textAlign: "center",
758
+ fontSize: "0.875rem",
759
+ borderTop: "1px solid var(--mk-border)"
760
+ };
761
+ function PricingComparison({
762
+ plans: plansProp,
763
+ groups,
764
+ highlightPlan,
765
+ billingCycle = "monthly",
766
+ locale,
767
+ sampleWhenEmpty = true,
768
+ disclaimer
769
+ }) {
770
+ const { client, tokens } = useMonetizeKit();
771
+ const [plans, setPlans] = react.useState(plansProp ?? null);
772
+ const [error, setError] = react.useState(null);
773
+ react.useEffect(() => {
774
+ if (plansProp) {
775
+ setPlans(plansProp);
776
+ return;
777
+ }
778
+ let active = true;
779
+ client.listPlans().then((res) => {
780
+ if (active) setPlans(res.data ?? []);
781
+ }).catch((e) => {
782
+ if (active) setError(e instanceof Error ? e : new Error(String(e)));
783
+ });
784
+ return () => {
785
+ active = false;
786
+ };
787
+ }, [client, plansProp]);
788
+ if (error) {
789
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "alert", style: { color: "var(--mk-muted)" }, children: "Unable to load plan comparison." });
790
+ }
791
+ if (!plans) {
792
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-busy": "true", style: { color: "var(--mk-muted)" }, children: "Loading comparison\u2026" });
793
+ }
794
+ const isSample = plans.length === 0 && sampleWhenEmpty;
795
+ const effectivePlans = isSample ? SAMPLE_PLANS : plans;
796
+ if (effectivePlans.length === 0) {
797
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: "var(--mk-muted)" }, children: "No plans to compare." });
798
+ }
799
+ const effectiveGroups = groups ?? deriveGroups(effectivePlans);
800
+ const entByPlan = effectivePlans.map(
801
+ (plan) => new Map((plan.entitlements ?? []).map((e) => [e.featureKey, e]))
802
+ );
803
+ return /* @__PURE__ */ jsxRuntime.jsxs(
804
+ "div",
805
+ {
806
+ style: { ...tokensToStyle(tokens), display: "flex", flexDirection: "column", gap: "1rem" },
807
+ "data-mk-component": "pricing-comparison",
808
+ "data-mk-sample": isSample ? "true" : void 0,
809
+ children: [
810
+ isSample ? /* @__PURE__ */ jsxRuntime.jsx(SampleNotice, { children: disclaimer }) : null,
811
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { overflowX: "auto" }, children: /* @__PURE__ */ jsxRuntime.jsxs(
812
+ "table",
813
+ {
814
+ style: {
815
+ width: "100%",
816
+ borderCollapse: "collapse",
817
+ background: "var(--mk-bg)",
818
+ color: "var(--mk-fg)",
819
+ fontFamily: "var(--mk-font)"
820
+ },
821
+ children: [
822
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
823
+ /* @__PURE__ */ jsxRuntime.jsx("th", { style: { ...cellStyle, textAlign: "left", borderTop: "none" }, children: "Features" }),
824
+ effectivePlans.map((plan) => {
825
+ const highlighted = highlightPlan != null && plan.name.toLowerCase() === highlightPlan.toLowerCase();
826
+ const price = describePlanPrice(plan, locale, billingCycle);
827
+ return /* @__PURE__ */ jsxRuntime.jsxs(
828
+ "th",
829
+ {
830
+ style: {
831
+ ...cellStyle,
832
+ borderTop: "none",
833
+ color: highlighted ? "var(--mk-primary)" : "var(--mk-fg)"
834
+ },
835
+ "data-mk-plan": plan.name,
836
+ "data-mk-highlighted": highlighted ? "true" : void 0,
837
+ children: [
838
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 700 }, children: plan.name }),
839
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: "0.75rem", color: "var(--mk-muted)", fontWeight: 400 }, children: price.contactSales ? "Custom" : price.headline })
840
+ ]
841
+ },
842
+ plan.id
843
+ );
844
+ })
845
+ ] }) }),
846
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: effectiveGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsx(
847
+ FeatureGroupRows,
848
+ {
849
+ group,
850
+ planCount: effectivePlans.length,
851
+ entByPlan,
852
+ locale
853
+ },
854
+ group.title
855
+ )) })
856
+ ]
857
+ }
858
+ ) })
859
+ ]
860
+ }
861
+ );
862
+ }
863
+ function FeatureGroupRows({
864
+ group,
865
+ planCount,
866
+ entByPlan,
867
+ locale
868
+ }) {
869
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
870
+ /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
871
+ "td",
872
+ {
873
+ colSpan: planCount + 1,
874
+ style: {
875
+ padding: "0.625rem 1rem",
876
+ fontSize: "0.8125rem",
877
+ fontWeight: 700,
878
+ background: "color-mix(in srgb, var(--mk-fg) 5%, transparent)",
879
+ borderTop: "1px solid var(--mk-border)"
880
+ },
881
+ children: group.title
882
+ }
883
+ ) }),
884
+ group.features.map((feature) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
885
+ /* @__PURE__ */ jsxRuntime.jsx("td", { style: { ...cellStyle, textAlign: "left", color: "var(--mk-fg)" }, children: feature.label }),
886
+ entByPlan.map((map, i) => /* @__PURE__ */ jsxRuntime.jsx("td", { style: cellStyle, children: entitlementCell(map.get(feature.key), locale) }, i))
887
+ ] }, feature.key))
888
+ ] });
889
+ }
629
890
  var overlayStyle = {
630
891
  border: "1px solid var(--mk-border)",
631
892
  borderRadius: "var(--mk-radius)",
@@ -645,14 +906,15 @@ function Paywall({
645
906
  title = "Upgrade to unlock this feature",
646
907
  description = "This feature isn't included in your current plan.",
647
908
  ctaLabel = "Upgrade",
648
- onUpgrade
909
+ onUpgrade,
910
+ sample = false
649
911
  }) {
650
912
  const { allowed, loading } = useEntitlement(feature);
651
913
  const { tokens } = useMonetizeKit();
652
- if (loading) {
914
+ if (!sample && loading) {
653
915
  return /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-busy": "true", style: { color: "var(--mk-muted)" }, children: "Checking access\u2026" });
654
916
  }
655
- if (allowed) {
917
+ if (!sample && allowed) {
656
918
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
657
919
  }
658
920
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...tokensToStyle(tokens), ...overlayStyle }, "data-mk-component": "paywall", children: [
@@ -731,6 +993,94 @@ function UsageBanner({
731
993
  over ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "var(--mk-danger)", fontSize: "0.75rem" }, children: "Over included allotment \u2014 overage billed per usage pricing." }) : null
732
994
  ] });
733
995
  }
996
+ var VARIANT_COLOR = {
997
+ info: "var(--mk-accent)",
998
+ warning: "var(--mk-warning)",
999
+ danger: "var(--mk-danger)",
1000
+ neutral: "var(--mk-muted)"
1001
+ };
1002
+ function actionStyle(variant, accent) {
1003
+ if (variant === "ghost") {
1004
+ return {
1005
+ background: "transparent",
1006
+ color: "var(--mk-muted)",
1007
+ border: "none",
1008
+ cursor: "pointer",
1009
+ fontSize: "0.8125rem",
1010
+ fontWeight: 600,
1011
+ padding: "0.375rem 0.5rem"
1012
+ };
1013
+ }
1014
+ return {
1015
+ background: accent,
1016
+ color: "var(--mk-card)",
1017
+ border: "none",
1018
+ borderRadius: "var(--mk-radius)",
1019
+ cursor: "pointer",
1020
+ fontSize: "0.8125rem",
1021
+ fontWeight: 600,
1022
+ padding: "0.375rem 0.75rem"
1023
+ };
1024
+ }
1025
+ function AlertBanner({
1026
+ variant = "info",
1027
+ title,
1028
+ description,
1029
+ progress,
1030
+ actions = [],
1031
+ icon
1032
+ }) {
1033
+ const { tokens } = useMonetizeKit();
1034
+ const accent = VARIANT_COLOR[variant];
1035
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1036
+ "div",
1037
+ {
1038
+ role: "status",
1039
+ "data-mk-component": "alert-banner",
1040
+ "data-mk-variant": variant,
1041
+ style: {
1042
+ ...tokensToStyle(tokens),
1043
+ display: "flex",
1044
+ gap: "0.75rem",
1045
+ alignItems: "flex-start",
1046
+ border: `1px solid color-mix(in srgb, ${accent} 35%, var(--mk-border))`,
1047
+ background: `color-mix(in srgb, ${accent} 8%, var(--mk-card))`,
1048
+ color: "var(--mk-card-fg)",
1049
+ borderRadius: "var(--mk-radius)",
1050
+ padding: "0.875rem 1rem",
1051
+ fontFamily: "var(--mk-font)"
1052
+ },
1053
+ children: [
1054
+ icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: { color: accent, fontSize: "1.1rem", lineHeight: 1 }, children: icon }) : null,
1055
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: "0.5rem" }, children: [
1056
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 600, fontSize: "0.875rem" }, children: title }),
1057
+ description ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: "0.8125rem", color: "var(--mk-muted)" }, children: description }) : null,
1058
+ typeof progress === "number" ? /* @__PURE__ */ jsxRuntime.jsx(
1059
+ "div",
1060
+ {
1061
+ style: { height: 6, borderRadius: 999, background: "var(--mk-border)", overflow: "hidden" },
1062
+ role: "progressbar",
1063
+ "aria-valuenow": Math.round(Math.min(1, Math.max(0, progress)) * 100),
1064
+ "aria-valuemin": 0,
1065
+ "aria-valuemax": 100,
1066
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: `${Math.min(1, Math.max(0, progress)) * 100}%`, height: "100%", background: accent } })
1067
+ }
1068
+ ) : null,
1069
+ actions.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "0.5rem" }, children: actions.map((action) => /* @__PURE__ */ jsxRuntime.jsx(
1070
+ "button",
1071
+ {
1072
+ type: "button",
1073
+ onClick: action.onClick,
1074
+ style: actionStyle(action.variant, accent),
1075
+ children: action.label
1076
+ },
1077
+ action.label
1078
+ )) }) : null
1079
+ ] })
1080
+ ]
1081
+ }
1082
+ );
1083
+ }
734
1084
  var containerStyle = {
735
1085
  border: "1px solid var(--mk-border)",
736
1086
  borderRadius: "var(--mk-radius)",
@@ -963,11 +1313,14 @@ function EntitlementGate({
963
1313
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: allowed ? children : fallback });
964
1314
  }
965
1315
 
1316
+ exports.AlertBanner = AlertBanner;
1317
+ exports.BillingCycleToggle = BillingCycleToggle;
966
1318
  exports.CustomerPortal = CustomerPortal;
967
1319
  exports.EntitlementGate = EntitlementGate;
968
1320
  exports.MonetizeKitClient = MonetizeKitClient;
969
1321
  exports.MonetizeKitProvider = MonetizeKitProvider;
970
1322
  exports.Paywall = Paywall;
1323
+ exports.PricingComparison = PricingComparison;
971
1324
  exports.PricingTable = PricingTable;
972
1325
  exports.SAMPLE_CREDITS = SAMPLE_CREDITS;
973
1326
  exports.SAMPLE_INVOICES = SAMPLE_INVOICES;