@kerne/react 0.1.3 → 1.1.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/README.md +18 -5
- package/dist/{chunk-F6NV76OR.js → chunk-IZMRK3WK.js} +150 -16
- package/dist/{chunk-7U3QBMA7.cjs → chunk-TYNZ2WU7.cjs} +156 -22
- package/dist/{i18n-BWTT1DWD.d.cts → i18n-QilNgwqe.d.cts} +33 -0
- package/dist/{i18n-BWTT1DWD.d.ts → i18n-QilNgwqe.d.ts} +33 -0
- package/dist/index.cjs +18 -20
- package/dist/index.d.cts +97 -13
- package/dist/index.d.ts +97 -13
- package/dist/index.js +13 -15
- package/dist/ui/index.cjs +669 -131
- package/dist/ui/index.d.cts +139 -4
- package/dist/ui/index.d.ts +139 -4
- package/dist/ui/index.js +602 -64
- package/package.json +3 -3
package/dist/ui/index.js
CHANGED
|
@@ -6,17 +6,18 @@ import {
|
|
|
6
6
|
useAccess,
|
|
7
7
|
useAuth,
|
|
8
8
|
useAuthConfig,
|
|
9
|
-
|
|
9
|
+
useCancelSubscription,
|
|
10
10
|
useClient,
|
|
11
11
|
useEntitlements,
|
|
12
12
|
useErrorResolver,
|
|
13
13
|
useLocalization,
|
|
14
|
+
usePlanSwitch,
|
|
14
15
|
usePlans,
|
|
15
16
|
usePortal,
|
|
16
17
|
useSubscription,
|
|
17
18
|
useUser,
|
|
18
19
|
useWaitlist
|
|
19
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-IZMRK3WK.js";
|
|
20
21
|
|
|
21
22
|
// src/ui/AuthFlow.tsx
|
|
22
23
|
import { useState as useState10 } from "react";
|
|
@@ -246,6 +247,7 @@ var CSS = `
|
|
|
246
247
|
|
|
247
248
|
/* ---------- Stack ---------- */
|
|
248
249
|
.kerne-stack { display: flex; flex-direction: column; gap: 16px; }
|
|
250
|
+
.kerne-inline-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
249
251
|
.kerne-form { display: flex; flex-direction: column; gap: 12px; }
|
|
250
252
|
.kerne-field { display: flex; flex-direction: column; gap: 6px; }
|
|
251
253
|
.kerne-label { font-size: 14px; font-weight: 500; color: var(--kerne-fg); }
|
|
@@ -512,6 +514,126 @@ var CSS = `
|
|
|
512
514
|
.kerne-meter-note { font-size: 12px; color: var(--kerne-fg-subtle); margin: 0; }
|
|
513
515
|
@media (prefers-reduced-motion: reduce) { .kerne-meter-fill { transition: none; } }
|
|
514
516
|
|
|
517
|
+
/* ---------- Pricing table ---------- */
|
|
518
|
+
.kerne-pricing-skeleton { width: 100%; }
|
|
519
|
+
|
|
520
|
+
.kerne-pricing-switch-wrap { display: flex; justify-content: center; }
|
|
521
|
+
.kerne-pricing-switch {
|
|
522
|
+
position: relative;
|
|
523
|
+
display: inline-flex;
|
|
524
|
+
gap: 2px;
|
|
525
|
+
padding: 4px;
|
|
526
|
+
background: var(--kerne-panel);
|
|
527
|
+
border-radius: 999px;
|
|
528
|
+
}
|
|
529
|
+
.kerne-pricing-switch-thumb {
|
|
530
|
+
position: absolute;
|
|
531
|
+
top: 4px;
|
|
532
|
+
bottom: 4px;
|
|
533
|
+
left: 0;
|
|
534
|
+
border-radius: 999px;
|
|
535
|
+
background: var(--kerne-surface);
|
|
536
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
537
|
+
transition: transform 0.32s cubic-bezier(0.16, 1, 0.3, 1), width 0.32s cubic-bezier(0.16, 1, 0.3, 1);
|
|
538
|
+
}
|
|
539
|
+
.kerne-pricing-switch-option {
|
|
540
|
+
position: relative;
|
|
541
|
+
z-index: 1;
|
|
542
|
+
display: inline-flex;
|
|
543
|
+
align-items: center;
|
|
544
|
+
gap: 7px;
|
|
545
|
+
font: inherit;
|
|
546
|
+
font-size: 13.5px;
|
|
547
|
+
font-weight: 500;
|
|
548
|
+
white-space: nowrap;
|
|
549
|
+
color: var(--kerne-fg-muted);
|
|
550
|
+
background: none;
|
|
551
|
+
border: none;
|
|
552
|
+
border-radius: 999px;
|
|
553
|
+
padding: 7px 16px;
|
|
554
|
+
cursor: pointer;
|
|
555
|
+
transition: color 0.18s ease;
|
|
556
|
+
}
|
|
557
|
+
.kerne-pricing-switch-option[aria-pressed="true"] { color: var(--kerne-fg); }
|
|
558
|
+
.kerne-pricing-switch-option:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: 2px; }
|
|
559
|
+
.kerne-pricing-switch-save {
|
|
560
|
+
font-size: 10.5px;
|
|
561
|
+
font-weight: 600;
|
|
562
|
+
letter-spacing: 0.01em;
|
|
563
|
+
color: var(--kerne-success);
|
|
564
|
+
background: color-mix(in srgb, var(--kerne-success) 14%, transparent);
|
|
565
|
+
padding: 2px 6px;
|
|
566
|
+
border-radius: 999px;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.kerne-pricing-grid {
|
|
570
|
+
display: grid;
|
|
571
|
+
grid-template-columns: repeat(auto-fit, minmax(248px, 1fr));
|
|
572
|
+
gap: 20px;
|
|
573
|
+
align-items: stretch;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.kerne-pricing-card {
|
|
577
|
+
position: relative;
|
|
578
|
+
display: flex;
|
|
579
|
+
flex-direction: column;
|
|
580
|
+
gap: 24px;
|
|
581
|
+
height: 100%;
|
|
582
|
+
padding: 28px 26px;
|
|
583
|
+
background: var(--kerne-surface);
|
|
584
|
+
border: 1px solid var(--kerne-border);
|
|
585
|
+
border-radius: calc(var(--kerne-radius) + 8px);
|
|
586
|
+
}
|
|
587
|
+
.kerne-pricing-card[data-featured="true"] {
|
|
588
|
+
background: var(--kerne-surface-raised);
|
|
589
|
+
border-color: var(--kerne-fg);
|
|
590
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 24px 48px -28px rgba(0, 0, 0, 0.22);
|
|
591
|
+
}
|
|
592
|
+
.kerne-pricing-card[data-current="true"]:not([data-featured="true"]) { border-color: var(--kerne-border-strong); }
|
|
593
|
+
|
|
594
|
+
/* A quiet inline pill, not a loud badge escaping the card border - that
|
|
595
|
+
shouted-uppercase-chip-on-the-edge look is the single most template-y
|
|
596
|
+
tell a pricing page can have. Reads as confirmation of what the card's
|
|
597
|
+
own elevation already signals, never as the primary signal itself. */
|
|
598
|
+
.kerne-pricing-badge {
|
|
599
|
+
align-self: flex-start;
|
|
600
|
+
font-size: 11.5px;
|
|
601
|
+
font-weight: 600;
|
|
602
|
+
letter-spacing: 0.01em;
|
|
603
|
+
color: var(--kerne-fg);
|
|
604
|
+
background: var(--kerne-panel);
|
|
605
|
+
padding: 3px 9px;
|
|
606
|
+
border-radius: 999px;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.kerne-pricing-card-head { display: flex; flex-direction: column; gap: 8px; }
|
|
610
|
+
.kerne-pricing-card-name { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
|
|
611
|
+
.kerne-pricing-card-desc { margin: 0; font-size: 13px; line-height: 1.5; color: var(--kerne-fg-muted); }
|
|
612
|
+
|
|
613
|
+
.kerne-pricing-price { display: flex; flex-direction: column; gap: 3px; min-height: 52px; justify-content: center; }
|
|
614
|
+
.kerne-pricing-price-row { display: flex; align-items: baseline; gap: 5px; }
|
|
615
|
+
.kerne-pricing-amount {
|
|
616
|
+
font-size: 38px;
|
|
617
|
+
font-weight: 600;
|
|
618
|
+
letter-spacing: -0.025em;
|
|
619
|
+
line-height: 1;
|
|
620
|
+
font-variant-numeric: tabular-nums;
|
|
621
|
+
}
|
|
622
|
+
.kerne-pricing-interval { font-size: 14px; color: var(--kerne-fg-muted); }
|
|
623
|
+
.kerne-pricing-billed { margin: 0; font-size: 12px; color: var(--kerne-fg-subtle); }
|
|
624
|
+
|
|
625
|
+
.kerne-pricing-features { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 11px; flex: 1; }
|
|
626
|
+
.kerne-pricing-feature { display: flex; align-items: flex-start; gap: 9px; font-size: 13.5px; line-height: 1.4; color: var(--kerne-fg); }
|
|
627
|
+
.kerne-pricing-feature svg { flex-shrink: 0; margin-top: 2px; color: var(--kerne-fg-subtle); }
|
|
628
|
+
.kerne-pricing-feature-more { font-size: 12.5px; color: var(--kerne-fg-subtle); padding-left: 24px; }
|
|
629
|
+
|
|
630
|
+
.kerne-pricing-cta { margin-top: auto; }
|
|
631
|
+
.kerne-pricing-cta .kerne-button { width: 100%; }
|
|
632
|
+
|
|
633
|
+
@media (prefers-reduced-motion: reduce) {
|
|
634
|
+
.kerne-pricing-switch-thumb { transition: none; }
|
|
635
|
+
}
|
|
636
|
+
|
|
515
637
|
/* ---------- Sections (profile, settings) ---------- */
|
|
516
638
|
.kerne-section-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
|
|
517
639
|
.kerne-section-title { font-size: 15px; font-weight: 600; margin: 0; }
|
|
@@ -2677,15 +2799,83 @@ function formatInterval(interval, labels) {
|
|
|
2677
2799
|
return interval;
|
|
2678
2800
|
}
|
|
2679
2801
|
}
|
|
2802
|
+
var ENCODED_INTERVAL_UNIT = { D: 12 / 365, M: 1, Y: 12 };
|
|
2803
|
+
function approxMonths(value) {
|
|
2804
|
+
const match = ENCODED_INTERVAL.exec(value);
|
|
2805
|
+
if (!match) return 0;
|
|
2806
|
+
return parseInt(match[1], 10) * ENCODED_INTERVAL_UNIT[match[2]];
|
|
2807
|
+
}
|
|
2808
|
+
function resolvePriceDisplay(price, displayInterval) {
|
|
2809
|
+
if (!displayInterval || displayInterval === price.interval) {
|
|
2810
|
+
return {
|
|
2811
|
+
billedAmount: price.amount,
|
|
2812
|
+
billedInterval: price.interval,
|
|
2813
|
+
displayAmount: price.amount,
|
|
2814
|
+
displayInterval: price.interval
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
const ratio = approxMonths(displayInterval) / approxMonths(price.interval);
|
|
2818
|
+
return {
|
|
2819
|
+
billedAmount: price.amount,
|
|
2820
|
+
billedInterval: price.interval,
|
|
2821
|
+
displayAmount: Math.round(price.amount * ratio),
|
|
2822
|
+
displayInterval
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
function collectPriceLabels(plans) {
|
|
2826
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
2827
|
+
for (const plan of plans) {
|
|
2828
|
+
for (const price of plan.prices) {
|
|
2829
|
+
if (!bySlug.has(price.label.slug)) bySlug.set(price.label.slug, price.label);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
return Array.from(bySlug.values()).sort(
|
|
2833
|
+
(a, b) => a.sort_order - b.sort_order || a.name.localeCompare(b.name)
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2836
|
+
function resolveLabelSavings(plans, labels) {
|
|
2837
|
+
if (labels.length < 2) return null;
|
|
2838
|
+
const priceFor = (labelSlug) => {
|
|
2839
|
+
for (const plan of plans) {
|
|
2840
|
+
const price = plan.prices.find((p) => p.label.slug === labelSlug);
|
|
2841
|
+
if (price) return price;
|
|
2842
|
+
}
|
|
2843
|
+
return void 0;
|
|
2844
|
+
};
|
|
2845
|
+
const sorted = [...labels].sort(
|
|
2846
|
+
(a, b) => approxMonths(priceFor(a.slug)?.interval ?? "1M") - approxMonths(priceFor(b.slug)?.interval ?? "1M")
|
|
2847
|
+
);
|
|
2848
|
+
const shortest = sorted[0];
|
|
2849
|
+
const longest = sorted[sorted.length - 1];
|
|
2850
|
+
if (shortest.slug === longest.slug) return null;
|
|
2851
|
+
for (const plan of plans) {
|
|
2852
|
+
const shortPrice = plan.prices.find((p) => p.label.slug === shortest.slug);
|
|
2853
|
+
const longPrice = plan.prices.find((p) => p.label.slug === longest.slug);
|
|
2854
|
+
if (!shortPrice?.amount || !longPrice) continue;
|
|
2855
|
+
const shortMonths = approxMonths(shortPrice.interval);
|
|
2856
|
+
const longMonths = approxMonths(longPrice.interval);
|
|
2857
|
+
if (!shortMonths || !longMonths) continue;
|
|
2858
|
+
const savings = 1 - longPrice.amount / longMonths / (shortPrice.amount / shortMonths);
|
|
2859
|
+
if (savings > 0.01) return { labelSlug: longest.slug, percent: Math.round(savings * 100) };
|
|
2860
|
+
}
|
|
2861
|
+
return null;
|
|
2862
|
+
}
|
|
2680
2863
|
|
|
2681
2864
|
// src/ui/UsageMeters.tsx
|
|
2682
|
-
import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2683
|
-
function
|
|
2684
|
-
|
|
2865
|
+
import { Fragment as Fragment8, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2866
|
+
function isSoftOverage(overage) {
|
|
2867
|
+
return overage === "ALLOW" || overage === "BILL";
|
|
2868
|
+
}
|
|
2869
|
+
function levelFor(ratio, softOverage) {
|
|
2870
|
+
if (softOverage) return ratio >= 1 ? "warning" : "normal";
|
|
2685
2871
|
if (ratio >= 0.9) return "critical";
|
|
2686
2872
|
if (ratio >= 0.75) return "warning";
|
|
2687
2873
|
return "normal";
|
|
2688
2874
|
}
|
|
2875
|
+
function quotaRatio(used, limit) {
|
|
2876
|
+
if (limit <= 0) return used > 0 ? 1 : 0;
|
|
2877
|
+
return Math.min(used / limit, 1);
|
|
2878
|
+
}
|
|
2689
2879
|
function UsageMeters({
|
|
2690
2880
|
title,
|
|
2691
2881
|
featureKeys,
|
|
@@ -2739,10 +2929,13 @@ function UsageMeters({
|
|
|
2739
2929
|
return shell(
|
|
2740
2930
|
/* @__PURE__ */ jsx15("div", { className: "kerne-meters", children: rows.map((e) => {
|
|
2741
2931
|
const unlimited = e.limit === null || e.limit === void 0;
|
|
2742
|
-
const
|
|
2743
|
-
const
|
|
2744
|
-
const
|
|
2932
|
+
const softOverage = isSoftOverage(e.overage_behavior);
|
|
2933
|
+
const billed = e.overage_behavior === "BILL";
|
|
2934
|
+
const ratio = unlimited ? 0 : quotaRatio(e.used, e.limit);
|
|
2935
|
+
const level = unlimited ? "normal" : levelFor(ratio, softOverage);
|
|
2745
2936
|
const over = !unlimited && e.used > e.limit;
|
|
2937
|
+
const standing = billed ? subscription?.entitlements.find((se) => se.feature_key === e.feature_key) : void 0;
|
|
2938
|
+
const capReached = billed && (standing?.cap_reached ?? false);
|
|
2746
2939
|
return /* @__PURE__ */ jsxs14("div", { className: "kerne-meter", "data-level": level, children: [
|
|
2747
2940
|
/* @__PURE__ */ jsxs14("div", { className: "kerne-meter-head", children: [
|
|
2748
2941
|
/* @__PURE__ */ jsx15("span", { className: "kerne-meter-name", children: nameFor(e.feature_key) }),
|
|
@@ -2760,9 +2953,18 @@ function UsageMeters({
|
|
|
2760
2953
|
children: /* @__PURE__ */ jsx15("div", { className: "kerne-meter-fill", style: { width: `${ratio * 100}%` } })
|
|
2761
2954
|
}
|
|
2762
2955
|
) : null,
|
|
2763
|
-
over &&
|
|
2764
|
-
|
|
2765
|
-
|
|
2956
|
+
over && softOverage ? /* @__PURE__ */ jsxs14(Fragment8, { children: [
|
|
2957
|
+
/* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: fill(
|
|
2958
|
+
// capReached inverts overageBilled's meaning (billing stopped,
|
|
2959
|
+
// access didn't) - a variant string would say the opposite of
|
|
2960
|
+
// the truth for the one plan that just stopped costing money.
|
|
2961
|
+
capReached ? l10n.usage.capReached : billed ? l10n.usage.overageBilled : l10n.usage.overage,
|
|
2962
|
+
{ count: formatNumber(e.used - e.limit) }
|
|
2963
|
+
) }),
|
|
2964
|
+
billed && standing?.billed_amount != null && standing.currency ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note kerne-meter-cost", children: fill(l10n.usage.billedAmount, {
|
|
2965
|
+
amount: formatMoney(standing.billed_amount, standing.currency)
|
|
2966
|
+
}) }) : null
|
|
2967
|
+
] }) : over ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: l10n.usage.limitReached }) : null
|
|
2766
2968
|
] }, e.feature_key);
|
|
2767
2969
|
}) })
|
|
2768
2970
|
);
|
|
@@ -2770,7 +2972,7 @@ function UsageMeters({
|
|
|
2770
2972
|
|
|
2771
2973
|
// src/ui/SubscriptionCard.tsx
|
|
2772
2974
|
import { useState as useState16 } from "react";
|
|
2773
|
-
import { Fragment as
|
|
2975
|
+
import { Fragment as Fragment9, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
2774
2976
|
var STATUS_TONES = {
|
|
2775
2977
|
ACTIVE: "success",
|
|
2776
2978
|
TRIALING: "success",
|
|
@@ -2786,11 +2988,13 @@ function SubscriptionCard({
|
|
|
2786
2988
|
upgradeUrl,
|
|
2787
2989
|
onUpgradeClick,
|
|
2788
2990
|
hidePortalButton = false,
|
|
2991
|
+
hideCancelButton = false,
|
|
2789
2992
|
appearance,
|
|
2790
2993
|
className
|
|
2791
2994
|
}) {
|
|
2792
|
-
const { subscription, isLoading, error } = useSubscription(productSlug);
|
|
2995
|
+
const { subscription, isLoading, error, refetch } = useSubscription(productSlug);
|
|
2793
2996
|
const { openPortal } = usePortal();
|
|
2997
|
+
const { cancelSubscription, isCanceling } = useCancelSubscription();
|
|
2794
2998
|
const l10n = useLocalization();
|
|
2795
2999
|
const showError = useErrorResolver();
|
|
2796
3000
|
const heading = title === null ? null : title ?? l10n.billing.plan;
|
|
@@ -2804,6 +3008,8 @@ function SubscriptionCard({
|
|
|
2804
3008
|
};
|
|
2805
3009
|
const [opening, setOpening] = useState16(false);
|
|
2806
3010
|
const [portalError, setPortalError] = useState16(null);
|
|
3011
|
+
const [confirmingCancel, setConfirmingCancel] = useState16(false);
|
|
3012
|
+
const [cancelError, setCancelError] = useState16(null);
|
|
2807
3013
|
const handlePortal = async () => {
|
|
2808
3014
|
setPortalError(null);
|
|
2809
3015
|
setOpening(true);
|
|
@@ -2814,6 +3020,17 @@ function SubscriptionCard({
|
|
|
2814
3020
|
setOpening(false);
|
|
2815
3021
|
}
|
|
2816
3022
|
};
|
|
3023
|
+
const handleCancel = async () => {
|
|
3024
|
+
if (!subscription) return;
|
|
3025
|
+
setCancelError(null);
|
|
3026
|
+
try {
|
|
3027
|
+
await cancelSubscription(subscription.id, false);
|
|
3028
|
+
setConfirmingCancel(false);
|
|
3029
|
+
refetch();
|
|
3030
|
+
} catch (e) {
|
|
3031
|
+
setCancelError(showError(e));
|
|
3032
|
+
}
|
|
3033
|
+
};
|
|
2817
3034
|
const shell = (children) => /* @__PURE__ */ jsx16(Shell, { appearance, className, children: /* @__PURE__ */ jsxs15("div", { className: "kerne-stack", children: [
|
|
2818
3035
|
heading ? /* @__PURE__ */ jsx16("h2", { className: "kerne-section-title", children: heading }) : null,
|
|
2819
3036
|
children
|
|
@@ -2831,7 +3048,7 @@ function SubscriptionCard({
|
|
|
2831
3048
|
}
|
|
2832
3049
|
if (!subscription) {
|
|
2833
3050
|
return shell(
|
|
2834
|
-
/* @__PURE__ */ jsxs15(
|
|
3051
|
+
/* @__PURE__ */ jsxs15(Fragment9, { children: [
|
|
2835
3052
|
/* @__PURE__ */ jsx16("p", { className: "kerne-subsection-desc", children: l10n.billing.freePlan }),
|
|
2836
3053
|
upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx16(
|
|
2837
3054
|
Button,
|
|
@@ -2850,7 +3067,7 @@ function SubscriptionCard({
|
|
|
2850
3067
|
const renews = formatDate(current_period_end);
|
|
2851
3068
|
const trialEnds = formatDate(trial_end);
|
|
2852
3069
|
return shell(
|
|
2853
|
-
/* @__PURE__ */ jsxs15(
|
|
3070
|
+
/* @__PURE__ */ jsxs15(Fragment9, { children: [
|
|
2854
3071
|
portalError ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: portalError }) : null,
|
|
2855
3072
|
/* @__PURE__ */ jsxs15("dl", { className: "kerne-rows", children: [
|
|
2856
3073
|
/* @__PURE__ */ jsxs15("div", { className: "kerne-row", children: [
|
|
@@ -2879,13 +3096,33 @@ function SubscriptionCard({
|
|
|
2879
3096
|
] }) : null
|
|
2880
3097
|
] }),
|
|
2881
3098
|
canceled_at ? /* @__PURE__ */ jsx16(Panel, { children: l10n.billing.cancelNotice }) : null,
|
|
2882
|
-
|
|
3099
|
+
cancelError ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: cancelError }) : null,
|
|
3100
|
+
confirmingCancel ? /* @__PURE__ */ jsxs15(Panel, { children: [
|
|
3101
|
+
/* @__PURE__ */ jsx16("p", { children: l10n.billing.cancelConfirmTitle }),
|
|
3102
|
+
/* @__PURE__ */ jsx16("p", { className: "kerne-subsection-desc", children: l10n.billing.cancelConfirmBody }),
|
|
3103
|
+
/* @__PURE__ */ jsxs15("div", { className: "kerne-inline-actions", children: [
|
|
3104
|
+
/* @__PURE__ */ jsx16(
|
|
3105
|
+
Button,
|
|
3106
|
+
{
|
|
3107
|
+
variant: "outline",
|
|
3108
|
+
type: "button",
|
|
3109
|
+
onClick: () => setConfirmingCancel(false),
|
|
3110
|
+
disabled: isCanceling,
|
|
3111
|
+
children: l10n.billing.cancelConfirmDismiss
|
|
3112
|
+
}
|
|
3113
|
+
),
|
|
3114
|
+
/* @__PURE__ */ jsx16(Button, { type: "button", onClick: handleCancel, loading: isCanceling, children: isCanceling ? l10n.billing.canceling : l10n.billing.cancelConfirmButton })
|
|
3115
|
+
] })
|
|
3116
|
+
] }) : /* @__PURE__ */ jsxs15("div", { className: "kerne-inline-actions", children: [
|
|
3117
|
+
!hidePortalButton ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", onClick: handlePortal, loading: opening, children: opening ? l10n.billing.opening : l10n.billing.managePortal }) : null,
|
|
3118
|
+
!hideCancelButton && !canceled_at && status !== "CANCELED" ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", onClick: () => setConfirmingCancel(true), children: l10n.billing.cancelSubscription }) : null
|
|
3119
|
+
] })
|
|
2883
3120
|
] })
|
|
2884
3121
|
);
|
|
2885
3122
|
}
|
|
2886
3123
|
|
|
2887
3124
|
// src/ui/profile/BillingSection.tsx
|
|
2888
|
-
import { Fragment as
|
|
3125
|
+
import { Fragment as Fragment10, jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2889
3126
|
function intervalsOf(plans) {
|
|
2890
3127
|
const seen = [];
|
|
2891
3128
|
for (const plan of plans) {
|
|
@@ -2906,31 +3143,37 @@ function BillingSection({
|
|
|
2906
3143
|
usageFeatureKeys,
|
|
2907
3144
|
usageLabels
|
|
2908
3145
|
}) {
|
|
2909
|
-
const { subscription } = useSubscription(productSlug);
|
|
3146
|
+
const { subscription, refetch: refetchSubscription } = useSubscription(productSlug);
|
|
2910
3147
|
const { plans, isLoading: plansLoading } = usePlans(pricingProductIdOrSlug);
|
|
2911
|
-
const { openCheckout } = useCheckout();
|
|
2912
3148
|
const l10n = useLocalization();
|
|
2913
3149
|
const showError = useErrorResolver();
|
|
2914
|
-
const
|
|
2915
|
-
|
|
3150
|
+
const {
|
|
3151
|
+
pending: pendingPlan,
|
|
3152
|
+
switching,
|
|
3153
|
+
switchError,
|
|
3154
|
+
checkoutPriceId: busy,
|
|
3155
|
+
checkoutError,
|
|
3156
|
+
selectPlan,
|
|
3157
|
+
confirmSwitch,
|
|
3158
|
+
dismissSwitch
|
|
3159
|
+
} = usePlanSwitch(subscription, refetchSubscription, {
|
|
3160
|
+
successUrl: checkoutSuccessUrl,
|
|
3161
|
+
cancelUrl: checkoutCancelUrl
|
|
3162
|
+
});
|
|
3163
|
+
const error = checkoutError ?? switchError;
|
|
2916
3164
|
const intervals = useMemo(() => intervalsOf(plans), [plans]);
|
|
2917
3165
|
const [interval, setInterval] = useState17(null);
|
|
2918
3166
|
const activeInterval = interval ?? subscription?.plan_price.interval ?? intervals[0] ?? null;
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
setBusy(null);
|
|
2930
|
-
}
|
|
2931
|
-
};
|
|
2932
|
-
return /* @__PURE__ */ jsxs16(Fragment9, { children: [
|
|
2933
|
-
error ? /* @__PURE__ */ jsx17(Panel, { variant: "danger", children: error }) : null,
|
|
3167
|
+
return /* @__PURE__ */ jsxs16(Fragment10, { children: [
|
|
3168
|
+
error ? /* @__PURE__ */ jsx17(Panel, { variant: "danger", children: showError(error) }) : null,
|
|
3169
|
+
pendingPlan ? /* @__PURE__ */ jsxs16(Panel, { children: [
|
|
3170
|
+
/* @__PURE__ */ jsx17("p", { children: fill(l10n.billing.switchConfirmTitle, { plan: pendingPlan.plan.name }) }),
|
|
3171
|
+
/* @__PURE__ */ jsx17("p", { className: "kerne-subsection-desc", children: l10n.billing.switchConfirmBody }),
|
|
3172
|
+
/* @__PURE__ */ jsxs16("div", { className: "kerne-inline-actions", children: [
|
|
3173
|
+
/* @__PURE__ */ jsx17(Button, { variant: "outline", type: "button", onClick: dismissSwitch, disabled: switching, children: l10n.billing.switchConfirmDismiss }),
|
|
3174
|
+
/* @__PURE__ */ jsx17(Button, { type: "button", onClick: confirmSwitch, loading: switching, children: switching ? l10n.billing.switching : l10n.billing.switchConfirmButton })
|
|
3175
|
+
] })
|
|
3176
|
+
] }) : null,
|
|
2934
3177
|
/* @__PURE__ */ jsx17(Subsection, { title: l10n.billing.currentPlan, children: /* @__PURE__ */ jsx17(
|
|
2935
3178
|
SubscriptionCard,
|
|
2936
3179
|
{
|
|
@@ -2947,7 +3190,7 @@ function BillingSection({
|
|
|
2947
3190
|
style: { height: 64 },
|
|
2948
3191
|
"aria-label": l10n.billing.loadingPlans
|
|
2949
3192
|
}
|
|
2950
|
-
) : plans.length === 0 ? /* @__PURE__ */ jsx17("p", { className: "kerne-subsection-desc", children: l10n.billing.noPlans }) : /* @__PURE__ */ jsxs16(
|
|
3193
|
+
) : plans.length === 0 ? /* @__PURE__ */ jsx17("p", { className: "kerne-subsection-desc", children: l10n.billing.noPlans }) : /* @__PURE__ */ jsxs16(Fragment10, { children: [
|
|
2951
3194
|
intervals.length > 1 ? /* @__PURE__ */ jsx17(
|
|
2952
3195
|
"div",
|
|
2953
3196
|
{
|
|
@@ -2988,7 +3231,7 @@ function BillingSection({
|
|
|
2988
3231
|
{
|
|
2989
3232
|
variant: "outline",
|
|
2990
3233
|
type: "button",
|
|
2991
|
-
onClick: () =>
|
|
3234
|
+
onClick: () => selectPlan(plan, price),
|
|
2992
3235
|
loading: busy === price.id,
|
|
2993
3236
|
disabled: busy !== null,
|
|
2994
3237
|
children: busy === price.id ? l10n.billing.opening : l10n.billing.switchTo
|
|
@@ -3004,7 +3247,7 @@ function BillingSection({
|
|
|
3004
3247
|
}
|
|
3005
3248
|
|
|
3006
3249
|
// src/ui/UserProfile.tsx
|
|
3007
|
-
import { Fragment as
|
|
3250
|
+
import { Fragment as Fragment11, jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
3008
3251
|
function UserProfile({
|
|
3009
3252
|
tabs = ["profile", "security", "billing"],
|
|
3010
3253
|
extraTabs = [],
|
|
@@ -3099,7 +3342,7 @@ function UserProfile({
|
|
|
3099
3342
|
tab.id
|
|
3100
3343
|
)) }),
|
|
3101
3344
|
/* @__PURE__ */ jsxs17("div", { className: "kerne-pane", children: [
|
|
3102
|
-
active ? /* @__PURE__ */ jsxs17(
|
|
3345
|
+
active ? /* @__PURE__ */ jsxs17(Fragment11, { children: [
|
|
3103
3346
|
/* @__PURE__ */ jsx18("h2", { className: "kerne-pane-title", children: active.title }),
|
|
3104
3347
|
active.description ? /* @__PURE__ */ jsx18("p", { className: "kerne-pane-desc", children: active.description }) : null
|
|
3105
3348
|
] }) : null,
|
|
@@ -3108,9 +3351,291 @@ function UserProfile({
|
|
|
3108
3351
|
] }) });
|
|
3109
3352
|
}
|
|
3110
3353
|
|
|
3354
|
+
// src/ui/PricingTable.tsx
|
|
3355
|
+
import { createContext as createContext2, useContext as useContext2, useEffect as useEffect7, useMemo as useMemo2, useRef as useRef4, useState as useState19 } from "react";
|
|
3356
|
+
import { Fragment as Fragment12, jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
3357
|
+
function formatEntitlement(e, unlimitedLabel) {
|
|
3358
|
+
if (e.feature_type === "BOOLEAN") return e.feature_name;
|
|
3359
|
+
if (e.feature_type === "STATIC")
|
|
3360
|
+
return e.value ? `${e.feature_name}: ${e.value}` : e.feature_name;
|
|
3361
|
+
if (e.limit === null) return e.feature_name;
|
|
3362
|
+
if (e.limit === -1) return `${unlimitedLabel} ${e.feature_name.toLowerCase()}`;
|
|
3363
|
+
return `${formatNumber(e.limit)} ${e.feature_name.toLowerCase()}`;
|
|
3364
|
+
}
|
|
3365
|
+
var PricingTableContext = createContext2(null);
|
|
3366
|
+
function usePricingTable() {
|
|
3367
|
+
const ctx = useContext2(PricingTableContext);
|
|
3368
|
+
if (!ctx) {
|
|
3369
|
+
throw new Error("usePricingTable() must be called inside <PricingTable>.");
|
|
3370
|
+
}
|
|
3371
|
+
return ctx;
|
|
3372
|
+
}
|
|
3373
|
+
function PricingTable({
|
|
3374
|
+
product,
|
|
3375
|
+
defaultLabel,
|
|
3376
|
+
checkoutSuccessUrl,
|
|
3377
|
+
checkoutCancelUrl,
|
|
3378
|
+
onSelectPlan,
|
|
3379
|
+
appearance,
|
|
3380
|
+
className,
|
|
3381
|
+
children
|
|
3382
|
+
}) {
|
|
3383
|
+
const { plans, isLoading, error } = usePlans(product);
|
|
3384
|
+
const { subscription, refetch: refetchSubscription } = useSubscription(product);
|
|
3385
|
+
const l10n = useLocalization();
|
|
3386
|
+
const showError = useErrorResolver();
|
|
3387
|
+
const [selectedLabelSlug, setSelectedLabelSlug] = useState19(defaultLabel ?? null);
|
|
3388
|
+
const {
|
|
3389
|
+
pending: pendingSwitch,
|
|
3390
|
+
switching,
|
|
3391
|
+
switchError,
|
|
3392
|
+
checkoutPriceId: busyPriceId,
|
|
3393
|
+
checkoutError,
|
|
3394
|
+
selectPlan,
|
|
3395
|
+
confirmSwitch,
|
|
3396
|
+
dismissSwitch
|
|
3397
|
+
} = usePlanSwitch(subscription, refetchSubscription, {
|
|
3398
|
+
successUrl: checkoutSuccessUrl,
|
|
3399
|
+
cancelUrl: checkoutCancelUrl
|
|
3400
|
+
});
|
|
3401
|
+
const labels = useMemo2(() => collectPriceLabels(plans), [plans]);
|
|
3402
|
+
const savings = useMemo2(() => resolveLabelSavings(plans, labels), [plans, labels]);
|
|
3403
|
+
useEffect7(() => {
|
|
3404
|
+
if (labels.length === 0) return;
|
|
3405
|
+
if (selectedLabelSlug && labels.some((l) => l.slug === selectedLabelSlug)) return;
|
|
3406
|
+
setSelectedLabelSlug(labels[0].slug);
|
|
3407
|
+
}, [labels]);
|
|
3408
|
+
const handleSelectPlan = (plan) => {
|
|
3409
|
+
const price = plan.prices.find((p) => p.label.slug === selectedLabelSlug) ?? plan.prices[0];
|
|
3410
|
+
if (!price) return;
|
|
3411
|
+
if (onSelectPlan) {
|
|
3412
|
+
onSelectPlan(plan, price);
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
void selectPlan(plan, price);
|
|
3416
|
+
};
|
|
3417
|
+
const ctx = {
|
|
3418
|
+
plans,
|
|
3419
|
+
labels,
|
|
3420
|
+
selectedLabelSlug,
|
|
3421
|
+
setSelectedLabelSlug,
|
|
3422
|
+
savings,
|
|
3423
|
+
currentPlanId: subscription?.plan?.id ?? null,
|
|
3424
|
+
busyPriceId,
|
|
3425
|
+
onSelectPlan: handleSelectPlan
|
|
3426
|
+
};
|
|
3427
|
+
return /* @__PURE__ */ jsx19(Shell, { appearance, className, size: "wide", children: isLoading ? /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-skeleton", "aria-busy": "true", "aria-label": l10n.pricing.loading, children: /* @__PURE__ */ jsx19("div", { className: "kerne-skeleton", style: { height: 320 } }) }) : error ? /* @__PURE__ */ jsx19(Panel, { variant: "danger", children: l10n.pricing.loadFailed }) : /* @__PURE__ */ jsxs18(PricingTableContext.Provider, { value: ctx, children: [
|
|
3428
|
+
checkoutError ? /* @__PURE__ */ jsx19(Panel, { variant: "danger", children: showError(checkoutError) }) : null,
|
|
3429
|
+
pendingSwitch ? /* @__PURE__ */ jsxs18(Panel, { children: [
|
|
3430
|
+
/* @__PURE__ */ jsx19("p", { children: fill(l10n.billing.switchConfirmTitle, { plan: pendingSwitch.plan.name }) }),
|
|
3431
|
+
/* @__PURE__ */ jsx19("p", { className: "kerne-subsection-desc", children: l10n.billing.switchConfirmBody }),
|
|
3432
|
+
switchError ? /* @__PURE__ */ jsx19(Panel, { variant: "danger", children: showError(switchError) }) : null,
|
|
3433
|
+
/* @__PURE__ */ jsxs18("div", { className: "kerne-inline-actions", children: [
|
|
3434
|
+
/* @__PURE__ */ jsx19(
|
|
3435
|
+
Button,
|
|
3436
|
+
{
|
|
3437
|
+
variant: "outline",
|
|
3438
|
+
type: "button",
|
|
3439
|
+
onClick: dismissSwitch,
|
|
3440
|
+
disabled: switching,
|
|
3441
|
+
children: l10n.billing.switchConfirmDismiss
|
|
3442
|
+
}
|
|
3443
|
+
),
|
|
3444
|
+
/* @__PURE__ */ jsx19(Button, { type: "button", onClick: confirmSwitch, loading: switching, children: switching ? l10n.billing.switching : l10n.billing.switchConfirmButton })
|
|
3445
|
+
] })
|
|
3446
|
+
] }) : null,
|
|
3447
|
+
children
|
|
3448
|
+
] }) });
|
|
3449
|
+
}
|
|
3450
|
+
function LabelSwitch() {
|
|
3451
|
+
const { labels, selectedLabelSlug, setSelectedLabelSlug, savings } = usePricingTable();
|
|
3452
|
+
const l10n = useLocalization();
|
|
3453
|
+
const containerRef = useRef4(null);
|
|
3454
|
+
const buttonRefs = useRef4(/* @__PURE__ */ new Map());
|
|
3455
|
+
const [thumb, setThumb] = useState19(null);
|
|
3456
|
+
useEffect7(() => {
|
|
3457
|
+
const measure = () => {
|
|
3458
|
+
const container = containerRef.current;
|
|
3459
|
+
const button = selectedLabelSlug ? buttonRefs.current.get(selectedLabelSlug) : null;
|
|
3460
|
+
if (!container || !button) return;
|
|
3461
|
+
const containerBox = container.getBoundingClientRect();
|
|
3462
|
+
const buttonBox = button.getBoundingClientRect();
|
|
3463
|
+
setThumb({ left: buttonBox.left - containerBox.left, width: buttonBox.width });
|
|
3464
|
+
};
|
|
3465
|
+
measure();
|
|
3466
|
+
window.addEventListener("resize", measure);
|
|
3467
|
+
return () => window.removeEventListener("resize", measure);
|
|
3468
|
+
}, [selectedLabelSlug, labels]);
|
|
3469
|
+
if (labels.length < 2) return null;
|
|
3470
|
+
return /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-switch-wrap", children: /* @__PURE__ */ jsxs18(
|
|
3471
|
+
"div",
|
|
3472
|
+
{
|
|
3473
|
+
ref: containerRef,
|
|
3474
|
+
className: "kerne-pricing-switch",
|
|
3475
|
+
role: "group",
|
|
3476
|
+
"aria-label": l10n.billing.intervalGroupLabel,
|
|
3477
|
+
children: [
|
|
3478
|
+
thumb ? /* @__PURE__ */ jsx19(
|
|
3479
|
+
"span",
|
|
3480
|
+
{
|
|
3481
|
+
className: "kerne-pricing-switch-thumb",
|
|
3482
|
+
style: { transform: `translateX(${thumb.left}px)`, width: `${thumb.width}px` },
|
|
3483
|
+
"aria-hidden": "true"
|
|
3484
|
+
}
|
|
3485
|
+
) : null,
|
|
3486
|
+
labels.map((label) => /* @__PURE__ */ jsxs18(
|
|
3487
|
+
"button",
|
|
3488
|
+
{
|
|
3489
|
+
ref: (el) => {
|
|
3490
|
+
if (el) buttonRefs.current.set(label.slug, el);
|
|
3491
|
+
else buttonRefs.current.delete(label.slug);
|
|
3492
|
+
},
|
|
3493
|
+
type: "button",
|
|
3494
|
+
className: "kerne-pricing-switch-option",
|
|
3495
|
+
"aria-pressed": label.slug === selectedLabelSlug,
|
|
3496
|
+
onClick: () => setSelectedLabelSlug(label.slug),
|
|
3497
|
+
children: [
|
|
3498
|
+
label.name,
|
|
3499
|
+
savings && savings.labelSlug === label.slug ? /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-switch-save", children: fill(l10n.pricing.save, { percent: savings.percent }) }) : null
|
|
3500
|
+
]
|
|
3501
|
+
},
|
|
3502
|
+
label.slug
|
|
3503
|
+
))
|
|
3504
|
+
]
|
|
3505
|
+
}
|
|
3506
|
+
) });
|
|
3507
|
+
}
|
|
3508
|
+
function useAnimatedAmount(target, durationMs = 420) {
|
|
3509
|
+
const [displayed, setDisplayed] = useState19(target);
|
|
3510
|
+
const displayedRef = useRef4(target);
|
|
3511
|
+
const frameRef = useRef4();
|
|
3512
|
+
useEffect7(() => {
|
|
3513
|
+
if (displayedRef.current === target) return;
|
|
3514
|
+
const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
3515
|
+
if (reduceMotion) {
|
|
3516
|
+
displayedRef.current = target;
|
|
3517
|
+
setDisplayed(target);
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
const from = displayedRef.current;
|
|
3521
|
+
const start = performance.now();
|
|
3522
|
+
const tick = (now) => {
|
|
3523
|
+
const t = Math.min((now - start) / durationMs, 1);
|
|
3524
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
3525
|
+
const value = Math.round(from + (target - from) * eased);
|
|
3526
|
+
displayedRef.current = value;
|
|
3527
|
+
setDisplayed(value);
|
|
3528
|
+
if (t < 1) frameRef.current = requestAnimationFrame(tick);
|
|
3529
|
+
};
|
|
3530
|
+
frameRef.current = requestAnimationFrame(tick);
|
|
3531
|
+
return () => {
|
|
3532
|
+
if (frameRef.current) cancelAnimationFrame(frameRef.current);
|
|
3533
|
+
};
|
|
3534
|
+
}, [target, durationMs]);
|
|
3535
|
+
return displayed;
|
|
3536
|
+
}
|
|
3537
|
+
function Plan({ slug, featured, badge, maxFeatures }) {
|
|
3538
|
+
const { plans, selectedLabelSlug, currentPlanId, busyPriceId, onSelectPlan } = usePricingTable();
|
|
3539
|
+
const l10n = useLocalization();
|
|
3540
|
+
const plan = plans.find((p) => p.slug === slug);
|
|
3541
|
+
useEffect7(() => {
|
|
3542
|
+
if (!plan && process.env.NODE_ENV === "development") {
|
|
3543
|
+
console.warn(
|
|
3544
|
+
`[Kerne] <PricingTable.Plan slug="${slug}"> - no plan with this slug on the loaded product.`
|
|
3545
|
+
);
|
|
3546
|
+
}
|
|
3547
|
+
}, [plan, slug]);
|
|
3548
|
+
const price = plan?.prices.find((p) => p.label.slug === selectedLabelSlug) ?? plan?.prices[0];
|
|
3549
|
+
const resolved = price ? resolvePriceDisplay(price, price.label.display_interval) : null;
|
|
3550
|
+
const animatedAmount = useAnimatedAmount(resolved?.displayAmount ?? 0);
|
|
3551
|
+
if (!plan) return null;
|
|
3552
|
+
const isCurrent = plan.id === currentPlanId;
|
|
3553
|
+
const isBusy = !!price && busyPriceId === price.id;
|
|
3554
|
+
const shownEntitlements = maxFeatures ? plan.entitlements.slice(0, maxFeatures) : plan.entitlements;
|
|
3555
|
+
const remaining = plan.entitlements.length - shownEntitlements.length;
|
|
3556
|
+
const billedCaption = resolved && resolved.displayInterval !== resolved.billedInterval ? fill(l10n.pricing.billedAt, {
|
|
3557
|
+
amount: formatMoney(resolved.billedAmount, price.currency),
|
|
3558
|
+
interval: formatInterval(resolved.billedInterval, l10n.billing)
|
|
3559
|
+
}) : null;
|
|
3560
|
+
return /* @__PURE__ */ jsxs18(
|
|
3561
|
+
"div",
|
|
3562
|
+
{
|
|
3563
|
+
className: "kerne-pricing-card",
|
|
3564
|
+
"data-featured": featured ? "true" : void 0,
|
|
3565
|
+
"data-current": isCurrent ? "true" : void 0,
|
|
3566
|
+
children: [
|
|
3567
|
+
/* @__PURE__ */ jsxs18("div", { className: "kerne-pricing-card-head", children: [
|
|
3568
|
+
badge ? /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-badge", children: badge }) : null,
|
|
3569
|
+
/* @__PURE__ */ jsx19("span", { className: "kerne-pricing-card-name", children: plan.name }),
|
|
3570
|
+
plan.description ? /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-card-desc", children: plan.description }) : null
|
|
3571
|
+
] }),
|
|
3572
|
+
/* @__PURE__ */ jsx19("div", { className: "kerne-pricing-price", children: resolved ? /* @__PURE__ */ jsxs18(Fragment12, { children: [
|
|
3573
|
+
/* @__PURE__ */ jsxs18("div", { className: "kerne-pricing-price-row", children: [
|
|
3574
|
+
/* @__PURE__ */ jsx19("span", { className: "kerne-pricing-amount", children: formatMoney(animatedAmount, price.currency) }),
|
|
3575
|
+
/* @__PURE__ */ jsxs18("span", { className: "kerne-pricing-interval", children: [
|
|
3576
|
+
"/ ",
|
|
3577
|
+
formatInterval(resolved.displayInterval, l10n.billing)
|
|
3578
|
+
] })
|
|
3579
|
+
] }),
|
|
3580
|
+
billedCaption ? /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-billed", children: billedCaption }) : null
|
|
3581
|
+
] }) : /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-billed", children: l10n.billing.notOnInterval }) }),
|
|
3582
|
+
shownEntitlements.length > 0 ? /* @__PURE__ */ jsxs18("ul", { className: "kerne-pricing-features", children: [
|
|
3583
|
+
shownEntitlements.map((e) => /* @__PURE__ */ jsxs18("li", { className: "kerne-pricing-feature", children: [
|
|
3584
|
+
/* @__PURE__ */ jsx19(CheckIcon2, {}),
|
|
3585
|
+
/* @__PURE__ */ jsx19("span", { children: formatEntitlement(e, l10n.pricing.unlimited) })
|
|
3586
|
+
] }, e.feature_key)),
|
|
3587
|
+
remaining > 0 ? /* @__PURE__ */ jsx19("li", { className: "kerne-pricing-feature-more", children: fill(l10n.pricing.moreFeatures, { count: remaining }) }) : null
|
|
3588
|
+
] }) : null,
|
|
3589
|
+
/* @__PURE__ */ jsx19("div", { className: "kerne-pricing-cta", children: isCurrent ? /* @__PURE__ */ jsx19(Button, { variant: "outline", type: "button", disabled: true, children: l10n.billing.current }) : /* @__PURE__ */ jsx19(
|
|
3590
|
+
Button,
|
|
3591
|
+
{
|
|
3592
|
+
variant: featured ? "primary" : "outline",
|
|
3593
|
+
type: "button",
|
|
3594
|
+
disabled: !price || busyPriceId !== null && !isBusy,
|
|
3595
|
+
loading: isBusy,
|
|
3596
|
+
onClick: () => onSelectPlan(plan),
|
|
3597
|
+
children: currentPlanId ? l10n.billing.switchTo : l10n.pricing.getStarted
|
|
3598
|
+
}
|
|
3599
|
+
) })
|
|
3600
|
+
]
|
|
3601
|
+
}
|
|
3602
|
+
);
|
|
3603
|
+
}
|
|
3604
|
+
function CheckIcon2() {
|
|
3605
|
+
return /* @__PURE__ */ jsx19(
|
|
3606
|
+
"svg",
|
|
3607
|
+
{
|
|
3608
|
+
width: "15",
|
|
3609
|
+
height: "15",
|
|
3610
|
+
viewBox: "0 0 24 24",
|
|
3611
|
+
fill: "none",
|
|
3612
|
+
stroke: "currentColor",
|
|
3613
|
+
strokeWidth: "2",
|
|
3614
|
+
"aria-hidden": "true",
|
|
3615
|
+
children: /* @__PURE__ */ jsx19("path", { d: "m5 13 4 4L19 7", strokeLinecap: "round", strokeLinejoin: "round" })
|
|
3616
|
+
}
|
|
3617
|
+
);
|
|
3618
|
+
}
|
|
3619
|
+
function Plans({ featuredSlug, featuredBadge, maxFeatures }) {
|
|
3620
|
+
const { plans } = usePricingTable();
|
|
3621
|
+
return /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-grid", children: plans.map((plan) => /* @__PURE__ */ jsx19(
|
|
3622
|
+
Plan,
|
|
3623
|
+
{
|
|
3624
|
+
slug: plan.slug,
|
|
3625
|
+
featured: plan.slug === featuredSlug,
|
|
3626
|
+
badge: plan.slug === featuredSlug ? featuredBadge : void 0,
|
|
3627
|
+
maxFeatures
|
|
3628
|
+
},
|
|
3629
|
+
plan.id
|
|
3630
|
+
)) });
|
|
3631
|
+
}
|
|
3632
|
+
PricingTable.LabelSwitch = LabelSwitch;
|
|
3633
|
+
PricingTable.Plan = Plan;
|
|
3634
|
+
PricingTable.Plans = Plans;
|
|
3635
|
+
|
|
3111
3636
|
// src/ui/CheckoutResult.tsx
|
|
3112
|
-
import { useEffect as
|
|
3113
|
-
import { jsx as
|
|
3637
|
+
import { useEffect as useEffect8, useRef as useRef5, useState as useState20 } from "react";
|
|
3638
|
+
import { jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
3114
3639
|
var POLL_INTERVAL_MS = 1500;
|
|
3115
3640
|
function CheckoutResult({
|
|
3116
3641
|
outcome = "success",
|
|
@@ -3128,9 +3653,9 @@ function CheckoutResult({
|
|
|
3128
3653
|
const client = useClient();
|
|
3129
3654
|
const { config } = useAuthConfig();
|
|
3130
3655
|
const l10n = useLocalization();
|
|
3131
|
-
const [state, setState] =
|
|
3132
|
-
const confirmed =
|
|
3133
|
-
|
|
3656
|
+
const [state, setState] = useState20("checking");
|
|
3657
|
+
const confirmed = useRef5(false);
|
|
3658
|
+
useEffect8(() => {
|
|
3134
3659
|
if (outcome !== "success") return;
|
|
3135
3660
|
let stopped = false;
|
|
3136
3661
|
const startedAt = Date.now();
|
|
@@ -3161,20 +3686,20 @@ function CheckoutResult({
|
|
|
3161
3686
|
const go = (url, handler) => handler ?? (() => {
|
|
3162
3687
|
if (url) window.location.href = url;
|
|
3163
3688
|
});
|
|
3164
|
-
const continueAction = continueUrl || onContinue ? /* @__PURE__ */
|
|
3165
|
-
const shell = (children) => /* @__PURE__ */
|
|
3166
|
-
logo ? /* @__PURE__ */
|
|
3689
|
+
const continueAction = continueUrl || onContinue ? /* @__PURE__ */ jsx20(Button, { type: "button", onClick: go(continueUrl, onContinue), children: l10n.common.continue }) : void 0;
|
|
3690
|
+
const shell = (children) => /* @__PURE__ */ jsxs19(Shell, { appearance, className, children: [
|
|
3691
|
+
logo ? /* @__PURE__ */ jsx20("div", { className: "kerne-masthead", children: logo }) : null,
|
|
3167
3692
|
children,
|
|
3168
|
-
/* @__PURE__ */
|
|
3693
|
+
/* @__PURE__ */ jsx20(KerneBranding, { show: config?.show_kerne_branding ?? false })
|
|
3169
3694
|
] });
|
|
3170
3695
|
if (outcome === "cancel") {
|
|
3171
3696
|
return shell(
|
|
3172
|
-
/* @__PURE__ */
|
|
3697
|
+
/* @__PURE__ */ jsx20(
|
|
3173
3698
|
Status,
|
|
3174
3699
|
{
|
|
3175
3700
|
tone: "danger",
|
|
3176
3701
|
title: l10n.checkout.cancelTitle,
|
|
3177
|
-
action: pricingUrl || onRetry ? /* @__PURE__ */
|
|
3702
|
+
action: pricingUrl || onRetry ? /* @__PURE__ */ jsx20(Button, { variant: "outline", type: "button", onClick: go(pricingUrl, onRetry), children: l10n.checkout.backToPlans }) : void 0,
|
|
3178
3703
|
children: l10n.checkout.cancelBody
|
|
3179
3704
|
}
|
|
3180
3705
|
)
|
|
@@ -3182,21 +3707,21 @@ function CheckoutResult({
|
|
|
3182
3707
|
}
|
|
3183
3708
|
if (state === "checking") {
|
|
3184
3709
|
return shell(
|
|
3185
|
-
/* @__PURE__ */
|
|
3710
|
+
/* @__PURE__ */ jsx20(Status, { tone: "loading", title: l10n.checkout.confirmingTitle, children: l10n.checkout.confirmingBody })
|
|
3186
3711
|
);
|
|
3187
3712
|
}
|
|
3188
3713
|
if (state === "pending") {
|
|
3189
3714
|
return shell(
|
|
3190
|
-
/* @__PURE__ */
|
|
3715
|
+
/* @__PURE__ */ jsx20(Status, { tone: "success", title: l10n.checkout.pendingTitle, action: continueAction, children: l10n.checkout.pendingBody })
|
|
3191
3716
|
);
|
|
3192
3717
|
}
|
|
3193
3718
|
return shell(
|
|
3194
|
-
/* @__PURE__ */
|
|
3719
|
+
/* @__PURE__ */ jsx20(Status, { tone: "success", title: l10n.checkout.doneTitle, action: continueAction, children: l10n.checkout.doneBody })
|
|
3195
3720
|
);
|
|
3196
3721
|
}
|
|
3197
3722
|
|
|
3198
3723
|
// src/ui/UpgradePrompt.tsx
|
|
3199
|
-
import { Fragment as
|
|
3724
|
+
import { Fragment as Fragment13, jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
3200
3725
|
function UpgradePrompt({
|
|
3201
3726
|
featureKey,
|
|
3202
3727
|
requested,
|
|
@@ -3215,22 +3740,25 @@ function UpgradePrompt({
|
|
|
3215
3740
|
const l10n = useLocalization();
|
|
3216
3741
|
if (isLoading) {
|
|
3217
3742
|
if (hideWhileLoading) return null;
|
|
3218
|
-
return /* @__PURE__ */
|
|
3743
|
+
return /* @__PURE__ */ jsx21(Shell, { appearance, className, children: /* @__PURE__ */ jsx21("div", { className: "kerne-skeleton", style: { height: 80 }, "aria-label": l10n.upgrade.checking }) });
|
|
3219
3744
|
}
|
|
3220
|
-
if (allowed) return /* @__PURE__ */
|
|
3745
|
+
if (allowed) return /* @__PURE__ */ jsx21(Fragment13, { children });
|
|
3221
3746
|
const name = featureName ?? humanizeFeatureKey(featureKey).toLowerCase();
|
|
3222
3747
|
const quotaExhausted = details?.feature_type === "QUOTA" && typeof details.limit === "number" && typeof details.used === "number";
|
|
3223
3748
|
const heading = title ?? fill(quotaExhausted ? l10n.upgrade.quotaTitle : l10n.upgrade.featureTitle, { feature: name });
|
|
3224
3749
|
const body = description ?? (quotaExhausted ? fill(l10n.upgrade.quotaBody, {
|
|
3225
3750
|
used: formatNumber(details.used),
|
|
3226
|
-
limit
|
|
3751
|
+
// `access_limit` is where access actually stops; `limit` is only the
|
|
3752
|
+
// included allocation and can understate it past an overage ceiling.
|
|
3753
|
+
// Falls back to `limit` defensively - should never be needed here.
|
|
3754
|
+
limit: formatNumber(details.access_limit ?? details.limit)
|
|
3227
3755
|
}) : fill(l10n.upgrade.featureBody, { feature: name }));
|
|
3228
|
-
return /* @__PURE__ */
|
|
3229
|
-
/* @__PURE__ */
|
|
3230
|
-
/* @__PURE__ */
|
|
3231
|
-
/* @__PURE__ */
|
|
3756
|
+
return /* @__PURE__ */ jsx21(Shell, { appearance, className, children: /* @__PURE__ */ jsxs20("div", { className: "kerne-status", children: [
|
|
3757
|
+
/* @__PURE__ */ jsxs20("div", { className: "kerne-heading", children: [
|
|
3758
|
+
/* @__PURE__ */ jsx21("h2", { className: "kerne-section-title", style: { fontSize: 17 }, children: heading }),
|
|
3759
|
+
/* @__PURE__ */ jsx21("p", { className: "kerne-status-text", children: body })
|
|
3232
3760
|
] }),
|
|
3233
|
-
upgradeUrl || onUpgradeClick ? /* @__PURE__ */
|
|
3761
|
+
upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx21(
|
|
3234
3762
|
Button,
|
|
3235
3763
|
{
|
|
3236
3764
|
type: "button",
|
|
@@ -3251,6 +3779,7 @@ export {
|
|
|
3251
3779
|
ForgotPasswordForm,
|
|
3252
3780
|
LoginForm,
|
|
3253
3781
|
MagicLinkCallback,
|
|
3782
|
+
PricingTable,
|
|
3254
3783
|
ProfileSection,
|
|
3255
3784
|
RegisterForm,
|
|
3256
3785
|
ResetPasswordForm,
|
|
@@ -3263,6 +3792,15 @@ export {
|
|
|
3263
3792
|
UserProfile,
|
|
3264
3793
|
VerifyEmailForm,
|
|
3265
3794
|
WaitlistForm,
|
|
3795
|
+
collectPriceLabels,
|
|
3266
3796
|
defaultLocalization,
|
|
3267
|
-
|
|
3797
|
+
formatDate,
|
|
3798
|
+
formatInterval,
|
|
3799
|
+
formatMoney,
|
|
3800
|
+
formatNumber,
|
|
3801
|
+
humanizeFeatureKey,
|
|
3802
|
+
resolveLabelSavings,
|
|
3803
|
+
resolvePriceDisplay,
|
|
3804
|
+
useLocalization,
|
|
3805
|
+
usePricingTable
|
|
3268
3806
|
};
|