@kerne/react 0.1.2 → 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-3PROPZWW.cjs → chunk-IMZFZ3P5.cjs} +122 -30
- package/dist/{chunk-AIUIBAYB.js → chunk-NFJ2S5VJ.js} +110 -18
- package/dist/{i18n-Qketn1RF.d.cts → i18n-Di5qlnL1.d.cts} +44 -0
- package/dist/{i18n-Qketn1RF.d.ts → i18n-Di5qlnL1.d.ts} +44 -0
- package/dist/index.cjs +16 -20
- package/dist/index.d.cts +63 -13
- package/dist/index.d.ts +63 -13
- package/dist/index.js +11 -15
- package/dist/ui/index.cjs +990 -194
- package/dist/ui/index.d.cts +67 -4
- package/dist/ui/index.d.ts +67 -4
- package/dist/ui/index.js +970 -174
- 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,10 +17,10 @@ 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
|
-
import { useState as
|
|
23
|
+
import { useState as useState10 } from "react";
|
|
23
24
|
|
|
24
25
|
// src/ui/useUrlToken.ts
|
|
25
26
|
import { useState, useEffect } from "react";
|
|
@@ -37,10 +38,10 @@ function useUrlToken(param = "token", override) {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
// src/ui/LoginForm.tsx
|
|
40
|
-
import { useState as
|
|
41
|
+
import { useState as useState3 } from "react";
|
|
41
42
|
|
|
42
43
|
// src/ui/primitives.tsx
|
|
43
|
-
import { useId, useContext, createContext } from "react";
|
|
44
|
+
import { useId, useState as useState2, useContext, createContext } from "react";
|
|
44
45
|
|
|
45
46
|
// src/ui/styles.ts
|
|
46
47
|
var STYLE_ELEMENT_ID = "kerne-ui-styles";
|
|
@@ -53,6 +54,7 @@ var DARK_TOKENS = `
|
|
|
53
54
|
--kerne-panel: #1b1f26;
|
|
54
55
|
--kerne-surface: #14171c;
|
|
55
56
|
--kerne-surface-raised: #2a2f38;
|
|
57
|
+
--kerne-track: #313743;
|
|
56
58
|
--kerne-primary: #f0f1f3;
|
|
57
59
|
--kerne-primary-fg: #16181d;
|
|
58
60
|
--kerne-primary-hover: #d8dade;
|
|
@@ -81,6 +83,11 @@ var CSS = `
|
|
|
81
83
|
variant, the dropdown floating over host content, a pressed segment. */
|
|
82
84
|
--kerne-surface: #ffffff;
|
|
83
85
|
--kerne-surface-raised: #ffffff;
|
|
86
|
+
/* Unfilled part of a meter, and the ground of a disabled button. Distinct
|
|
87
|
+
from --kerne-panel, which is tuned to sit on the page: on a card the two
|
|
88
|
+
are nearly the same colour, so a meter drawn in panel had invisible empty
|
|
89
|
+
segments and read as having fewer steps than it has. */
|
|
90
|
+
--kerne-track: #e3e5e9;
|
|
84
91
|
--kerne-primary: #16171a;
|
|
85
92
|
--kerne-primary-fg: #ffffff;
|
|
86
93
|
--kerne-primary-hover: #2c2e33;
|
|
@@ -226,13 +233,29 @@ var CSS = `
|
|
|
226
233
|
}
|
|
227
234
|
.kerne-subtitle { margin: 0; font-size: 14px; color: var(--kerne-fg-muted); line-height: 1.5; }
|
|
228
235
|
|
|
236
|
+
/* Moving between steps (sign-in -> sign-up -> forgot password) used to be an
|
|
237
|
+
instant swap: the content and the height both jumped at once, which reads
|
|
238
|
+
as a page reload rather than one flow. */
|
|
239
|
+
@keyframes kerne-step-in {
|
|
240
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
241
|
+
to { opacity: 1; transform: none; }
|
|
242
|
+
}
|
|
243
|
+
.kerne-step { animation: kerne-step-in .18s ease-out; }
|
|
244
|
+
@media (prefers-reduced-motion: reduce) {
|
|
245
|
+
.kerne-step { animation: none; }
|
|
246
|
+
}
|
|
247
|
+
|
|
229
248
|
/* ---------- Stack ---------- */
|
|
230
249
|
.kerne-stack { display: flex; flex-direction: column; gap: 16px; }
|
|
250
|
+
.kerne-inline-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
231
251
|
.kerne-form { display: flex; flex-direction: column; gap: 12px; }
|
|
232
252
|
.kerne-field { display: flex; flex-direction: column; gap: 6px; }
|
|
233
253
|
.kerne-label { font-size: 14px; font-weight: 500; color: var(--kerne-fg); }
|
|
234
254
|
.kerne-label-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
|
|
235
255
|
|
|
256
|
+
/* Positioning context for the reveal toggle, which sits inside the input box. */
|
|
257
|
+
.kerne-control { position: relative; display: flex; }
|
|
258
|
+
|
|
236
259
|
.kerne-input {
|
|
237
260
|
font: inherit;
|
|
238
261
|
font-size: 14px;
|
|
@@ -245,6 +268,57 @@ var CSS = `
|
|
|
245
268
|
width: 100%;
|
|
246
269
|
transition: border-color .12s ease, box-shadow .12s ease;
|
|
247
270
|
}
|
|
271
|
+
/* Room for the toggle, so a long password never runs underneath it. */
|
|
272
|
+
.kerne-input[data-kerne-reveal="true"] { padding-right: 42px; }
|
|
273
|
+
|
|
274
|
+
.kerne-reveal {
|
|
275
|
+
position: absolute;
|
|
276
|
+
top: 0; right: 0; bottom: 0;
|
|
277
|
+
width: 40px;
|
|
278
|
+
display: flex; align-items: center; justify-content: center;
|
|
279
|
+
background: none; border: none; padding: 0; cursor: pointer;
|
|
280
|
+
color: var(--kerne-fg-subtle);
|
|
281
|
+
border-radius: var(--kerne-radius);
|
|
282
|
+
transition: color .12s ease;
|
|
283
|
+
}
|
|
284
|
+
.kerne-reveal:hover:not(:disabled) { color: var(--kerne-fg); }
|
|
285
|
+
.kerne-reveal:disabled { cursor: not-allowed; opacity: .5; }
|
|
286
|
+
.kerne-reveal:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: -2px; }
|
|
287
|
+
|
|
288
|
+
.kerne-field-note { font-size: 12.5px; line-height: 1.4; color: var(--kerne-fg-muted); margin: 0; }
|
|
289
|
+
.kerne-field-error { font-size: 12.5px; line-height: 1.4; color: var(--kerne-danger); margin: 0; }
|
|
290
|
+
/* Caps Lock is a "you are about to get this wrong" signal, not background
|
|
291
|
+
guidance - in muted grey it sat at the same weight as a hint and got read
|
|
292
|
+
as one. */
|
|
293
|
+
.kerne-field-note[data-kerne-warn="true"] { color: var(--kerne-warning); }
|
|
294
|
+
|
|
295
|
+
/* ---------- Password rules ---------- */
|
|
296
|
+
/* Unmet stays at hint weight rather than turning red: nothing is wrong yet,
|
|
297
|
+
the user is still typing. Only the tick earns a color. */
|
|
298
|
+
.kerne-rules { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
|
|
299
|
+
.kerne-rule {
|
|
300
|
+
display: flex; align-items: center; gap: 6px;
|
|
301
|
+
font-size: 12.5px; line-height: 1.4; color: var(--kerne-fg-muted);
|
|
302
|
+
}
|
|
303
|
+
.kerne-rule[data-met] { color: var(--kerne-success); }
|
|
304
|
+
|
|
305
|
+
/* ---------- Password strength ---------- */
|
|
306
|
+
.kerne-strength { display: flex; flex-direction: column; gap: 5px; }
|
|
307
|
+
.kerne-strength-track { display: flex; gap: 4px; }
|
|
308
|
+
.kerne-strength-step {
|
|
309
|
+
flex: 1; height: 3px; border-radius: 999px;
|
|
310
|
+
background: var(--kerne-track);
|
|
311
|
+
transition: background-color .18s ease;
|
|
312
|
+
}
|
|
313
|
+
.kerne-strength-label { font-size: 12px; color: var(--kerne-fg-muted); }
|
|
314
|
+
.kerne-strength[data-score="0"] .kerne-strength-step[data-on],
|
|
315
|
+
.kerne-strength[data-score="1"] .kerne-strength-step[data-on] { background: var(--kerne-danger); }
|
|
316
|
+
.kerne-strength[data-score="2"] .kerne-strength-step[data-on] { background: var(--kerne-warning); }
|
|
317
|
+
.kerne-strength[data-score="3"] .kerne-strength-step[data-on],
|
|
318
|
+
.kerne-strength[data-score="4"] .kerne-strength-step[data-on] { background: var(--kerne-success); }
|
|
319
|
+
@media (prefers-reduced-motion: reduce) {
|
|
320
|
+
.kerne-strength-step { transition: none; }
|
|
321
|
+
}
|
|
248
322
|
.kerne-input::placeholder { color: var(--kerne-fg-subtle); }
|
|
249
323
|
.kerne-input:hover:not(:disabled):not(:focus) { border-color: var(--kerne-fg-subtle); }
|
|
250
324
|
.kerne-input:focus {
|
|
@@ -279,6 +353,20 @@ var CSS = `
|
|
|
279
353
|
}
|
|
280
354
|
.kerne-button:focus-visible { outline: 2px solid var(--kerne-accent); outline-offset: 2px; }
|
|
281
355
|
.kerne-button:disabled { opacity: .5; cursor: not-allowed; }
|
|
356
|
+
/* A disabled primary drops the brand entirely rather than wearing it at half
|
|
357
|
+
opacity: on a dark theme the light primary faded into an opaque grey slab
|
|
358
|
+
that read as an enabled button, and a brand colour has no business marking
|
|
359
|
+
an action you cannot take. Loading is excluded - it is still the real
|
|
360
|
+
button, just busy. */
|
|
361
|
+
.kerne-button-primary:disabled:not([data-kerne-loading="true"]) {
|
|
362
|
+
opacity: 1;
|
|
363
|
+
background: var(--kerne-track);
|
|
364
|
+
color: var(--kerne-fg-subtle);
|
|
365
|
+
}
|
|
366
|
+
/* A button mid-request is busy, not unavailable - dimming it to the disabled
|
|
367
|
+
opacity reads as "this stopped working". */
|
|
368
|
+
.kerne-button[data-kerne-loading="true"] { opacity: 1; cursor: progress; }
|
|
369
|
+
.kerne-button .kerne-spinner { width: 15px; height: 15px; border-width: 2px; }
|
|
282
370
|
.kerne-button-primary { background: var(--kerne-primary); color: var(--kerne-primary-fg); }
|
|
283
371
|
.kerne-button-primary:hover:not(:disabled) { background: var(--kerne-primary-hover); }
|
|
284
372
|
.kerne-button-outline {
|
|
@@ -417,7 +505,7 @@ var CSS = `
|
|
|
417
505
|
/* Digits line up between rows instead of jittering as counts change. */
|
|
418
506
|
font-variant-numeric: tabular-nums;
|
|
419
507
|
}
|
|
420
|
-
.kerne-meter-track { height: 6px; border-radius: 999px; background: var(--kerne-
|
|
508
|
+
.kerne-meter-track { height: 6px; border-radius: 999px; background: var(--kerne-track); overflow: hidden; }
|
|
421
509
|
.kerne-meter-fill { height: 100%; border-radius: 999px; background: var(--kerne-fg); transition: width .3s ease; }
|
|
422
510
|
.kerne-meter[data-level="warning"] .kerne-meter-fill { background: var(--kerne-warning); }
|
|
423
511
|
.kerne-meter[data-level="critical"] .kerne-meter-fill { background: var(--kerne-danger); }
|
|
@@ -426,6 +514,126 @@ var CSS = `
|
|
|
426
514
|
.kerne-meter-note { font-size: 12px; color: var(--kerne-fg-subtle); margin: 0; }
|
|
427
515
|
@media (prefers-reduced-motion: reduce) { .kerne-meter-fill { transition: none; } }
|
|
428
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
|
+
|
|
429
637
|
/* ---------- Sections (profile, settings) ---------- */
|
|
430
638
|
.kerne-section-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
|
|
431
639
|
.kerne-section-title { font-size: 15px; font-weight: 600; margin: 0; }
|
|
@@ -577,6 +785,43 @@ function resolveTheme(appearance) {
|
|
|
577
785
|
return appearance?.theme ?? "light";
|
|
578
786
|
}
|
|
579
787
|
|
|
788
|
+
// src/ui/password.ts
|
|
789
|
+
var MIN_PASSWORD_LENGTH = 8;
|
|
790
|
+
var BASELINE_POLICY = {
|
|
791
|
+
min_length: MIN_PASSWORD_LENGTH,
|
|
792
|
+
require_uppercase: false,
|
|
793
|
+
require_number: false,
|
|
794
|
+
require_symbol: false
|
|
795
|
+
};
|
|
796
|
+
function resolvePolicy(config) {
|
|
797
|
+
return config?.password_policy ?? BASELINE_POLICY;
|
|
798
|
+
}
|
|
799
|
+
function passwordRules(password, policy) {
|
|
800
|
+
const rules = [{ key: "min_length", met: password.length >= policy.min_length }];
|
|
801
|
+
if (policy.require_uppercase) {
|
|
802
|
+
rules.push({ key: "require_uppercase", met: /[A-Z]/.test(password) });
|
|
803
|
+
}
|
|
804
|
+
if (policy.require_number) {
|
|
805
|
+
rules.push({ key: "require_number", met: /\d/.test(password) });
|
|
806
|
+
}
|
|
807
|
+
if (policy.require_symbol) {
|
|
808
|
+
rules.push({ key: "require_symbol", met: /[^A-Za-z0-9]/.test(password) });
|
|
809
|
+
}
|
|
810
|
+
return rules;
|
|
811
|
+
}
|
|
812
|
+
function meetsPolicy(password, policy) {
|
|
813
|
+
return passwordRules(password, policy).every((rule) => rule.met);
|
|
814
|
+
}
|
|
815
|
+
function passwordStrength(password) {
|
|
816
|
+
if (password.length < MIN_PASSWORD_LENGTH) return 0;
|
|
817
|
+
const variety = [/[a-z]/, /[A-Z]/, /\d/, /[^A-Za-z0-9]/].filter((re) => re.test(password)).length;
|
|
818
|
+
let score = 1;
|
|
819
|
+
if (password.length >= 12) score++;
|
|
820
|
+
if (password.length >= 16) score++;
|
|
821
|
+
if (variety >= 3) score++;
|
|
822
|
+
return Math.min(score, 4);
|
|
823
|
+
}
|
|
824
|
+
|
|
580
825
|
// src/ui/primitives.tsx
|
|
581
826
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
582
827
|
var InsideShell = createContext(false);
|
|
@@ -635,6 +880,10 @@ function Masthead({
|
|
|
635
880
|
] })
|
|
636
881
|
] });
|
|
637
882
|
}
|
|
883
|
+
function prefersAutoFocus() {
|
|
884
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
885
|
+
return window.matchMedia("(pointer: fine)").matches;
|
|
886
|
+
}
|
|
638
887
|
function Field({
|
|
639
888
|
label,
|
|
640
889
|
type,
|
|
@@ -645,55 +894,185 @@ function Field({
|
|
|
645
894
|
disabled,
|
|
646
895
|
required,
|
|
647
896
|
invalid,
|
|
897
|
+
error,
|
|
648
898
|
action,
|
|
649
899
|
code,
|
|
650
|
-
autoFocus
|
|
900
|
+
autoFocus,
|
|
901
|
+
children
|
|
651
902
|
}) {
|
|
652
903
|
const id = useId();
|
|
904
|
+
const errorId = `${id}-error`;
|
|
905
|
+
const noteId = `${id}-note`;
|
|
906
|
+
const l10n = useLocalization();
|
|
907
|
+
const isPassword = type === "password";
|
|
908
|
+
const [revealed, setRevealed] = useState2(false);
|
|
909
|
+
const [capsLock, setCapsLock] = useState2(false);
|
|
910
|
+
const trackCapsLock = (e) => {
|
|
911
|
+
if (!isPassword) return;
|
|
912
|
+
setCapsLock(e.getModifierState?.("CapsLock") ?? false);
|
|
913
|
+
};
|
|
914
|
+
const describedBy = [error ? errorId : null, children ? noteId : null].filter(Boolean).join(" ");
|
|
653
915
|
return /* @__PURE__ */ jsxs("div", { className: "kerne-field", children: [
|
|
654
916
|
action ? /* @__PURE__ */ jsxs("div", { className: "kerne-label-row", children: [
|
|
655
917
|
/* @__PURE__ */ jsx("label", { className: "kerne-label", htmlFor: id, children: label }),
|
|
656
918
|
action
|
|
657
919
|
] }) : /* @__PURE__ */ jsx("label", { className: "kerne-label", htmlFor: id, children: label }),
|
|
658
|
-
/* @__PURE__ */
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
920
|
+
/* @__PURE__ */ jsxs("div", { className: "kerne-control", children: [
|
|
921
|
+
/* @__PURE__ */ jsx(
|
|
922
|
+
"input",
|
|
923
|
+
{
|
|
924
|
+
id,
|
|
925
|
+
className: "kerne-input",
|
|
926
|
+
type: isPassword && revealed ? "text" : type,
|
|
927
|
+
value,
|
|
928
|
+
onChange: (e) => onChange(e.target.value),
|
|
929
|
+
onKeyUp: trackCapsLock,
|
|
930
|
+
onBlur: () => setCapsLock(false),
|
|
931
|
+
autoComplete,
|
|
932
|
+
placeholder,
|
|
933
|
+
disabled,
|
|
934
|
+
required,
|
|
935
|
+
"aria-invalid": invalid || !!error || void 0,
|
|
936
|
+
"aria-describedby": describedBy || void 0,
|
|
937
|
+
"data-kerne-code": code ? "true" : void 0,
|
|
938
|
+
"data-kerne-reveal": isPassword ? "true" : void 0,
|
|
939
|
+
autoFocus: autoFocus && prefersAutoFocus()
|
|
940
|
+
}
|
|
941
|
+
),
|
|
942
|
+
isPassword ? /* @__PURE__ */ jsx(
|
|
943
|
+
"button",
|
|
944
|
+
{
|
|
945
|
+
type: "button",
|
|
946
|
+
className: "kerne-reveal",
|
|
947
|
+
onClick: () => setRevealed((v) => !v),
|
|
948
|
+
disabled,
|
|
949
|
+
"aria-label": revealed ? l10n.common.hidePassword : l10n.common.showPassword,
|
|
950
|
+
"aria-pressed": revealed,
|
|
951
|
+
tabIndex: -1,
|
|
952
|
+
children: revealed ? /* @__PURE__ */ jsx(EyeOffIcon, {}) : /* @__PURE__ */ jsx(EyeIcon, {})
|
|
953
|
+
}
|
|
954
|
+
) : null
|
|
955
|
+
] }),
|
|
956
|
+
children ? /* @__PURE__ */ jsx("div", { id: noteId, children }) : null,
|
|
957
|
+
capsLock ? /* @__PURE__ */ jsx("p", { className: "kerne-field-note", "data-kerne-warn": "true", role: "status", children: l10n.common.capsLockOn }) : null,
|
|
958
|
+
error ? /* @__PURE__ */ jsx("p", { className: "kerne-field-error", id: errorId, role: "alert", children: error }) : null
|
|
675
959
|
] });
|
|
676
960
|
}
|
|
961
|
+
function EyeIcon() {
|
|
962
|
+
return /* @__PURE__ */ jsxs(
|
|
963
|
+
"svg",
|
|
964
|
+
{
|
|
965
|
+
width: "16",
|
|
966
|
+
height: "16",
|
|
967
|
+
viewBox: "0 0 24 24",
|
|
968
|
+
fill: "none",
|
|
969
|
+
stroke: "currentColor",
|
|
970
|
+
strokeWidth: "2",
|
|
971
|
+
"aria-hidden": "true",
|
|
972
|
+
children: [
|
|
973
|
+
/* @__PURE__ */ jsx("path", { d: "M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z", strokeLinejoin: "round" }),
|
|
974
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" })
|
|
975
|
+
]
|
|
976
|
+
}
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
function EyeOffIcon() {
|
|
980
|
+
return /* @__PURE__ */ jsxs(
|
|
981
|
+
"svg",
|
|
982
|
+
{
|
|
983
|
+
width: "16",
|
|
984
|
+
height: "16",
|
|
985
|
+
viewBox: "0 0 24 24",
|
|
986
|
+
fill: "none",
|
|
987
|
+
stroke: "currentColor",
|
|
988
|
+
strokeWidth: "2",
|
|
989
|
+
"aria-hidden": "true",
|
|
990
|
+
children: [
|
|
991
|
+
/* @__PURE__ */ jsx(
|
|
992
|
+
"path",
|
|
993
|
+
{
|
|
994
|
+
d: "M10.6 6.2A9.9 9.9 0 0 1 12 6c6.4 0 10 7 10 7a15.7 15.7 0 0 1-3 3.7M6.2 7.4A15.6 15.6 0 0 0 2 13s3.6 7 10 7a9.7 9.7 0 0 0 4.3-1",
|
|
995
|
+
strokeLinecap: "round",
|
|
996
|
+
strokeLinejoin: "round"
|
|
997
|
+
}
|
|
998
|
+
),
|
|
999
|
+
/* @__PURE__ */ jsx("path", { d: "m3 3 18 18", strokeLinecap: "round" })
|
|
1000
|
+
]
|
|
1001
|
+
}
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
677
1004
|
function Button({
|
|
678
1005
|
children,
|
|
679
1006
|
variant = "primary",
|
|
680
1007
|
disabled,
|
|
1008
|
+
loading,
|
|
681
1009
|
type = "submit",
|
|
682
1010
|
onClick,
|
|
683
1011
|
ariaLabel
|
|
684
1012
|
}) {
|
|
685
|
-
return /* @__PURE__ */
|
|
1013
|
+
return /* @__PURE__ */ jsxs(
|
|
686
1014
|
"button",
|
|
687
1015
|
{
|
|
688
1016
|
type,
|
|
689
1017
|
className: `kerne-button kerne-button-${variant}`,
|
|
690
|
-
disabled,
|
|
1018
|
+
disabled: disabled || loading,
|
|
691
1019
|
onClick,
|
|
692
1020
|
"aria-label": ariaLabel,
|
|
693
|
-
|
|
1021
|
+
"aria-busy": loading || void 0,
|
|
1022
|
+
"data-kerne-loading": loading ? "true" : void 0,
|
|
1023
|
+
children: [
|
|
1024
|
+
loading ? /* @__PURE__ */ jsx("span", { className: "kerne-spinner", "aria-hidden": "true" }) : null,
|
|
1025
|
+
children
|
|
1026
|
+
]
|
|
694
1027
|
}
|
|
695
1028
|
);
|
|
696
1029
|
}
|
|
1030
|
+
function PasswordStrength({
|
|
1031
|
+
password,
|
|
1032
|
+
policy
|
|
1033
|
+
}) {
|
|
1034
|
+
const l10n = useLocalization();
|
|
1035
|
+
const resolved = policy ?? resolvePolicy(null);
|
|
1036
|
+
if (!meetsPolicy(password, resolved)) {
|
|
1037
|
+
const labels2 = {
|
|
1038
|
+
min_length: fill(l10n.common.passwordHint, { count: resolved.min_length }),
|
|
1039
|
+
require_uppercase: l10n.common.passwordRequireUppercase,
|
|
1040
|
+
require_number: l10n.common.passwordRequireNumber,
|
|
1041
|
+
require_symbol: l10n.common.passwordRequireSymbol
|
|
1042
|
+
};
|
|
1043
|
+
const rules = passwordRules(password, resolved);
|
|
1044
|
+
if (rules.length === 1) {
|
|
1045
|
+
return /* @__PURE__ */ jsx("p", { className: "kerne-field-note", children: labels2[rules[0].key] });
|
|
1046
|
+
}
|
|
1047
|
+
return /* @__PURE__ */ jsx("ul", { className: "kerne-rules", children: rules.map((rule) => /* @__PURE__ */ jsxs("li", { className: "kerne-rule", "data-met": rule.met ? "true" : void 0, children: [
|
|
1048
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: rule.met ? "\u2713" : "\xB7" }),
|
|
1049
|
+
labels2[rule.key]
|
|
1050
|
+
] }, rule.key)) });
|
|
1051
|
+
}
|
|
1052
|
+
const score = passwordStrength(password);
|
|
1053
|
+
const labels = [
|
|
1054
|
+
l10n.common.strengthWeak,
|
|
1055
|
+
l10n.common.strengthWeak,
|
|
1056
|
+
l10n.common.strengthFair,
|
|
1057
|
+
l10n.common.strengthGood,
|
|
1058
|
+
l10n.common.strengthStrong
|
|
1059
|
+
];
|
|
1060
|
+
return /* @__PURE__ */ jsxs("div", { className: "kerne-strength", "data-score": score, children: [
|
|
1061
|
+
/* @__PURE__ */ jsx("div", { className: "kerne-strength-track", "aria-hidden": "true", children: [1, 2, 3, 4].map((step) => /* @__PURE__ */ jsx(
|
|
1062
|
+
"span",
|
|
1063
|
+
{
|
|
1064
|
+
className: "kerne-strength-step",
|
|
1065
|
+
"data-on": score >= step ? "true" : void 0
|
|
1066
|
+
},
|
|
1067
|
+
step
|
|
1068
|
+
)) }),
|
|
1069
|
+
/* @__PURE__ */ jsxs("span", { className: "kerne-strength-label", children: [
|
|
1070
|
+
l10n.common.strengthLabel,
|
|
1071
|
+
": ",
|
|
1072
|
+
labels[score]
|
|
1073
|
+
] })
|
|
1074
|
+
] });
|
|
1075
|
+
}
|
|
697
1076
|
function MethodSwitch({
|
|
698
1077
|
onClick,
|
|
699
1078
|
children
|
|
@@ -746,7 +1125,7 @@ function Panel({
|
|
|
746
1125
|
"div",
|
|
747
1126
|
{
|
|
748
1127
|
className: `kerne-panel${danger ? " kerne-panel-danger" : ""}${variant === "success" ? " kerne-panel-success" : ""}`,
|
|
749
|
-
...danger ? { role: "alert" } : {},
|
|
1128
|
+
...danger ? { role: "alert" } : { role: "status", "aria-live": "polite" },
|
|
750
1129
|
children: [
|
|
751
1130
|
icon,
|
|
752
1131
|
/* @__PURE__ */ jsx("span", { children })
|
|
@@ -911,12 +1290,12 @@ function LoginForm({
|
|
|
911
1290
|
const { config, isLoading: configLoading, error: configError } = useAuthConfig();
|
|
912
1291
|
const l10n = useLocalization();
|
|
913
1292
|
const showError = useErrorResolver();
|
|
914
|
-
const [email, setEmail] =
|
|
915
|
-
const [password, setPassword] =
|
|
916
|
-
const [submitting, setSubmitting] =
|
|
917
|
-
const [error, setError] =
|
|
918
|
-
const [linkSentTo, setLinkSentTo] =
|
|
919
|
-
const [passwordMode, setPasswordMode] =
|
|
1293
|
+
const [email, setEmail] = useState3("");
|
|
1294
|
+
const [password, setPassword] = useState3("");
|
|
1295
|
+
const [submitting, setSubmitting] = useState3(false);
|
|
1296
|
+
const [error, setError] = useState3(null);
|
|
1297
|
+
const [linkSentTo, setLinkSentTo] = useState3(null);
|
|
1298
|
+
const [passwordMode, setPasswordMode] = useState3(false);
|
|
920
1299
|
const shell = (children) => /* @__PURE__ */ jsx2(Shell, { appearance, className, children });
|
|
921
1300
|
const hasOAuth = (config?.oauth_providers.length ?? 0) > 0;
|
|
922
1301
|
const hasCredentials = config?.enable_credentials ?? false;
|
|
@@ -1012,7 +1391,7 @@ function LoginForm({
|
|
|
1012
1391
|
action: forgotPasswordUrl || onForgotPasswordClick ? /* @__PURE__ */ jsx2(TextLink, { href: forgotPasswordUrl, onClick: onForgotPasswordClick, children: l10n.signIn.forgotPassword }) : void 0
|
|
1013
1392
|
}
|
|
1014
1393
|
) : null,
|
|
1015
|
-
/* @__PURE__ */ jsx2(Button, {
|
|
1394
|
+
/* @__PURE__ */ jsx2(Button, { loading: submitting, disabled: !email || usePassword && !password, children: submitLabel }),
|
|
1016
1395
|
hasCredentials && hasMagicLink ? /* @__PURE__ */ jsx2(
|
|
1017
1396
|
MethodSwitch,
|
|
1018
1397
|
{
|
|
@@ -1041,7 +1420,7 @@ function LoginForm({
|
|
|
1041
1420
|
}
|
|
1042
1421
|
|
|
1043
1422
|
// src/ui/RegisterForm.tsx
|
|
1044
|
-
import { useState as
|
|
1423
|
+
import { useState as useState4 } from "react";
|
|
1045
1424
|
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1046
1425
|
function RegisterForm({
|
|
1047
1426
|
onSuccess,
|
|
@@ -1062,17 +1441,18 @@ function RegisterForm({
|
|
|
1062
1441
|
const client = useClient();
|
|
1063
1442
|
const { register, startPasswordless } = useAuth();
|
|
1064
1443
|
const { config, isLoading: configLoading, error: configError } = useAuthConfig();
|
|
1444
|
+
const policy = resolvePolicy(config);
|
|
1065
1445
|
const invitationToken = useUrlToken("token", invitationTokenProp) || void 0;
|
|
1066
1446
|
const l10n = useLocalization();
|
|
1067
1447
|
const showError = useErrorResolver();
|
|
1068
|
-
const [email, setEmail] =
|
|
1069
|
-
const [password, setPassword] =
|
|
1070
|
-
const [name, setName] =
|
|
1071
|
-
const [invitationCode, setInvitationCode] =
|
|
1072
|
-
const [submitting, setSubmitting] =
|
|
1073
|
-
const [error, setError] =
|
|
1074
|
-
const [linkSentTo, setLinkSentTo] =
|
|
1075
|
-
const [passwordMode, setPasswordMode] =
|
|
1448
|
+
const [email, setEmail] = useState4("");
|
|
1449
|
+
const [password, setPassword] = useState4("");
|
|
1450
|
+
const [name, setName] = useState4("");
|
|
1451
|
+
const [invitationCode, setInvitationCode] = useState4("");
|
|
1452
|
+
const [submitting, setSubmitting] = useState4(false);
|
|
1453
|
+
const [error, setError] = useState4(null);
|
|
1454
|
+
const [linkSentTo, setLinkSentTo] = useState4(null);
|
|
1455
|
+
const [passwordMode, setPasswordMode] = useState4(false);
|
|
1076
1456
|
const shell = (children) => /* @__PURE__ */ jsx3(Shell, { appearance, className, children });
|
|
1077
1457
|
const appName = config?.app_name;
|
|
1078
1458
|
const heading = title ?? (appName ? fill(l10n.signUp.titleWithApp, { app: appName }) : l10n.signUp.title);
|
|
@@ -1216,13 +1596,15 @@ function RegisterForm({
|
|
|
1216
1596
|
autoComplete: "new-password",
|
|
1217
1597
|
placeholder: l10n.common.passwordPlaceholder,
|
|
1218
1598
|
disabled: submitting,
|
|
1219
|
-
required: true
|
|
1599
|
+
required: true,
|
|
1600
|
+
children: /* @__PURE__ */ jsx3(PasswordStrength, { password, policy })
|
|
1220
1601
|
}
|
|
1221
1602
|
) : null,
|
|
1222
1603
|
/* @__PURE__ */ jsx3(
|
|
1223
1604
|
Button,
|
|
1224
1605
|
{
|
|
1225
|
-
|
|
1606
|
+
loading: submitting,
|
|
1607
|
+
disabled: !email || needsCode && !invitationCode || usePassword && !meetsPolicy(password, policy),
|
|
1226
1608
|
children: submitLabel
|
|
1227
1609
|
}
|
|
1228
1610
|
),
|
|
@@ -1254,7 +1636,7 @@ function RegisterForm({
|
|
|
1254
1636
|
}
|
|
1255
1637
|
|
|
1256
1638
|
// src/ui/ForgotPasswordForm.tsx
|
|
1257
|
-
import { useState as
|
|
1639
|
+
import { useState as useState5 } from "react";
|
|
1258
1640
|
import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1259
1641
|
function ForgotPasswordForm({
|
|
1260
1642
|
callbackUrl,
|
|
@@ -1271,10 +1653,10 @@ function ForgotPasswordForm({
|
|
|
1271
1653
|
const { config } = useAuthConfig();
|
|
1272
1654
|
const l10n = useLocalization();
|
|
1273
1655
|
const showError = useErrorResolver();
|
|
1274
|
-
const [email, setEmail] =
|
|
1275
|
-
const [submitting, setSubmitting] =
|
|
1276
|
-
const [error, setError] =
|
|
1277
|
-
const [sentTo, setSentTo] =
|
|
1656
|
+
const [email, setEmail] = useState5("");
|
|
1657
|
+
const [submitting, setSubmitting] = useState5(false);
|
|
1658
|
+
const [error, setError] = useState5(null);
|
|
1659
|
+
const [sentTo, setSentTo] = useState5(null);
|
|
1278
1660
|
const handleSubmit = async (e) => {
|
|
1279
1661
|
e.preventDefault();
|
|
1280
1662
|
setError(null);
|
|
@@ -1321,7 +1703,7 @@ function ForgotPasswordForm({
|
|
|
1321
1703
|
autoFocus: true
|
|
1322
1704
|
}
|
|
1323
1705
|
),
|
|
1324
|
-
/* @__PURE__ */ jsx4(Button, {
|
|
1706
|
+
/* @__PURE__ */ jsx4(Button, { loading: submitting, disabled: !email, children: submitting ? l10n.resetPassword.requestSubmitting : l10n.resetPassword.requestSubmit }),
|
|
1325
1707
|
backLine ? /* @__PURE__ */ jsx4("p", { className: "kerne-footer", children: backLine }) : null
|
|
1326
1708
|
] })
|
|
1327
1709
|
] }),
|
|
@@ -1330,7 +1712,7 @@ function ForgotPasswordForm({
|
|
|
1330
1712
|
}
|
|
1331
1713
|
|
|
1332
1714
|
// src/ui/ResetPasswordForm.tsx
|
|
1333
|
-
import { useState as
|
|
1715
|
+
import { useState as useState6 } from "react";
|
|
1334
1716
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1335
1717
|
function ResetPasswordForm({
|
|
1336
1718
|
token: tokenProp,
|
|
@@ -1345,14 +1727,15 @@ function ResetPasswordForm({
|
|
|
1345
1727
|
}) {
|
|
1346
1728
|
const { confirmPasswordReset } = useAuth();
|
|
1347
1729
|
const { config } = useAuthConfig();
|
|
1730
|
+
const policy = resolvePolicy(config);
|
|
1348
1731
|
const token = useUrlToken("token", tokenProp);
|
|
1349
1732
|
const l10n = useLocalization();
|
|
1350
1733
|
const showError = useErrorResolver();
|
|
1351
|
-
const [password, setPassword] =
|
|
1352
|
-
const [confirm, setConfirm] =
|
|
1353
|
-
const [submitting, setSubmitting] =
|
|
1354
|
-
const [error, setError] =
|
|
1355
|
-
const [done, setDone] =
|
|
1734
|
+
const [password, setPassword] = useState6("");
|
|
1735
|
+
const [confirm, setConfirm] = useState6("");
|
|
1736
|
+
const [submitting, setSubmitting] = useState6(false);
|
|
1737
|
+
const [error, setError] = useState6(null);
|
|
1738
|
+
const [done, setDone] = useState6(false);
|
|
1356
1739
|
const heading = title ?? l10n.resetPassword.title;
|
|
1357
1740
|
const mismatch = confirm.length > 0 && password !== confirm;
|
|
1358
1741
|
const signInAction = signInUrl || onSignInClick ? /* @__PURE__ */ jsx5(
|
|
@@ -1416,7 +1799,8 @@ function ResetPasswordForm({
|
|
|
1416
1799
|
placeholder: l10n.common.passwordPlaceholder,
|
|
1417
1800
|
disabled: submitting,
|
|
1418
1801
|
required: true,
|
|
1419
|
-
autoFocus: true
|
|
1802
|
+
autoFocus: true,
|
|
1803
|
+
children: /* @__PURE__ */ jsx5(PasswordStrength, { password, policy })
|
|
1420
1804
|
}
|
|
1421
1805
|
),
|
|
1422
1806
|
/* @__PURE__ */ jsx5(
|
|
@@ -1430,11 +1814,17 @@ function ResetPasswordForm({
|
|
|
1430
1814
|
placeholder: l10n.common.passwordPlaceholder,
|
|
1431
1815
|
disabled: submitting,
|
|
1432
1816
|
required: true,
|
|
1433
|
-
|
|
1817
|
+
error: mismatch ? l10n.resetPassword.mismatch : void 0
|
|
1818
|
+
}
|
|
1819
|
+
),
|
|
1820
|
+
/* @__PURE__ */ jsx5(
|
|
1821
|
+
Button,
|
|
1822
|
+
{
|
|
1823
|
+
loading: submitting,
|
|
1824
|
+
disabled: !meetsPolicy(password, policy) || !confirm || mismatch,
|
|
1825
|
+
children: submitting ? l10n.resetPassword.submitting : l10n.resetPassword.submit
|
|
1434
1826
|
}
|
|
1435
1827
|
),
|
|
1436
|
-
mismatch ? /* @__PURE__ */ jsx5("p", { className: "kerne-footer", children: l10n.resetPassword.mismatch }) : null,
|
|
1437
|
-
/* @__PURE__ */ jsx5(Button, { disabled: submitting || !password || !confirm || mismatch, children: submitting ? l10n.resetPassword.submitting : l10n.resetPassword.submit }),
|
|
1438
1828
|
signInUrl || onSignInClick ? /* @__PURE__ */ jsx5("p", { className: "kerne-footer", children: /* @__PURE__ */ jsx5(TextLink, { href: signInUrl, onClick: onSignInClick, children: l10n.common.backToSignIn }) }) : null
|
|
1439
1829
|
] })
|
|
1440
1830
|
] }),
|
|
@@ -1443,7 +1833,7 @@ function ResetPasswordForm({
|
|
|
1443
1833
|
}
|
|
1444
1834
|
|
|
1445
1835
|
// src/ui/MagicLinkCallback.tsx
|
|
1446
|
-
import { useEffect as useEffect2, useRef, useState as
|
|
1836
|
+
import { useEffect as useEffect2, useRef, useState as useState7 } from "react";
|
|
1447
1837
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1448
1838
|
function MagicLinkCallback({
|
|
1449
1839
|
token: tokenProp,
|
|
@@ -1461,8 +1851,8 @@ function MagicLinkCallback({
|
|
|
1461
1851
|
const token = useUrlToken("token", tokenProp);
|
|
1462
1852
|
const l10n = useLocalization();
|
|
1463
1853
|
const showError = useErrorResolver();
|
|
1464
|
-
const [state, setState] =
|
|
1465
|
-
const [message, setMessage] =
|
|
1854
|
+
const [state, setState] = useState7("pending");
|
|
1855
|
+
const [message, setMessage] = useState7(null);
|
|
1466
1856
|
const consumed = useRef(false);
|
|
1467
1857
|
useEffect2(() => {
|
|
1468
1858
|
if (token === null || consumed.current) return;
|
|
@@ -1502,7 +1892,7 @@ function MagicLinkCallback({
|
|
|
1502
1892
|
}
|
|
1503
1893
|
|
|
1504
1894
|
// src/ui/VerifyEmailForm.tsx
|
|
1505
|
-
import { useEffect as useEffect3, useRef as useRef2, useState as
|
|
1895
|
+
import { useEffect as useEffect3, useRef as useRef2, useState as useState8 } from "react";
|
|
1506
1896
|
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1507
1897
|
function VerifyEmailForm({
|
|
1508
1898
|
token: tokenProp,
|
|
@@ -1519,12 +1909,12 @@ function VerifyEmailForm({
|
|
|
1519
1909
|
const token = useUrlToken("token", tokenProp);
|
|
1520
1910
|
const l10n = useLocalization();
|
|
1521
1911
|
const showError = useErrorResolver();
|
|
1522
|
-
const [code, setCode] =
|
|
1523
|
-
const [submitting, setSubmitting] =
|
|
1524
|
-
const [error, setError] =
|
|
1525
|
-
const [resent, setResent] =
|
|
1526
|
-
const [linkState, setLinkState] =
|
|
1527
|
-
const [verified, setVerified] =
|
|
1912
|
+
const [code, setCode] = useState8("");
|
|
1913
|
+
const [submitting, setSubmitting] = useState8(false);
|
|
1914
|
+
const [error, setError] = useState8(null);
|
|
1915
|
+
const [resent, setResent] = useState8(false);
|
|
1916
|
+
const [linkState, setLinkState] = useState8("idle");
|
|
1917
|
+
const [verified, setVerified] = useState8(false);
|
|
1528
1918
|
const consumed = useRef2(false);
|
|
1529
1919
|
const heading = title ?? l10n.verifyEmail.title;
|
|
1530
1920
|
const settle = async (needsTokenRefresh) => {
|
|
@@ -1618,7 +2008,7 @@ function VerifyEmailForm({
|
|
|
1618
2008
|
autoFocus: true
|
|
1619
2009
|
}
|
|
1620
2010
|
),
|
|
1621
|
-
/* @__PURE__ */ jsx7(Button, {
|
|
2011
|
+
/* @__PURE__ */ jsx7(Button, { loading: submitting, disabled: !code, children: submitting ? l10n.verifyEmail.submitting : l10n.verifyEmail.submit }),
|
|
1622
2012
|
/* @__PURE__ */ jsx7(MethodSwitch, { onClick: handleResend, children: l10n.verifyEmail.resend })
|
|
1623
2013
|
] })
|
|
1624
2014
|
] }),
|
|
@@ -1627,7 +2017,7 @@ function VerifyEmailForm({
|
|
|
1627
2017
|
}
|
|
1628
2018
|
|
|
1629
2019
|
// src/ui/ActivateAccountForm.tsx
|
|
1630
|
-
import { useEffect as useEffect4, useState as
|
|
2020
|
+
import { useEffect as useEffect4, useState as useState9 } from "react";
|
|
1631
2021
|
import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1632
2022
|
function ActivateAccountForm({
|
|
1633
2023
|
token: tokenProp,
|
|
@@ -1643,15 +2033,16 @@ function ActivateAccountForm({
|
|
|
1643
2033
|
const client = useClient();
|
|
1644
2034
|
const { getActivationContext, completeActivation } = useAuth();
|
|
1645
2035
|
const { config } = useAuthConfig();
|
|
2036
|
+
const policy = resolvePolicy(config);
|
|
1646
2037
|
const token = useUrlToken("token", tokenProp);
|
|
1647
2038
|
const l10n = useLocalization();
|
|
1648
2039
|
const showError = useErrorResolver();
|
|
1649
|
-
const [context, setContext] =
|
|
1650
|
-
const [loadError, setLoadError] =
|
|
1651
|
-
const [password, setPassword] =
|
|
1652
|
-
const [confirm, setConfirm] =
|
|
1653
|
-
const [submitting, setSubmitting] =
|
|
1654
|
-
const [error, setError] =
|
|
2040
|
+
const [context, setContext] = useState9(null);
|
|
2041
|
+
const [loadError, setLoadError] = useState9(null);
|
|
2042
|
+
const [password, setPassword] = useState9("");
|
|
2043
|
+
const [confirm, setConfirm] = useState9("");
|
|
2044
|
+
const [submitting, setSubmitting] = useState9(false);
|
|
2045
|
+
const [error, setError] = useState9(null);
|
|
1655
2046
|
const heading = title ?? l10n.activate.title;
|
|
1656
2047
|
const mismatch = confirm.length > 0 && password !== confirm;
|
|
1657
2048
|
useEffect4(() => {
|
|
@@ -1736,7 +2127,8 @@ function ActivateAccountForm({
|
|
|
1736
2127
|
placeholder: l10n.common.passwordPlaceholder,
|
|
1737
2128
|
disabled: submitting,
|
|
1738
2129
|
required: true,
|
|
1739
|
-
autoFocus: true
|
|
2130
|
+
autoFocus: true,
|
|
2131
|
+
children: /* @__PURE__ */ jsx8(PasswordStrength, { password, policy })
|
|
1740
2132
|
}
|
|
1741
2133
|
),
|
|
1742
2134
|
/* @__PURE__ */ jsx8(
|
|
@@ -1750,12 +2142,18 @@ function ActivateAccountForm({
|
|
|
1750
2142
|
placeholder: l10n.common.passwordPlaceholder,
|
|
1751
2143
|
disabled: submitting,
|
|
1752
2144
|
required: true,
|
|
1753
|
-
|
|
2145
|
+
error: mismatch ? l10n.activate.mismatch : void 0
|
|
1754
2146
|
}
|
|
1755
|
-
)
|
|
1756
|
-
mismatch ? /* @__PURE__ */ jsx8("p", { className: "kerne-footer", children: l10n.activate.mismatch }) : null
|
|
2147
|
+
)
|
|
1757
2148
|
] }) : null,
|
|
1758
|
-
/* @__PURE__ */ jsx8(
|
|
2149
|
+
/* @__PURE__ */ jsx8(
|
|
2150
|
+
Button,
|
|
2151
|
+
{
|
|
2152
|
+
loading: submitting,
|
|
2153
|
+
disabled: credentialsMode && (!meetsPolicy(password, policy) || !confirm || mismatch),
|
|
2154
|
+
children: submitting ? l10n.activate.submitting : l10n.activate.submit
|
|
2155
|
+
}
|
|
2156
|
+
)
|
|
1759
2157
|
] })
|
|
1760
2158
|
] }),
|
|
1761
2159
|
/* @__PURE__ */ jsx8(KerneBranding, { show: config?.show_kerne_branding ?? false })
|
|
@@ -1782,7 +2180,7 @@ function AuthFlow({
|
|
|
1782
2180
|
}) {
|
|
1783
2181
|
const { config } = useAuthConfig();
|
|
1784
2182
|
const token = useUrlToken("token");
|
|
1785
|
-
const [internalStep, setInternalStep] =
|
|
2183
|
+
const [internalStep, setInternalStep] = useState10(null);
|
|
1786
2184
|
const go = (next) => {
|
|
1787
2185
|
if (controlledStep === void 0) setInternalStep(next);
|
|
1788
2186
|
onStepChange?.(next);
|
|
@@ -1790,7 +2188,11 @@ function AuthFlow({
|
|
|
1790
2188
|
const tokenStep = token && (initialStep === void 0 || TOKEN_STEPS.includes(initialStep)) ? initialStep ?? "magic-link" : null;
|
|
1791
2189
|
const step = controlledStep ?? internalStep ?? tokenStep ?? initialStep ?? "sign-in";
|
|
1792
2190
|
const canSignUp = allowSignUp && config?.registration_mode !== "CLOSED";
|
|
1793
|
-
const shared = {
|
|
2191
|
+
const shared = {
|
|
2192
|
+
appearance,
|
|
2193
|
+
logo,
|
|
2194
|
+
className: `kerne-step${className ? ` ${className}` : ""}`
|
|
2195
|
+
};
|
|
1794
2196
|
switch (step) {
|
|
1795
2197
|
case "sign-up":
|
|
1796
2198
|
return /* @__PURE__ */ jsx9(
|
|
@@ -1855,7 +2257,7 @@ function AuthFlow({
|
|
|
1855
2257
|
}
|
|
1856
2258
|
|
|
1857
2259
|
// src/ui/ChangePasswordForm.tsx
|
|
1858
|
-
import { useState as
|
|
2260
|
+
import { useState as useState11 } from "react";
|
|
1859
2261
|
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1860
2262
|
function ChangePasswordForm({
|
|
1861
2263
|
onSuccess,
|
|
@@ -1864,15 +2266,17 @@ function ChangePasswordForm({
|
|
|
1864
2266
|
className
|
|
1865
2267
|
}) {
|
|
1866
2268
|
const { updatePassword } = useAuth();
|
|
2269
|
+
const { config } = useAuthConfig();
|
|
2270
|
+
const policy = resolvePolicy(config);
|
|
1867
2271
|
const l10n = useLocalization();
|
|
1868
2272
|
const showError = useErrorResolver();
|
|
1869
2273
|
const heading = title === null ? null : title ?? l10n.security.submit;
|
|
1870
|
-
const [currentPassword, setCurrentPassword] =
|
|
1871
|
-
const [newPassword, setNewPassword] =
|
|
1872
|
-
const [confirm, setConfirm] =
|
|
1873
|
-
const [submitting, setSubmitting] =
|
|
1874
|
-
const [error, setError] =
|
|
1875
|
-
const [done, setDone] =
|
|
2274
|
+
const [currentPassword, setCurrentPassword] = useState11("");
|
|
2275
|
+
const [newPassword, setNewPassword] = useState11("");
|
|
2276
|
+
const [confirm, setConfirm] = useState11("");
|
|
2277
|
+
const [submitting, setSubmitting] = useState11(false);
|
|
2278
|
+
const [error, setError] = useState11(null);
|
|
2279
|
+
const [done, setDone] = useState11(false);
|
|
1876
2280
|
const mismatch = confirm.length > 0 && newPassword !== confirm;
|
|
1877
2281
|
const unchanged = newPassword.length > 0 && newPassword === currentPassword;
|
|
1878
2282
|
const handleSubmit = async (e) => {
|
|
@@ -1924,7 +2328,8 @@ function ChangePasswordForm({
|
|
|
1924
2328
|
placeholder: l10n.common.passwordPlaceholder,
|
|
1925
2329
|
disabled: submitting,
|
|
1926
2330
|
required: true,
|
|
1927
|
-
|
|
2331
|
+
error: unchanged ? l10n.security.sameAsCurrent : void 0,
|
|
2332
|
+
children: /* @__PURE__ */ jsx10(PasswordStrength, { password: newPassword, policy })
|
|
1928
2333
|
}
|
|
1929
2334
|
),
|
|
1930
2335
|
/* @__PURE__ */ jsx10(
|
|
@@ -1938,14 +2343,14 @@ function ChangePasswordForm({
|
|
|
1938
2343
|
placeholder: l10n.common.passwordPlaceholder,
|
|
1939
2344
|
disabled: submitting,
|
|
1940
2345
|
required: true,
|
|
1941
|
-
|
|
2346
|
+
error: mismatch ? l10n.security.mismatch : void 0
|
|
1942
2347
|
}
|
|
1943
2348
|
),
|
|
1944
|
-
unchanged ? /* @__PURE__ */ jsx10("p", { className: "kerne-footer", children: l10n.security.sameAsCurrent }) : mismatch ? /* @__PURE__ */ jsx10("p", { className: "kerne-footer", children: l10n.security.mismatch }) : null,
|
|
1945
2349
|
/* @__PURE__ */ jsx10(
|
|
1946
2350
|
Button,
|
|
1947
2351
|
{
|
|
1948
|
-
|
|
2352
|
+
loading: submitting,
|
|
2353
|
+
disabled: !currentPassword || !meetsPolicy(newPassword, policy) || !confirm || mismatch || unchanged,
|
|
1949
2354
|
children: submitting ? l10n.security.submitting : l10n.security.submit
|
|
1950
2355
|
}
|
|
1951
2356
|
)
|
|
@@ -1955,7 +2360,7 @@ function ChangePasswordForm({
|
|
|
1955
2360
|
}
|
|
1956
2361
|
|
|
1957
2362
|
// src/ui/WaitlistForm.tsx
|
|
1958
|
-
import { useState as
|
|
2363
|
+
import { useState as useState12 } from "react";
|
|
1959
2364
|
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1960
2365
|
function WaitlistForm({
|
|
1961
2366
|
title,
|
|
@@ -1974,11 +2379,11 @@ function WaitlistForm({
|
|
|
1974
2379
|
const { config } = useAuthConfig();
|
|
1975
2380
|
const l10n = useLocalization();
|
|
1976
2381
|
const showError = useErrorResolver();
|
|
1977
|
-
const [email, setEmail] =
|
|
1978
|
-
const [name, setName] =
|
|
1979
|
-
const [submitting, setSubmitting] =
|
|
1980
|
-
const [error, setError] =
|
|
1981
|
-
const [joined, setJoined] =
|
|
2382
|
+
const [email, setEmail] = useState12("");
|
|
2383
|
+
const [name, setName] = useState12("");
|
|
2384
|
+
const [submitting, setSubmitting] = useState12(false);
|
|
2385
|
+
const [error, setError] = useState12(null);
|
|
2386
|
+
const [joined, setJoined] = useState12(false);
|
|
1982
2387
|
const handleSubmit = async (e) => {
|
|
1983
2388
|
e.preventDefault();
|
|
1984
2389
|
setError(null);
|
|
@@ -2035,7 +2440,7 @@ function WaitlistForm({
|
|
|
2035
2440
|
autoFocus: true
|
|
2036
2441
|
}
|
|
2037
2442
|
),
|
|
2038
|
-
/* @__PURE__ */ jsx11(Button, {
|
|
2443
|
+
/* @__PURE__ */ jsx11(Button, { loading: submitting, disabled: !email, children: submitting ? l10n.waitlist.submitting : submitLabel ?? l10n.waitlist.submit })
|
|
2039
2444
|
] })
|
|
2040
2445
|
] }),
|
|
2041
2446
|
legal ? /* @__PURE__ */ jsx11("p", { className: "kerne-legal", children: legal }) : null,
|
|
@@ -2044,7 +2449,7 @@ function WaitlistForm({
|
|
|
2044
2449
|
}
|
|
2045
2450
|
|
|
2046
2451
|
// src/ui/UserButton.tsx
|
|
2047
|
-
import { useEffect as useEffect5, useRef as useRef3, useState as
|
|
2452
|
+
import { useEffect as useEffect5, useRef as useRef3, useState as useState13 } from "react";
|
|
2048
2453
|
import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2049
2454
|
function SignOutButton({
|
|
2050
2455
|
children,
|
|
@@ -2092,7 +2497,7 @@ function UserButton({
|
|
|
2092
2497
|
const { logout, isAuthenticated } = useAuth();
|
|
2093
2498
|
const { user, fullName, initials, email } = useUser();
|
|
2094
2499
|
const l10n = useLocalization();
|
|
2095
|
-
const [open, setOpen] =
|
|
2500
|
+
const [open, setOpen] = useState13(false);
|
|
2096
2501
|
const wrapRef = useRef3(null);
|
|
2097
2502
|
useEffect5(() => {
|
|
2098
2503
|
if (!open) return;
|
|
@@ -2176,24 +2581,24 @@ function UserButton({
|
|
|
2176
2581
|
}
|
|
2177
2582
|
|
|
2178
2583
|
// src/ui/UserProfile.tsx
|
|
2179
|
-
import { useState as
|
|
2584
|
+
import { useState as useState18 } from "react";
|
|
2180
2585
|
|
|
2181
2586
|
// src/ui/profile/ProfileSection.tsx
|
|
2182
|
-
import { useEffect as useEffect6, useState as
|
|
2587
|
+
import { useEffect as useEffect6, useState as useState14 } from "react";
|
|
2183
2588
|
import { Fragment as Fragment6, jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2184
2589
|
function ProfileSection({ verificationCallbackUrl, onSaved }) {
|
|
2185
2590
|
const { updateProfile, sendVerificationEmail, getVerificationStatus } = useAuth();
|
|
2186
2591
|
const { user, fullName, initials, email } = useUser();
|
|
2187
2592
|
const l10n = useLocalization();
|
|
2188
2593
|
const showError = useErrorResolver();
|
|
2189
|
-
const [firstName, setFirstName] =
|
|
2190
|
-
const [lastName, setLastName] =
|
|
2191
|
-
const [saving, setSaving] =
|
|
2192
|
-
const [saved, setSaved] =
|
|
2193
|
-
const [error, setError] =
|
|
2194
|
-
const [verified, setVerified] =
|
|
2195
|
-
const [sending, setSending] =
|
|
2196
|
-
const [sent, setSent] =
|
|
2594
|
+
const [firstName, setFirstName] = useState14("");
|
|
2595
|
+
const [lastName, setLastName] = useState14("");
|
|
2596
|
+
const [saving, setSaving] = useState14(false);
|
|
2597
|
+
const [saved, setSaved] = useState14(false);
|
|
2598
|
+
const [error, setError] = useState14(null);
|
|
2599
|
+
const [verified, setVerified] = useState14(null);
|
|
2600
|
+
const [sending, setSending] = useState14(false);
|
|
2601
|
+
const [sent, setSent] = useState14(false);
|
|
2197
2602
|
useEffect6(() => {
|
|
2198
2603
|
if (!user) return;
|
|
2199
2604
|
setFirstName(user.first_name ?? "");
|
|
@@ -2269,7 +2674,7 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
|
|
|
2269
2674
|
disabled: saving
|
|
2270
2675
|
}
|
|
2271
2676
|
),
|
|
2272
|
-
/* @__PURE__ */ jsx13(Button, {
|
|
2677
|
+
/* @__PURE__ */ jsx13(Button, { loading: saving, children: saving ? l10n.profile.saving : l10n.profile.save })
|
|
2273
2678
|
] })
|
|
2274
2679
|
] }),
|
|
2275
2680
|
/* @__PURE__ */ jsxs12(
|
|
@@ -2283,7 +2688,7 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
|
|
|
2283
2688
|
email,
|
|
2284
2689
|
verified === true ? /* @__PURE__ */ jsx13("span", { className: "kerne-badge", "data-tone": "success", children: l10n.profile.verified }) : verified === false ? /* @__PURE__ */ jsx13("span", { className: "kerne-badge", "data-tone": "warning", children: l10n.profile.unverified }) : null
|
|
2285
2690
|
] }) }),
|
|
2286
|
-
verified === false ? /* @__PURE__ */ jsx13(Button, { variant: "outline", type: "button", onClick: handleResend,
|
|
2691
|
+
verified === false ? /* @__PURE__ */ jsx13(Button, { variant: "outline", type: "button", onClick: handleResend, loading: sending, children: sending ? l10n.common.sending : l10n.profile.verify }) : null
|
|
2287
2692
|
] }),
|
|
2288
2693
|
sent ? /* @__PURE__ */ jsx13(Panel, { variant: "success", children: l10n.profile.verificationSent }) : null
|
|
2289
2694
|
]
|
|
@@ -2293,16 +2698,16 @@ function ProfileSection({ verificationCallbackUrl, onSaved }) {
|
|
|
2293
2698
|
}
|
|
2294
2699
|
|
|
2295
2700
|
// src/ui/profile/SecuritySection.tsx
|
|
2296
|
-
import { useState as
|
|
2701
|
+
import { useState as useState15 } from "react";
|
|
2297
2702
|
import { Fragment as Fragment7, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2298
2703
|
function SecuritySection({ showDeleteAccount = true, onDeleted }) {
|
|
2299
2704
|
const { requestAccountDeletion } = useAuth();
|
|
2300
2705
|
const { config } = useAuthConfig();
|
|
2301
2706
|
const l10n = useLocalization();
|
|
2302
2707
|
const showError = useErrorResolver();
|
|
2303
|
-
const [error, setError] =
|
|
2304
|
-
const [confirmingDelete, setConfirmingDelete] =
|
|
2305
|
-
const [deleting, setDeleting] =
|
|
2708
|
+
const [error, setError] = useState15(null);
|
|
2709
|
+
const [confirmingDelete, setConfirmingDelete] = useState15(false);
|
|
2710
|
+
const [deleting, setDeleting] = useState15(false);
|
|
2306
2711
|
const hasPassword = config?.enable_credentials ?? true;
|
|
2307
2712
|
const handleDelete = async () => {
|
|
2308
2713
|
setError(null);
|
|
@@ -2319,7 +2724,7 @@ function SecuritySection({ showDeleteAccount = true, onDeleted }) {
|
|
|
2319
2724
|
error ? /* @__PURE__ */ jsx14(Panel, { variant: "danger", children: error }) : null,
|
|
2320
2725
|
hasPassword ? /* @__PURE__ */ jsx14(Subsection, { title: l10n.security.passwordTitle, description: l10n.security.passwordDesc, children: /* @__PURE__ */ jsx14(ChangePasswordForm, { title: null }) }) : null,
|
|
2321
2726
|
showDeleteAccount ? /* @__PURE__ */ jsx14(Subsection, { title: l10n.security.deleteTitle, description: l10n.security.deleteDesc, children: confirmingDelete ? /* @__PURE__ */ jsxs13("div", { style: { display: "flex", gap: 10, flexWrap: "wrap" }, children: [
|
|
2322
|
-
/* @__PURE__ */ jsx14(Button, { variant: "danger", type: "button", onClick: handleDelete,
|
|
2727
|
+
/* @__PURE__ */ jsx14(Button, { variant: "danger", type: "button", onClick: handleDelete, loading: deleting, children: deleting ? l10n.security.deleting : l10n.security.deleteConfirm }),
|
|
2323
2728
|
/* @__PURE__ */ jsx14(
|
|
2324
2729
|
Button,
|
|
2325
2730
|
{
|
|
@@ -2335,7 +2740,7 @@ function SecuritySection({ showDeleteAccount = true, onDeleted }) {
|
|
|
2335
2740
|
}
|
|
2336
2741
|
|
|
2337
2742
|
// src/ui/profile/BillingSection.tsx
|
|
2338
|
-
import { useMemo, useState as
|
|
2743
|
+
import { useMemo, useState as useState17 } from "react";
|
|
2339
2744
|
|
|
2340
2745
|
// src/ui/format.ts
|
|
2341
2746
|
function humanizeFeatureKey(key) {
|
|
@@ -2394,15 +2799,45 @@ function formatInterval(interval, labels) {
|
|
|
2394
2799
|
return interval;
|
|
2395
2800
|
}
|
|
2396
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
|
+
}
|
|
2397
2825
|
|
|
2398
2826
|
// src/ui/UsageMeters.tsx
|
|
2399
|
-
import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2400
|
-
function
|
|
2401
|
-
|
|
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";
|
|
2402
2833
|
if (ratio >= 0.9) return "critical";
|
|
2403
2834
|
if (ratio >= 0.75) return "warning";
|
|
2404
2835
|
return "normal";
|
|
2405
2836
|
}
|
|
2837
|
+
function quotaRatio(used, limit) {
|
|
2838
|
+
if (limit <= 0) return used > 0 ? 1 : 0;
|
|
2839
|
+
return Math.min(used / limit, 1);
|
|
2840
|
+
}
|
|
2406
2841
|
function UsageMeters({
|
|
2407
2842
|
title,
|
|
2408
2843
|
featureKeys,
|
|
@@ -2456,10 +2891,13 @@ function UsageMeters({
|
|
|
2456
2891
|
return shell(
|
|
2457
2892
|
/* @__PURE__ */ jsx15("div", { className: "kerne-meters", children: rows.map((e) => {
|
|
2458
2893
|
const unlimited = e.limit === null || e.limit === void 0;
|
|
2459
|
-
const
|
|
2460
|
-
const
|
|
2461
|
-
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);
|
|
2462
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);
|
|
2463
2901
|
return /* @__PURE__ */ jsxs14("div", { className: "kerne-meter", "data-level": level, children: [
|
|
2464
2902
|
/* @__PURE__ */ jsxs14("div", { className: "kerne-meter-head", children: [
|
|
2465
2903
|
/* @__PURE__ */ jsx15("span", { className: "kerne-meter-name", children: nameFor(e.feature_key) }),
|
|
@@ -2477,17 +2915,26 @@ function UsageMeters({
|
|
|
2477
2915
|
children: /* @__PURE__ */ jsx15("div", { className: "kerne-meter-fill", style: { width: `${ratio * 100}%` } })
|
|
2478
2916
|
}
|
|
2479
2917
|
) : null,
|
|
2480
|
-
over &&
|
|
2481
|
-
|
|
2482
|
-
|
|
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
|
|
2483
2930
|
] }, e.feature_key);
|
|
2484
2931
|
}) })
|
|
2485
2932
|
);
|
|
2486
2933
|
}
|
|
2487
2934
|
|
|
2488
2935
|
// src/ui/SubscriptionCard.tsx
|
|
2489
|
-
import { useState as
|
|
2490
|
-
import { Fragment as
|
|
2936
|
+
import { useState as useState16 } from "react";
|
|
2937
|
+
import { Fragment as Fragment9, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
2491
2938
|
var STATUS_TONES = {
|
|
2492
2939
|
ACTIVE: "success",
|
|
2493
2940
|
TRIALING: "success",
|
|
@@ -2503,11 +2950,13 @@ function SubscriptionCard({
|
|
|
2503
2950
|
upgradeUrl,
|
|
2504
2951
|
onUpgradeClick,
|
|
2505
2952
|
hidePortalButton = false,
|
|
2953
|
+
hideCancelButton = false,
|
|
2506
2954
|
appearance,
|
|
2507
2955
|
className
|
|
2508
2956
|
}) {
|
|
2509
|
-
const { subscription, isLoading, error } = useSubscription(productSlug);
|
|
2957
|
+
const { subscription, isLoading, error, refetch } = useSubscription(productSlug);
|
|
2510
2958
|
const { openPortal } = usePortal();
|
|
2959
|
+
const { cancelSubscription, isCanceling } = useCancelSubscription();
|
|
2511
2960
|
const l10n = useLocalization();
|
|
2512
2961
|
const showError = useErrorResolver();
|
|
2513
2962
|
const heading = title === null ? null : title ?? l10n.billing.plan;
|
|
@@ -2519,8 +2968,10 @@ function SubscriptionCard({
|
|
|
2519
2968
|
UNPAID: l10n.billing.statusUnpaid,
|
|
2520
2969
|
PAUSED: l10n.billing.statusPaused
|
|
2521
2970
|
};
|
|
2522
|
-
const [opening, setOpening] =
|
|
2523
|
-
const [portalError, setPortalError] =
|
|
2971
|
+
const [opening, setOpening] = useState16(false);
|
|
2972
|
+
const [portalError, setPortalError] = useState16(null);
|
|
2973
|
+
const [confirmingCancel, setConfirmingCancel] = useState16(false);
|
|
2974
|
+
const [cancelError, setCancelError] = useState16(null);
|
|
2524
2975
|
const handlePortal = async () => {
|
|
2525
2976
|
setPortalError(null);
|
|
2526
2977
|
setOpening(true);
|
|
@@ -2531,6 +2982,17 @@ function SubscriptionCard({
|
|
|
2531
2982
|
setOpening(false);
|
|
2532
2983
|
}
|
|
2533
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
|
+
};
|
|
2534
2996
|
const shell = (children) => /* @__PURE__ */ jsx16(Shell, { appearance, className, children: /* @__PURE__ */ jsxs15("div", { className: "kerne-stack", children: [
|
|
2535
2997
|
heading ? /* @__PURE__ */ jsx16("h2", { className: "kerne-section-title", children: heading }) : null,
|
|
2536
2998
|
children
|
|
@@ -2548,7 +3010,7 @@ function SubscriptionCard({
|
|
|
2548
3010
|
}
|
|
2549
3011
|
if (!subscription) {
|
|
2550
3012
|
return shell(
|
|
2551
|
-
/* @__PURE__ */ jsxs15(
|
|
3013
|
+
/* @__PURE__ */ jsxs15(Fragment9, { children: [
|
|
2552
3014
|
/* @__PURE__ */ jsx16("p", { className: "kerne-subsection-desc", children: l10n.billing.freePlan }),
|
|
2553
3015
|
upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx16(
|
|
2554
3016
|
Button,
|
|
@@ -2567,7 +3029,7 @@ function SubscriptionCard({
|
|
|
2567
3029
|
const renews = formatDate(current_period_end);
|
|
2568
3030
|
const trialEnds = formatDate(trial_end);
|
|
2569
3031
|
return shell(
|
|
2570
|
-
/* @__PURE__ */ jsxs15(
|
|
3032
|
+
/* @__PURE__ */ jsxs15(Fragment9, { children: [
|
|
2571
3033
|
portalError ? /* @__PURE__ */ jsx16(Panel, { variant: "danger", children: portalError }) : null,
|
|
2572
3034
|
/* @__PURE__ */ jsxs15("dl", { className: "kerne-rows", children: [
|
|
2573
3035
|
/* @__PURE__ */ jsxs15("div", { className: "kerne-row", children: [
|
|
@@ -2596,13 +3058,33 @@ function SubscriptionCard({
|
|
|
2596
3058
|
] }) : null
|
|
2597
3059
|
] }),
|
|
2598
3060
|
canceled_at ? /* @__PURE__ */ jsx16(Panel, { children: l10n.billing.cancelNotice }) : null,
|
|
2599
|
-
|
|
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
|
+
] })
|
|
2600
3082
|
] })
|
|
2601
3083
|
);
|
|
2602
3084
|
}
|
|
2603
3085
|
|
|
2604
3086
|
// src/ui/profile/BillingSection.tsx
|
|
2605
|
-
import { Fragment as
|
|
3087
|
+
import { Fragment as Fragment10, jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
2606
3088
|
function intervalsOf(plans) {
|
|
2607
3089
|
const seen = [];
|
|
2608
3090
|
for (const plan of plans) {
|
|
@@ -2628,10 +3110,10 @@ function BillingSection({
|
|
|
2628
3110
|
const { openCheckout } = useCheckout();
|
|
2629
3111
|
const l10n = useLocalization();
|
|
2630
3112
|
const showError = useErrorResolver();
|
|
2631
|
-
const [busy, setBusy] =
|
|
2632
|
-
const [error, setError] =
|
|
3113
|
+
const [busy, setBusy] = useState17(null);
|
|
3114
|
+
const [error, setError] = useState17(null);
|
|
2633
3115
|
const intervals = useMemo(() => intervalsOf(plans), [plans]);
|
|
2634
|
-
const [interval, setInterval] =
|
|
3116
|
+
const [interval, setInterval] = useState17(null);
|
|
2635
3117
|
const activeInterval = interval ?? subscription?.plan_price.interval ?? intervals[0] ?? null;
|
|
2636
3118
|
const handleSwitch = async (planPriceId) => {
|
|
2637
3119
|
setError(null);
|
|
@@ -2646,7 +3128,7 @@ function BillingSection({
|
|
|
2646
3128
|
setBusy(null);
|
|
2647
3129
|
}
|
|
2648
3130
|
};
|
|
2649
|
-
return /* @__PURE__ */ jsxs16(
|
|
3131
|
+
return /* @__PURE__ */ jsxs16(Fragment10, { children: [
|
|
2650
3132
|
error ? /* @__PURE__ */ jsx17(Panel, { variant: "danger", children: error }) : null,
|
|
2651
3133
|
/* @__PURE__ */ jsx17(Subsection, { title: l10n.billing.currentPlan, children: /* @__PURE__ */ jsx17(
|
|
2652
3134
|
SubscriptionCard,
|
|
@@ -2664,7 +3146,7 @@ function BillingSection({
|
|
|
2664
3146
|
style: { height: 64 },
|
|
2665
3147
|
"aria-label": l10n.billing.loadingPlans
|
|
2666
3148
|
}
|
|
2667
|
-
) : 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: [
|
|
2668
3150
|
intervals.length > 1 ? /* @__PURE__ */ jsx17(
|
|
2669
3151
|
"div",
|
|
2670
3152
|
{
|
|
@@ -2706,6 +3188,7 @@ function BillingSection({
|
|
|
2706
3188
|
variant: "outline",
|
|
2707
3189
|
type: "button",
|
|
2708
3190
|
onClick: () => handleSwitch(price.id),
|
|
3191
|
+
loading: busy === price.id,
|
|
2709
3192
|
disabled: busy !== null,
|
|
2710
3193
|
children: busy === price.id ? l10n.billing.opening : l10n.billing.switchTo
|
|
2711
3194
|
}
|
|
@@ -2720,7 +3203,7 @@ function BillingSection({
|
|
|
2720
3203
|
}
|
|
2721
3204
|
|
|
2722
3205
|
// src/ui/UserProfile.tsx
|
|
2723
|
-
import { Fragment as
|
|
3206
|
+
import { Fragment as Fragment11, jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
2724
3207
|
function UserProfile({
|
|
2725
3208
|
tabs = ["profile", "security", "billing"],
|
|
2726
3209
|
extraTabs = [],
|
|
@@ -2770,7 +3253,7 @@ function UserProfile({
|
|
|
2770
3253
|
description: t.description
|
|
2771
3254
|
}))
|
|
2772
3255
|
];
|
|
2773
|
-
const [internalTab, setInternalTab] =
|
|
3256
|
+
const [internalTab, setInternalTab] = useState18(defaultTab ?? allTabs[0]?.id ?? "profile");
|
|
2774
3257
|
const current = controlledTab ?? internalTab;
|
|
2775
3258
|
const select = (id) => {
|
|
2776
3259
|
if (controlledTab === void 0) setInternalTab(id);
|
|
@@ -2815,7 +3298,7 @@ function UserProfile({
|
|
|
2815
3298
|
tab.id
|
|
2816
3299
|
)) }),
|
|
2817
3300
|
/* @__PURE__ */ jsxs17("div", { className: "kerne-pane", children: [
|
|
2818
|
-
active ? /* @__PURE__ */ jsxs17(
|
|
3301
|
+
active ? /* @__PURE__ */ jsxs17(Fragment11, { children: [
|
|
2819
3302
|
/* @__PURE__ */ jsx18("h2", { className: "kerne-pane-title", children: active.title }),
|
|
2820
3303
|
active.description ? /* @__PURE__ */ jsx18("p", { className: "kerne-pane-desc", children: active.description }) : null
|
|
2821
3304
|
] }) : null,
|
|
@@ -2824,9 +3307,317 @@ function UserProfile({
|
|
|
2824
3307
|
] }) });
|
|
2825
3308
|
}
|
|
2826
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
|
+
|
|
2827
3618
|
// src/ui/CheckoutResult.tsx
|
|
2828
|
-
import { useEffect as
|
|
2829
|
-
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";
|
|
2830
3621
|
var POLL_INTERVAL_MS = 1500;
|
|
2831
3622
|
function CheckoutResult({
|
|
2832
3623
|
outcome = "success",
|
|
@@ -2844,9 +3635,9 @@ function CheckoutResult({
|
|
|
2844
3635
|
const client = useClient();
|
|
2845
3636
|
const { config } = useAuthConfig();
|
|
2846
3637
|
const l10n = useLocalization();
|
|
2847
|
-
const [state, setState] =
|
|
2848
|
-
const confirmed =
|
|
2849
|
-
|
|
3638
|
+
const [state, setState] = useState20("checking");
|
|
3639
|
+
const confirmed = useRef5(false);
|
|
3640
|
+
useEffect8(() => {
|
|
2850
3641
|
if (outcome !== "success") return;
|
|
2851
3642
|
let stopped = false;
|
|
2852
3643
|
const startedAt = Date.now();
|
|
@@ -2877,20 +3668,20 @@ function CheckoutResult({
|
|
|
2877
3668
|
const go = (url, handler) => handler ?? (() => {
|
|
2878
3669
|
if (url) window.location.href = url;
|
|
2879
3670
|
});
|
|
2880
|
-
const continueAction = continueUrl || onContinue ? /* @__PURE__ */
|
|
2881
|
-
const shell = (children) => /* @__PURE__ */
|
|
2882
|
-
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,
|
|
2883
3674
|
children,
|
|
2884
|
-
/* @__PURE__ */
|
|
3675
|
+
/* @__PURE__ */ jsx20(KerneBranding, { show: config?.show_kerne_branding ?? false })
|
|
2885
3676
|
] });
|
|
2886
3677
|
if (outcome === "cancel") {
|
|
2887
3678
|
return shell(
|
|
2888
|
-
/* @__PURE__ */
|
|
3679
|
+
/* @__PURE__ */ jsx20(
|
|
2889
3680
|
Status,
|
|
2890
3681
|
{
|
|
2891
3682
|
tone: "danger",
|
|
2892
3683
|
title: l10n.checkout.cancelTitle,
|
|
2893
|
-
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,
|
|
2894
3685
|
children: l10n.checkout.cancelBody
|
|
2895
3686
|
}
|
|
2896
3687
|
)
|
|
@@ -2898,21 +3689,21 @@ function CheckoutResult({
|
|
|
2898
3689
|
}
|
|
2899
3690
|
if (state === "checking") {
|
|
2900
3691
|
return shell(
|
|
2901
|
-
/* @__PURE__ */
|
|
3692
|
+
/* @__PURE__ */ jsx20(Status, { tone: "loading", title: l10n.checkout.confirmingTitle, children: l10n.checkout.confirmingBody })
|
|
2902
3693
|
);
|
|
2903
3694
|
}
|
|
2904
3695
|
if (state === "pending") {
|
|
2905
3696
|
return shell(
|
|
2906
|
-
/* @__PURE__ */
|
|
3697
|
+
/* @__PURE__ */ jsx20(Status, { tone: "success", title: l10n.checkout.pendingTitle, action: continueAction, children: l10n.checkout.pendingBody })
|
|
2907
3698
|
);
|
|
2908
3699
|
}
|
|
2909
3700
|
return shell(
|
|
2910
|
-
/* @__PURE__ */
|
|
3701
|
+
/* @__PURE__ */ jsx20(Status, { tone: "success", title: l10n.checkout.doneTitle, action: continueAction, children: l10n.checkout.doneBody })
|
|
2911
3702
|
);
|
|
2912
3703
|
}
|
|
2913
3704
|
|
|
2914
3705
|
// src/ui/UpgradePrompt.tsx
|
|
2915
|
-
import { Fragment as
|
|
3706
|
+
import { Fragment as Fragment13, jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
2916
3707
|
function UpgradePrompt({
|
|
2917
3708
|
featureKey,
|
|
2918
3709
|
requested,
|
|
@@ -2931,22 +3722,25 @@ function UpgradePrompt({
|
|
|
2931
3722
|
const l10n = useLocalization();
|
|
2932
3723
|
if (isLoading) {
|
|
2933
3724
|
if (hideWhileLoading) return null;
|
|
2934
|
-
return /* @__PURE__ */
|
|
3725
|
+
return /* @__PURE__ */ jsx21(Shell, { appearance, className, children: /* @__PURE__ */ jsx21("div", { className: "kerne-skeleton", style: { height: 80 }, "aria-label": l10n.upgrade.checking }) });
|
|
2935
3726
|
}
|
|
2936
|
-
if (allowed) return /* @__PURE__ */
|
|
3727
|
+
if (allowed) return /* @__PURE__ */ jsx21(Fragment13, { children });
|
|
2937
3728
|
const name = featureName ?? humanizeFeatureKey(featureKey).toLowerCase();
|
|
2938
3729
|
const quotaExhausted = details?.feature_type === "QUOTA" && typeof details.limit === "number" && typeof details.used === "number";
|
|
2939
3730
|
const heading = title ?? fill(quotaExhausted ? l10n.upgrade.quotaTitle : l10n.upgrade.featureTitle, { feature: name });
|
|
2940
3731
|
const body = description ?? (quotaExhausted ? fill(l10n.upgrade.quotaBody, {
|
|
2941
3732
|
used: formatNumber(details.used),
|
|
2942
|
-
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)
|
|
2943
3737
|
}) : fill(l10n.upgrade.featureBody, { feature: name }));
|
|
2944
|
-
return /* @__PURE__ */
|
|
2945
|
-
/* @__PURE__ */
|
|
2946
|
-
/* @__PURE__ */
|
|
2947
|
-
/* @__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 })
|
|
2948
3742
|
] }),
|
|
2949
|
-
upgradeUrl || onUpgradeClick ? /* @__PURE__ */
|
|
3743
|
+
upgradeUrl || onUpgradeClick ? /* @__PURE__ */ jsx21(
|
|
2950
3744
|
Button,
|
|
2951
3745
|
{
|
|
2952
3746
|
type: "button",
|
|
@@ -2967,6 +3761,7 @@ export {
|
|
|
2967
3761
|
ForgotPasswordForm,
|
|
2968
3762
|
LoginForm,
|
|
2969
3763
|
MagicLinkCallback,
|
|
3764
|
+
PricingTable,
|
|
2970
3765
|
ProfileSection,
|
|
2971
3766
|
RegisterForm,
|
|
2972
3767
|
ResetPasswordForm,
|
|
@@ -2980,5 +3775,6 @@ export {
|
|
|
2980
3775
|
VerifyEmailForm,
|
|
2981
3776
|
WaitlistForm,
|
|
2982
3777
|
defaultLocalization,
|
|
2983
|
-
useLocalization
|
|
3778
|
+
useLocalization,
|
|
3779
|
+
usePricingTable
|
|
2984
3780
|
};
|