@kerne/react 0.1.3 → 1.0.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-7U3QBMA7.cjs → chunk-IMZFZ3P5.cjs} +88 -17
- package/dist/{chunk-F6NV76OR.js → chunk-NFJ2S5VJ.js} +87 -16
- package/dist/{i18n-BWTT1DWD.d.cts → i18n-Di5qlnL1.d.cts} +28 -0
- package/dist/{i18n-BWTT1DWD.d.ts → i18n-Di5qlnL1.d.ts} +28 -0
- package/dist/index.cjs +16 -20
- package/dist/index.d.cts +52 -13
- package/dist/index.d.ts +52 -13
- package/dist/index.js +11 -15
- package/dist/ui/index.cjs +624 -112
- package/dist/ui/index.d.cts +67 -4
- package/dist/ui/index.d.ts +67 -4
- package/dist/ui/index.js +556 -44
- package/package.json +3 -3
package/dist/ui/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useAccess,
|
|
7
7
|
useAuth,
|
|
8
8
|
useAuthConfig,
|
|
9
|
+
useCancelSubscription,
|
|
9
10
|
useCheckout,
|
|
10
11
|
useClient,
|
|
11
12
|
useEntitlements,
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
useSubscription,
|
|
17
18
|
useUser,
|
|
18
19
|
useWaitlist
|
|
19
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-NFJ2S5VJ.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,45 @@ 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
|
+
}
|
|
2680
2825
|
|
|
2681
2826
|
// src/ui/UsageMeters.tsx
|
|
2682
|
-
import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2683
|
-
function
|
|
2684
|
-
|
|
2827
|
+
import { Fragment as Fragment8, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2828
|
+
function isSoftOverage(overage) {
|
|
2829
|
+
return overage === "ALLOW" || overage === "BILL";
|
|
2830
|
+
}
|
|
2831
|
+
function levelFor(ratio, softOverage) {
|
|
2832
|
+
if (softOverage) return ratio >= 1 ? "warning" : "normal";
|
|
2685
2833
|
if (ratio >= 0.9) return "critical";
|
|
2686
2834
|
if (ratio >= 0.75) return "warning";
|
|
2687
2835
|
return "normal";
|
|
2688
2836
|
}
|
|
2837
|
+
function quotaRatio(used, limit) {
|
|
2838
|
+
if (limit <= 0) return used > 0 ? 1 : 0;
|
|
2839
|
+
return Math.min(used / limit, 1);
|
|
2840
|
+
}
|
|
2689
2841
|
function UsageMeters({
|
|
2690
2842
|
title,
|
|
2691
2843
|
featureKeys,
|
|
@@ -2739,10 +2891,13 @@ function UsageMeters({
|
|
|
2739
2891
|
return shell(
|
|
2740
2892
|
/* @__PURE__ */ jsx15("div", { className: "kerne-meters", children: rows.map((e) => {
|
|
2741
2893
|
const unlimited = e.limit === null || e.limit === void 0;
|
|
2742
|
-
const
|
|
2743
|
-
const
|
|
2744
|
-
const
|
|
2894
|
+
const softOverage = isSoftOverage(e.overage_behavior);
|
|
2895
|
+
const billed = e.overage_behavior === "BILL";
|
|
2896
|
+
const ratio = unlimited ? 0 : quotaRatio(e.used, e.limit);
|
|
2897
|
+
const level = unlimited ? "normal" : levelFor(ratio, softOverage);
|
|
2745
2898
|
const over = !unlimited && e.used > e.limit;
|
|
2899
|
+
const standing = billed ? subscription?.entitlements.find((se) => se.feature_key === e.feature_key) : void 0;
|
|
2900
|
+
const capReached = billed && (standing?.cap_reached ?? false);
|
|
2746
2901
|
return /* @__PURE__ */ jsxs14("div", { className: "kerne-meter", "data-level": level, children: [
|
|
2747
2902
|
/* @__PURE__ */ jsxs14("div", { className: "kerne-meter-head", children: [
|
|
2748
2903
|
/* @__PURE__ */ jsx15("span", { className: "kerne-meter-name", children: nameFor(e.feature_key) }),
|
|
@@ -2760,9 +2915,18 @@ function UsageMeters({
|
|
|
2760
2915
|
children: /* @__PURE__ */ jsx15("div", { className: "kerne-meter-fill", style: { width: `${ratio * 100}%` } })
|
|
2761
2916
|
}
|
|
2762
2917
|
) : null,
|
|
2763
|
-
over &&
|
|
2764
|
-
|
|
2765
|
-
|
|
2918
|
+
over && softOverage ? /* @__PURE__ */ jsxs14(Fragment8, { children: [
|
|
2919
|
+
/* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: fill(
|
|
2920
|
+
// capReached inverts overageBilled's meaning (billing stopped,
|
|
2921
|
+
// access didn't) - a variant string would say the opposite of
|
|
2922
|
+
// the truth for the one plan that just stopped costing money.
|
|
2923
|
+
capReached ? l10n.usage.capReached : billed ? l10n.usage.overageBilled : l10n.usage.overage,
|
|
2924
|
+
{ count: formatNumber(e.used - e.limit) }
|
|
2925
|
+
) }),
|
|
2926
|
+
billed && standing?.billed_amount != null && standing.currency ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note kerne-meter-cost", children: fill(l10n.usage.billedAmount, {
|
|
2927
|
+
amount: formatMoney(standing.billed_amount, standing.currency)
|
|
2928
|
+
}) }) : null
|
|
2929
|
+
] }) : over ? /* @__PURE__ */ jsx15("p", { className: "kerne-meter-note", children: l10n.usage.limitReached }) : null
|
|
2766
2930
|
] }, e.feature_key);
|
|
2767
2931
|
}) })
|
|
2768
2932
|
);
|
|
@@ -2770,7 +2934,7 @@ function UsageMeters({
|
|
|
2770
2934
|
|
|
2771
2935
|
// src/ui/SubscriptionCard.tsx
|
|
2772
2936
|
import { useState as useState16 } from "react";
|
|
2773
|
-
import { Fragment as
|
|
2937
|
+
import { Fragment as Fragment9, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
2774
2938
|
var STATUS_TONES = {
|
|
2775
2939
|
ACTIVE: "success",
|
|
2776
2940
|
TRIALING: "success",
|
|
@@ -2786,11 +2950,13 @@ function SubscriptionCard({
|
|
|
2786
2950
|
upgradeUrl,
|
|
2787
2951
|
onUpgradeClick,
|
|
2788
2952
|
hidePortalButton = false,
|
|
2953
|
+
hideCancelButton = false,
|
|
2789
2954
|
appearance,
|
|
2790
2955
|
className
|
|
2791
2956
|
}) {
|
|
2792
|
-
const { subscription, isLoading, error } = useSubscription(productSlug);
|
|
2957
|
+
const { subscription, isLoading, error, refetch } = useSubscription(productSlug);
|
|
2793
2958
|
const { openPortal } = usePortal();
|
|
2959
|
+
const { cancelSubscription, isCanceling } = useCancelSubscription();
|
|
2794
2960
|
const l10n = useLocalization();
|
|
2795
2961
|
const showError = useErrorResolver();
|
|
2796
2962
|
const heading = title === null ? null : title ?? l10n.billing.plan;
|
|
@@ -2804,6 +2970,8 @@ function SubscriptionCard({
|
|
|
2804
2970
|
};
|
|
2805
2971
|
const [opening, setOpening] = useState16(false);
|
|
2806
2972
|
const [portalError, setPortalError] = useState16(null);
|
|
2973
|
+
const [confirmingCancel, setConfirmingCancel] = useState16(false);
|
|
2974
|
+
const [cancelError, setCancelError] = useState16(null);
|
|
2807
2975
|
const handlePortal = async () => {
|
|
2808
2976
|
setPortalError(null);
|
|
2809
2977
|
setOpening(true);
|
|
@@ -2814,6 +2982,17 @@ function SubscriptionCard({
|
|
|
2814
2982
|
setOpening(false);
|
|
2815
2983
|
}
|
|
2816
2984
|
};
|
|
2985
|
+
const handleCancel = async () => {
|
|
2986
|
+
if (!subscription) return;
|
|
2987
|
+
setCancelError(null);
|
|
2988
|
+
try {
|
|
2989
|
+
await cancelSubscription(subscription.id, false);
|
|
2990
|
+
setConfirmingCancel(false);
|
|
2991
|
+
refetch();
|
|
2992
|
+
} catch (e) {
|
|
2993
|
+
setCancelError(showError(e));
|
|
2994
|
+
}
|
|
2995
|
+
};
|
|
2817
2996
|
const shell = (children) => /* @__PURE__ */ jsx16(Shell, { appearance, className, children: /* @__PURE__ */ jsxs15("div", { className: "kerne-stack", children: [
|
|
2818
2997
|
heading ? /* @__PURE__ */ jsx16("h2", { className: "kerne-section-title", children: heading }) : null,
|
|
2819
2998
|
children
|
|
@@ -2831,7 +3010,7 @@ function SubscriptionCard({
|
|
|
2831
3010
|
}
|
|
2832
3011
|
if (!subscription) {
|
|
2833
3012
|
return shell(
|
|
2834
|
-
/* @__PURE__ */ jsxs15(
|
|
3013
|
+
/* @__PURE__ */ jsxs15(Fragment9, { children: [
|
|
2835
3014
|
/* @__PURE__ */ jsx16("p", { className: "kerne-subsection-desc", children: l10n.billing.freePlan }),
|
|
2836
3015
|
upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx16(
|
|
2837
3016
|
Button,
|
|
@@ -2850,7 +3029,7 @@ function SubscriptionCard({
|
|
|
2850
3029
|
const renews = formatDate(current_period_end);
|
|
2851
3030
|
const trialEnds = formatDate(trial_end);
|
|
2852
3031
|
return shell(
|
|
2853
|
-
/* @__PURE__ */ jsxs15(
|
|
3032
|
+
/* @__PURE__ */ jsxs15(Fragment9, { children: [
|
|
2854
3033
|
portalError ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: portalError }) : null,
|
|
2855
3034
|
/* @__PURE__ */ jsxs15("dl", { className: "kerne-rows", children: [
|
|
2856
3035
|
/* @__PURE__ */ jsxs15("div", { className: "kerne-row", children: [
|
|
@@ -2879,13 +3058,33 @@ function SubscriptionCard({
|
|
|
2879
3058
|
] }) : null
|
|
2880
3059
|
] }),
|
|
2881
3060
|
canceled_at ? /* @__PURE__ */ jsx16(Panel, { children: l10n.billing.cancelNotice }) : null,
|
|
2882
|
-
|
|
3061
|
+
cancelError ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: cancelError }) : null,
|
|
3062
|
+
confirmingCancel ? /* @__PURE__ */ jsxs15(Panel, { children: [
|
|
3063
|
+
/* @__PURE__ */ jsx16("p", { children: l10n.billing.cancelConfirmTitle }),
|
|
3064
|
+
/* @__PURE__ */ jsx16("p", { className: "kerne-subsection-desc", children: l10n.billing.cancelConfirmBody }),
|
|
3065
|
+
/* @__PURE__ */ jsxs15("div", { className: "kerne-inline-actions", children: [
|
|
3066
|
+
/* @__PURE__ */ jsx16(
|
|
3067
|
+
Button,
|
|
3068
|
+
{
|
|
3069
|
+
variant: "outline",
|
|
3070
|
+
type: "button",
|
|
3071
|
+
onClick: () => setConfirmingCancel(false),
|
|
3072
|
+
disabled: isCanceling,
|
|
3073
|
+
children: l10n.billing.cancelConfirmDismiss
|
|
3074
|
+
}
|
|
3075
|
+
),
|
|
3076
|
+
/* @__PURE__ */ jsx16(Button, { type: "button", onClick: handleCancel, loading: isCanceling, children: isCanceling ? l10n.billing.canceling : l10n.billing.cancelConfirmButton })
|
|
3077
|
+
] })
|
|
3078
|
+
] }) : /* @__PURE__ */ jsxs15("div", { className: "kerne-inline-actions", children: [
|
|
3079
|
+
!hidePortalButton ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", onClick: handlePortal, loading: opening, children: opening ? l10n.billing.opening : l10n.billing.managePortal }) : null,
|
|
3080
|
+
!hideCancelButton && !canceled_at && status !== "CANCELED" ? /* @__PURE__ */ jsx16(Button, { variant: "outline", type: "button", onClick: () => setConfirmingCancel(true), children: l10n.billing.cancelSubscription }) : null
|
|
3081
|
+
] })
|
|
2883
3082
|
] })
|
|
2884
3083
|
);
|
|
2885
3084
|
}
|
|
2886
3085
|
|
|
2887
3086
|
// src/ui/profile/BillingSection.tsx
|
|
2888
|
-
import { Fragment as
|
|
3087
|
+
import { Fragment as Fragment10, jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2889
3088
|
function intervalsOf(plans) {
|
|
2890
3089
|
const seen = [];
|
|
2891
3090
|
for (const plan of plans) {
|
|
@@ -2929,7 +3128,7 @@ function BillingSection({
|
|
|
2929
3128
|
setBusy(null);
|
|
2930
3129
|
}
|
|
2931
3130
|
};
|
|
2932
|
-
return /* @__PURE__ */ jsxs16(
|
|
3131
|
+
return /* @__PURE__ */ jsxs16(Fragment10, { children: [
|
|
2933
3132
|
error ? /* @__PURE__ */ jsx17(Panel, { variant: "danger", children: error }) : null,
|
|
2934
3133
|
/* @__PURE__ */ jsx17(Subsection, { title: l10n.billing.currentPlan, children: /* @__PURE__ */ jsx17(
|
|
2935
3134
|
SubscriptionCard,
|
|
@@ -2947,7 +3146,7 @@ function BillingSection({
|
|
|
2947
3146
|
style: { height: 64 },
|
|
2948
3147
|
"aria-label": l10n.billing.loadingPlans
|
|
2949
3148
|
}
|
|
2950
|
-
) : plans.length === 0 ? /* @__PURE__ */ jsx17("p", { className: "kerne-subsection-desc", children: l10n.billing.noPlans }) : /* @__PURE__ */ jsxs16(
|
|
3149
|
+
) : plans.length === 0 ? /* @__PURE__ */ jsx17("p", { className: "kerne-subsection-desc", children: l10n.billing.noPlans }) : /* @__PURE__ */ jsxs16(Fragment10, { children: [
|
|
2951
3150
|
intervals.length > 1 ? /* @__PURE__ */ jsx17(
|
|
2952
3151
|
"div",
|
|
2953
3152
|
{
|
|
@@ -3004,7 +3203,7 @@ function BillingSection({
|
|
|
3004
3203
|
}
|
|
3005
3204
|
|
|
3006
3205
|
// src/ui/UserProfile.tsx
|
|
3007
|
-
import { Fragment as
|
|
3206
|
+
import { Fragment as Fragment11, jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
3008
3207
|
function UserProfile({
|
|
3009
3208
|
tabs = ["profile", "security", "billing"],
|
|
3010
3209
|
extraTabs = [],
|
|
@@ -3099,7 +3298,7 @@ function UserProfile({
|
|
|
3099
3298
|
tab.id
|
|
3100
3299
|
)) }),
|
|
3101
3300
|
/* @__PURE__ */ jsxs17("div", { className: "kerne-pane", children: [
|
|
3102
|
-
active ? /* @__PURE__ */ jsxs17(
|
|
3301
|
+
active ? /* @__PURE__ */ jsxs17(Fragment11, { children: [
|
|
3103
3302
|
/* @__PURE__ */ jsx18("h2", { className: "kerne-pane-title", children: active.title }),
|
|
3104
3303
|
active.description ? /* @__PURE__ */ jsx18("p", { className: "kerne-pane-desc", children: active.description }) : null
|
|
3105
3304
|
] }) : null,
|
|
@@ -3108,9 +3307,317 @@ function UserProfile({
|
|
|
3108
3307
|
] }) });
|
|
3109
3308
|
}
|
|
3110
3309
|
|
|
3310
|
+
// src/ui/PricingTable.tsx
|
|
3311
|
+
import { createContext as createContext2, useContext as useContext2, useEffect as useEffect7, useMemo as useMemo2, useRef as useRef4, useState as useState19 } from "react";
|
|
3312
|
+
import { Fragment as Fragment12, jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
3313
|
+
function formatEntitlement(e, unlimitedLabel) {
|
|
3314
|
+
if (e.feature_type === "BOOLEAN") return e.feature_name;
|
|
3315
|
+
if (e.feature_type === "STATIC")
|
|
3316
|
+
return e.value ? `${e.feature_name}: ${e.value}` : e.feature_name;
|
|
3317
|
+
if (e.limit === null) return e.feature_name;
|
|
3318
|
+
if (e.limit === -1) return `${unlimitedLabel} ${e.feature_name.toLowerCase()}`;
|
|
3319
|
+
return `${formatNumber(e.limit)} ${e.feature_name.toLowerCase()}`;
|
|
3320
|
+
}
|
|
3321
|
+
function collectLabels(plans) {
|
|
3322
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
3323
|
+
for (const plan of plans) {
|
|
3324
|
+
for (const price of plan.prices) {
|
|
3325
|
+
if (!bySlug.has(price.label.slug)) bySlug.set(price.label.slug, price.label);
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
return Array.from(bySlug.values()).sort(
|
|
3329
|
+
(a, b) => a.sort_order - b.sort_order || a.name.localeCompare(b.name)
|
|
3330
|
+
);
|
|
3331
|
+
}
|
|
3332
|
+
var ENCODED_INTERVAL_MONTHS = { D: 12 / 365, M: 1, Y: 12 };
|
|
3333
|
+
function approxMonths2(value) {
|
|
3334
|
+
const match = /^([1-9]\d*)([DMY])$/.exec(value);
|
|
3335
|
+
if (!match) return 0;
|
|
3336
|
+
return parseInt(match[1], 10) * ENCODED_INTERVAL_MONTHS[match[2]];
|
|
3337
|
+
}
|
|
3338
|
+
function labelSavings(plans, labels) {
|
|
3339
|
+
if (labels.length < 2) return null;
|
|
3340
|
+
const priceFor = (labelSlug) => {
|
|
3341
|
+
for (const plan of plans) {
|
|
3342
|
+
const price = plan.prices.find((p) => p.label.slug === labelSlug);
|
|
3343
|
+
if (price) return price;
|
|
3344
|
+
}
|
|
3345
|
+
return void 0;
|
|
3346
|
+
};
|
|
3347
|
+
const sorted = [...labels].sort(
|
|
3348
|
+
(a, b) => approxMonths2(priceFor(a.slug)?.interval ?? "1M") - approxMonths2(priceFor(b.slug)?.interval ?? "1M")
|
|
3349
|
+
);
|
|
3350
|
+
const shortest = sorted[0];
|
|
3351
|
+
const longest = sorted[sorted.length - 1];
|
|
3352
|
+
if (shortest.slug === longest.slug) return null;
|
|
3353
|
+
for (const plan of plans) {
|
|
3354
|
+
const shortPrice = plan.prices.find((p) => p.label.slug === shortest.slug);
|
|
3355
|
+
const longPrice = plan.prices.find((p) => p.label.slug === longest.slug);
|
|
3356
|
+
if (!shortPrice?.amount || !longPrice) continue;
|
|
3357
|
+
const shortMonths = approxMonths2(shortPrice.interval);
|
|
3358
|
+
const longMonths = approxMonths2(longPrice.interval);
|
|
3359
|
+
if (!shortMonths || !longMonths) continue;
|
|
3360
|
+
const savings = 1 - longPrice.amount / longMonths / (shortPrice.amount / shortMonths);
|
|
3361
|
+
if (savings > 0.01) return { labelSlug: longest.slug, percent: Math.round(savings * 100) };
|
|
3362
|
+
}
|
|
3363
|
+
return null;
|
|
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 } = useSubscription(product);
|
|
3385
|
+
const { openCheckout } = useCheckout();
|
|
3386
|
+
const l10n = useLocalization();
|
|
3387
|
+
const showError = useErrorResolver();
|
|
3388
|
+
const [selectedLabelSlug, setSelectedLabelSlug] = useState19(defaultLabel ?? null);
|
|
3389
|
+
const [busyPriceId, setBusyPriceId] = useState19(null);
|
|
3390
|
+
const [checkoutError, setCheckoutError] = useState19(null);
|
|
3391
|
+
const labels = useMemo2(() => collectLabels(plans), [plans]);
|
|
3392
|
+
const savings = useMemo2(() => labelSavings(plans, labels), [plans, labels]);
|
|
3393
|
+
useEffect7(() => {
|
|
3394
|
+
if (labels.length === 0) return;
|
|
3395
|
+
if (selectedLabelSlug && labels.some((l) => l.slug === selectedLabelSlug)) return;
|
|
3396
|
+
setSelectedLabelSlug(labels[0].slug);
|
|
3397
|
+
}, [labels]);
|
|
3398
|
+
const handleSelectPlan = async (plan) => {
|
|
3399
|
+
const price = plan.prices.find((p) => p.label.slug === selectedLabelSlug) ?? plan.prices[0];
|
|
3400
|
+
if (!price) return;
|
|
3401
|
+
if (onSelectPlan) {
|
|
3402
|
+
onSelectPlan(plan, price);
|
|
3403
|
+
return;
|
|
3404
|
+
}
|
|
3405
|
+
setCheckoutError(null);
|
|
3406
|
+
setBusyPriceId(price.id);
|
|
3407
|
+
try {
|
|
3408
|
+
await openCheckout(price.id, {
|
|
3409
|
+
successUrl: checkoutSuccessUrl,
|
|
3410
|
+
cancelUrl: checkoutCancelUrl
|
|
3411
|
+
});
|
|
3412
|
+
} catch (e) {
|
|
3413
|
+
setCheckoutError(showError(e));
|
|
3414
|
+
setBusyPriceId(null);
|
|
3415
|
+
}
|
|
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: checkoutError }) : null,
|
|
3429
|
+
children
|
|
3430
|
+
] }) });
|
|
3431
|
+
}
|
|
3432
|
+
function LabelSwitch() {
|
|
3433
|
+
const { labels, selectedLabelSlug, setSelectedLabelSlug, savings } = usePricingTable();
|
|
3434
|
+
const l10n = useLocalization();
|
|
3435
|
+
const containerRef = useRef4(null);
|
|
3436
|
+
const buttonRefs = useRef4(/* @__PURE__ */ new Map());
|
|
3437
|
+
const [thumb, setThumb] = useState19(null);
|
|
3438
|
+
useEffect7(() => {
|
|
3439
|
+
const measure = () => {
|
|
3440
|
+
const container = containerRef.current;
|
|
3441
|
+
const button = selectedLabelSlug ? buttonRefs.current.get(selectedLabelSlug) : null;
|
|
3442
|
+
if (!container || !button) return;
|
|
3443
|
+
const containerBox = container.getBoundingClientRect();
|
|
3444
|
+
const buttonBox = button.getBoundingClientRect();
|
|
3445
|
+
setThumb({ left: buttonBox.left - containerBox.left, width: buttonBox.width });
|
|
3446
|
+
};
|
|
3447
|
+
measure();
|
|
3448
|
+
window.addEventListener("resize", measure);
|
|
3449
|
+
return () => window.removeEventListener("resize", measure);
|
|
3450
|
+
}, [selectedLabelSlug, labels]);
|
|
3451
|
+
if (labels.length < 2) return null;
|
|
3452
|
+
return /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-switch-wrap", children: /* @__PURE__ */ jsxs18(
|
|
3453
|
+
"div",
|
|
3454
|
+
{
|
|
3455
|
+
ref: containerRef,
|
|
3456
|
+
className: "kerne-pricing-switch",
|
|
3457
|
+
role: "group",
|
|
3458
|
+
"aria-label": l10n.billing.intervalGroupLabel,
|
|
3459
|
+
children: [
|
|
3460
|
+
thumb ? /* @__PURE__ */ jsx19(
|
|
3461
|
+
"span",
|
|
3462
|
+
{
|
|
3463
|
+
className: "kerne-pricing-switch-thumb",
|
|
3464
|
+
style: { transform: `translateX(${thumb.left}px)`, width: `${thumb.width}px` },
|
|
3465
|
+
"aria-hidden": "true"
|
|
3466
|
+
}
|
|
3467
|
+
) : null,
|
|
3468
|
+
labels.map((label) => /* @__PURE__ */ jsxs18(
|
|
3469
|
+
"button",
|
|
3470
|
+
{
|
|
3471
|
+
ref: (el) => {
|
|
3472
|
+
if (el) buttonRefs.current.set(label.slug, el);
|
|
3473
|
+
else buttonRefs.current.delete(label.slug);
|
|
3474
|
+
},
|
|
3475
|
+
type: "button",
|
|
3476
|
+
className: "kerne-pricing-switch-option",
|
|
3477
|
+
"aria-pressed": label.slug === selectedLabelSlug,
|
|
3478
|
+
onClick: () => setSelectedLabelSlug(label.slug),
|
|
3479
|
+
children: [
|
|
3480
|
+
label.name,
|
|
3481
|
+
savings && savings.labelSlug === label.slug ? /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-switch-save", children: fill(l10n.pricing.save, { percent: savings.percent }) }) : null
|
|
3482
|
+
]
|
|
3483
|
+
},
|
|
3484
|
+
label.slug
|
|
3485
|
+
))
|
|
3486
|
+
]
|
|
3487
|
+
}
|
|
3488
|
+
) });
|
|
3489
|
+
}
|
|
3490
|
+
function useAnimatedAmount(target, durationMs = 420) {
|
|
3491
|
+
const [displayed, setDisplayed] = useState19(target);
|
|
3492
|
+
const displayedRef = useRef4(target);
|
|
3493
|
+
const frameRef = useRef4();
|
|
3494
|
+
useEffect7(() => {
|
|
3495
|
+
if (displayedRef.current === target) return;
|
|
3496
|
+
const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
3497
|
+
if (reduceMotion) {
|
|
3498
|
+
displayedRef.current = target;
|
|
3499
|
+
setDisplayed(target);
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3502
|
+
const from = displayedRef.current;
|
|
3503
|
+
const start = performance.now();
|
|
3504
|
+
const tick = (now) => {
|
|
3505
|
+
const t = Math.min((now - start) / durationMs, 1);
|
|
3506
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
3507
|
+
const value = Math.round(from + (target - from) * eased);
|
|
3508
|
+
displayedRef.current = value;
|
|
3509
|
+
setDisplayed(value);
|
|
3510
|
+
if (t < 1) frameRef.current = requestAnimationFrame(tick);
|
|
3511
|
+
};
|
|
3512
|
+
frameRef.current = requestAnimationFrame(tick);
|
|
3513
|
+
return () => {
|
|
3514
|
+
if (frameRef.current) cancelAnimationFrame(frameRef.current);
|
|
3515
|
+
};
|
|
3516
|
+
}, [target, durationMs]);
|
|
3517
|
+
return displayed;
|
|
3518
|
+
}
|
|
3519
|
+
function Plan({ slug, featured, badge, maxFeatures }) {
|
|
3520
|
+
const { plans, selectedLabelSlug, currentPlanId, busyPriceId, onSelectPlan } = usePricingTable();
|
|
3521
|
+
const l10n = useLocalization();
|
|
3522
|
+
const plan = plans.find((p) => p.slug === slug);
|
|
3523
|
+
useEffect7(() => {
|
|
3524
|
+
if (!plan && process.env.NODE_ENV === "development") {
|
|
3525
|
+
console.warn(
|
|
3526
|
+
`[Kerne] <PricingTable.Plan slug="${slug}"> - no plan with this slug on the loaded product.`
|
|
3527
|
+
);
|
|
3528
|
+
}
|
|
3529
|
+
}, [plan, slug]);
|
|
3530
|
+
const price = plan?.prices.find((p) => p.label.slug === selectedLabelSlug) ?? plan?.prices[0];
|
|
3531
|
+
const resolved = price ? resolvePriceDisplay(price, price.label.display_interval) : null;
|
|
3532
|
+
const animatedAmount = useAnimatedAmount(resolved?.displayAmount ?? 0);
|
|
3533
|
+
if (!plan) return null;
|
|
3534
|
+
const isCurrent = plan.id === currentPlanId;
|
|
3535
|
+
const isBusy = !!price && busyPriceId === price.id;
|
|
3536
|
+
const shownEntitlements = maxFeatures ? plan.entitlements.slice(0, maxFeatures) : plan.entitlements;
|
|
3537
|
+
const remaining = plan.entitlements.length - shownEntitlements.length;
|
|
3538
|
+
const billedCaption = resolved && resolved.displayInterval !== resolved.billedInterval ? fill(l10n.pricing.billedAt, {
|
|
3539
|
+
amount: formatMoney(resolved.billedAmount, price.currency),
|
|
3540
|
+
interval: formatInterval(resolved.billedInterval, l10n.billing)
|
|
3541
|
+
}) : null;
|
|
3542
|
+
return /* @__PURE__ */ jsxs18(
|
|
3543
|
+
"div",
|
|
3544
|
+
{
|
|
3545
|
+
className: "kerne-pricing-card",
|
|
3546
|
+
"data-featured": featured ? "true" : void 0,
|
|
3547
|
+
"data-current": isCurrent ? "true" : void 0,
|
|
3548
|
+
children: [
|
|
3549
|
+
/* @__PURE__ */ jsxs18("div", { className: "kerne-pricing-card-head", children: [
|
|
3550
|
+
badge ? /* @__PURE__ */ jsx19("span", { className: "kerne-pricing-badge", children: badge }) : null,
|
|
3551
|
+
/* @__PURE__ */ jsx19("span", { className: "kerne-pricing-card-name", children: plan.name }),
|
|
3552
|
+
plan.description ? /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-card-desc", children: plan.description }) : null
|
|
3553
|
+
] }),
|
|
3554
|
+
/* @__PURE__ */ jsx19("div", { className: "kerne-pricing-price", children: resolved ? /* @__PURE__ */ jsxs18(Fragment12, { children: [
|
|
3555
|
+
/* @__PURE__ */ jsxs18("div", { className: "kerne-pricing-price-row", children: [
|
|
3556
|
+
/* @__PURE__ */ jsx19("span", { className: "kerne-pricing-amount", children: formatMoney(animatedAmount, price.currency) }),
|
|
3557
|
+
/* @__PURE__ */ jsxs18("span", { className: "kerne-pricing-interval", children: [
|
|
3558
|
+
"/ ",
|
|
3559
|
+
formatInterval(resolved.displayInterval, l10n.billing)
|
|
3560
|
+
] })
|
|
3561
|
+
] }),
|
|
3562
|
+
billedCaption ? /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-billed", children: billedCaption }) : null
|
|
3563
|
+
] }) : /* @__PURE__ */ jsx19("p", { className: "kerne-pricing-billed", children: l10n.billing.notOnInterval }) }),
|
|
3564
|
+
shownEntitlements.length > 0 ? /* @__PURE__ */ jsxs18("ul", { className: "kerne-pricing-features", children: [
|
|
3565
|
+
shownEntitlements.map((e) => /* @__PURE__ */ jsxs18("li", { className: "kerne-pricing-feature", children: [
|
|
3566
|
+
/* @__PURE__ */ jsx19(CheckIcon2, {}),
|
|
3567
|
+
/* @__PURE__ */ jsx19("span", { children: formatEntitlement(e, l10n.pricing.unlimited) })
|
|
3568
|
+
] }, e.feature_key)),
|
|
3569
|
+
remaining > 0 ? /* @__PURE__ */ jsx19("li", { className: "kerne-pricing-feature-more", children: fill(l10n.pricing.moreFeatures, { count: remaining }) }) : null
|
|
3570
|
+
] }) : null,
|
|
3571
|
+
/* @__PURE__ */ jsx19("div", { className: "kerne-pricing-cta", children: isCurrent ? /* @__PURE__ */ jsx19(Button, { variant: "outline", type: "button", disabled: true, children: l10n.billing.current }) : /* @__PURE__ */ jsx19(
|
|
3572
|
+
Button,
|
|
3573
|
+
{
|
|
3574
|
+
variant: featured ? "primary" : "outline",
|
|
3575
|
+
type: "button",
|
|
3576
|
+
disabled: !price || busyPriceId !== null && !isBusy,
|
|
3577
|
+
loading: isBusy,
|
|
3578
|
+
onClick: () => onSelectPlan(plan),
|
|
3579
|
+
children: currentPlanId ? l10n.billing.switchTo : l10n.pricing.getStarted
|
|
3580
|
+
}
|
|
3581
|
+
) })
|
|
3582
|
+
]
|
|
3583
|
+
}
|
|
3584
|
+
);
|
|
3585
|
+
}
|
|
3586
|
+
function CheckIcon2() {
|
|
3587
|
+
return /* @__PURE__ */ jsx19(
|
|
3588
|
+
"svg",
|
|
3589
|
+
{
|
|
3590
|
+
width: "15",
|
|
3591
|
+
height: "15",
|
|
3592
|
+
viewBox: "0 0 24 24",
|
|
3593
|
+
fill: "none",
|
|
3594
|
+
stroke: "currentColor",
|
|
3595
|
+
strokeWidth: "2",
|
|
3596
|
+
"aria-hidden": "true",
|
|
3597
|
+
children: /* @__PURE__ */ jsx19("path", { d: "m5 13 4 4L19 7", strokeLinecap: "round", strokeLinejoin: "round" })
|
|
3598
|
+
}
|
|
3599
|
+
);
|
|
3600
|
+
}
|
|
3601
|
+
function Plans({ featuredSlug, featuredBadge, maxFeatures }) {
|
|
3602
|
+
const { plans } = usePricingTable();
|
|
3603
|
+
return /* @__PURE__ */ jsx19("div", { className: "kerne-pricing-grid", children: plans.map((plan) => /* @__PURE__ */ jsx19(
|
|
3604
|
+
Plan,
|
|
3605
|
+
{
|
|
3606
|
+
slug: plan.slug,
|
|
3607
|
+
featured: plan.slug === featuredSlug,
|
|
3608
|
+
badge: plan.slug === featuredSlug ? featuredBadge : void 0,
|
|
3609
|
+
maxFeatures
|
|
3610
|
+
},
|
|
3611
|
+
plan.id
|
|
3612
|
+
)) });
|
|
3613
|
+
}
|
|
3614
|
+
PricingTable.LabelSwitch = LabelSwitch;
|
|
3615
|
+
PricingTable.Plan = Plan;
|
|
3616
|
+
PricingTable.Plans = Plans;
|
|
3617
|
+
|
|
3111
3618
|
// src/ui/CheckoutResult.tsx
|
|
3112
|
-
import { useEffect as
|
|
3113
|
-
import { jsx as
|
|
3619
|
+
import { useEffect as useEffect8, useRef as useRef5, useState as useState20 } from "react";
|
|
3620
|
+
import { jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
3114
3621
|
var POLL_INTERVAL_MS = 1500;
|
|
3115
3622
|
function CheckoutResult({
|
|
3116
3623
|
outcome = "success",
|
|
@@ -3128,9 +3635,9 @@ function CheckoutResult({
|
|
|
3128
3635
|
const client = useClient();
|
|
3129
3636
|
const { config } = useAuthConfig();
|
|
3130
3637
|
const l10n = useLocalization();
|
|
3131
|
-
const [state, setState] =
|
|
3132
|
-
const confirmed =
|
|
3133
|
-
|
|
3638
|
+
const [state, setState] = useState20("checking");
|
|
3639
|
+
const confirmed = useRef5(false);
|
|
3640
|
+
useEffect8(() => {
|
|
3134
3641
|
if (outcome !== "success") return;
|
|
3135
3642
|
let stopped = false;
|
|
3136
3643
|
const startedAt = Date.now();
|
|
@@ -3161,20 +3668,20 @@ function CheckoutResult({
|
|
|
3161
3668
|
const go = (url, handler) => handler ?? (() => {
|
|
3162
3669
|
if (url) window.location.href = url;
|
|
3163
3670
|
});
|
|
3164
|
-
const continueAction = continueUrl || onContinue ? /* @__PURE__ */
|
|
3165
|
-
const shell = (children) => /* @__PURE__ */
|
|
3166
|
-
logo ? /* @__PURE__ */
|
|
3671
|
+
const continueAction = continueUrl || onContinue ? /* @__PURE__ */ jsx20(Button, { type: "button", onClick: go(continueUrl, onContinue), children: l10n.common.continue }) : void 0;
|
|
3672
|
+
const shell = (children) => /* @__PURE__ */ jsxs19(Shell, { appearance, className, children: [
|
|
3673
|
+
logo ? /* @__PURE__ */ jsx20("div", { className: "kerne-masthead", children: logo }) : null,
|
|
3167
3674
|
children,
|
|
3168
|
-
/* @__PURE__ */
|
|
3675
|
+
/* @__PURE__ */ jsx20(KerneBranding, { show: config?.show_kerne_branding ?? false })
|
|
3169
3676
|
] });
|
|
3170
3677
|
if (outcome === "cancel") {
|
|
3171
3678
|
return shell(
|
|
3172
|
-
/* @__PURE__ */
|
|
3679
|
+
/* @__PURE__ */ jsx20(
|
|
3173
3680
|
Status,
|
|
3174
3681
|
{
|
|
3175
3682
|
tone: "danger",
|
|
3176
3683
|
title: l10n.checkout.cancelTitle,
|
|
3177
|
-
action: pricingUrl || onRetry ? /* @__PURE__ */
|
|
3684
|
+
action: pricingUrl || onRetry ? /* @__PURE__ */ jsx20(Button, { variant: "outline", type: "button", onClick: go(pricingUrl, onRetry), children: l10n.checkout.backToPlans }) : void 0,
|
|
3178
3685
|
children: l10n.checkout.cancelBody
|
|
3179
3686
|
}
|
|
3180
3687
|
)
|
|
@@ -3182,21 +3689,21 @@ function CheckoutResult({
|
|
|
3182
3689
|
}
|
|
3183
3690
|
if (state === "checking") {
|
|
3184
3691
|
return shell(
|
|
3185
|
-
/* @__PURE__ */
|
|
3692
|
+
/* @__PURE__ */ jsx20(Status, { tone: "loading", title: l10n.checkout.confirmingTitle, children: l10n.checkout.confirmingBody })
|
|
3186
3693
|
);
|
|
3187
3694
|
}
|
|
3188
3695
|
if (state === "pending") {
|
|
3189
3696
|
return shell(
|
|
3190
|
-
/* @__PURE__ */
|
|
3697
|
+
/* @__PURE__ */ jsx20(Status, { tone: "success", title: l10n.checkout.pendingTitle, action: continueAction, children: l10n.checkout.pendingBody })
|
|
3191
3698
|
);
|
|
3192
3699
|
}
|
|
3193
3700
|
return shell(
|
|
3194
|
-
/* @__PURE__ */
|
|
3701
|
+
/* @__PURE__ */ jsx20(Status, { tone: "success", title: l10n.checkout.doneTitle, action: continueAction, children: l10n.checkout.doneBody })
|
|
3195
3702
|
);
|
|
3196
3703
|
}
|
|
3197
3704
|
|
|
3198
3705
|
// src/ui/UpgradePrompt.tsx
|
|
3199
|
-
import { Fragment as
|
|
3706
|
+
import { Fragment as Fragment13, jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
3200
3707
|
function UpgradePrompt({
|
|
3201
3708
|
featureKey,
|
|
3202
3709
|
requested,
|
|
@@ -3215,22 +3722,25 @@ function UpgradePrompt({
|
|
|
3215
3722
|
const l10n = useLocalization();
|
|
3216
3723
|
if (isLoading) {
|
|
3217
3724
|
if (hideWhileLoading) return null;
|
|
3218
|
-
return /* @__PURE__ */
|
|
3725
|
+
return /* @__PURE__ */ jsx21(Shell, { appearance, className, children: /* @__PURE__ */ jsx21("div", { className: "kerne-skeleton", style: { height: 80 }, "aria-label": l10n.upgrade.checking }) });
|
|
3219
3726
|
}
|
|
3220
|
-
if (allowed) return /* @__PURE__ */
|
|
3727
|
+
if (allowed) return /* @__PURE__ */ jsx21(Fragment13, { children });
|
|
3221
3728
|
const name = featureName ?? humanizeFeatureKey(featureKey).toLowerCase();
|
|
3222
3729
|
const quotaExhausted = details?.feature_type === "QUOTA" && typeof details.limit === "number" && typeof details.used === "number";
|
|
3223
3730
|
const heading = title ?? fill(quotaExhausted ? l10n.upgrade.quotaTitle : l10n.upgrade.featureTitle, { feature: name });
|
|
3224
3731
|
const body = description ?? (quotaExhausted ? fill(l10n.upgrade.quotaBody, {
|
|
3225
3732
|
used: formatNumber(details.used),
|
|
3226
|
-
limit
|
|
3733
|
+
// `access_limit` is where access actually stops; `limit` is only the
|
|
3734
|
+
// included allocation and can understate it past an overage ceiling.
|
|
3735
|
+
// Falls back to `limit` defensively - should never be needed here.
|
|
3736
|
+
limit: formatNumber(details.access_limit ?? details.limit)
|
|
3227
3737
|
}) : fill(l10n.upgrade.featureBody, { feature: name }));
|
|
3228
|
-
return /* @__PURE__ */
|
|
3229
|
-
/* @__PURE__ */
|
|
3230
|
-
/* @__PURE__ */
|
|
3231
|
-
/* @__PURE__ */
|
|
3738
|
+
return /* @__PURE__ */ jsx21(Shell, { appearance, className, children: /* @__PURE__ */ jsxs20("div", { className: "kerne-status", children: [
|
|
3739
|
+
/* @__PURE__ */ jsxs20("div", { className: "kerne-heading", children: [
|
|
3740
|
+
/* @__PURE__ */ jsx21("h2", { className: "kerne-section-title", style: { fontSize: 17 }, children: heading }),
|
|
3741
|
+
/* @__PURE__ */ jsx21("p", { className: "kerne-status-text", children: body })
|
|
3232
3742
|
] }),
|
|
3233
|
-
upgradeUrl || onUpgradeClick ? /* @__PURE__ */
|
|
3743
|
+
upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx21(
|
|
3234
3744
|
Button,
|
|
3235
3745
|
{
|
|
3236
3746
|
type: "button",
|
|
@@ -3251,6 +3761,7 @@ export {
|
|
|
3251
3761
|
ForgotPasswordForm,
|
|
3252
3762
|
LoginForm,
|
|
3253
3763
|
MagicLinkCallback,
|
|
3764
|
+
PricingTable,
|
|
3254
3765
|
ProfileSection,
|
|
3255
3766
|
RegisterForm,
|
|
3256
3767
|
ResetPasswordForm,
|
|
@@ -3264,5 +3775,6 @@ export {
|
|
|
3264
3775
|
VerifyEmailForm,
|
|
3265
3776
|
WaitlistForm,
|
|
3266
3777
|
defaultLocalization,
|
|
3267
|
-
useLocalization
|
|
3778
|
+
useLocalization,
|
|
3779
|
+
usePricingTable
|
|
3268
3780
|
};
|