@wealthx/shadcn 1.5.29 → 1.5.30
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/.turbo/turbo-build.log +109 -109
- package/CHANGELOG.md +6 -0
- package/dist/{chunk-DWJHPNFL.mjs → chunk-4UT3RZ2D.mjs} +32 -16
- package/dist/{chunk-RNLIZRAK.mjs → chunk-C6R42PCL.mjs} +1 -1
- package/dist/{chunk-EVUY67CY.mjs → chunk-FTQ2AKZ2.mjs} +1 -1
- package/dist/{chunk-7T4TYUO3.mjs → chunk-H5ZD63NT.mjs} +31 -16
- package/dist/{chunk-SO4RB3XB.mjs → chunk-IEQX4UVP.mjs} +2 -2
- package/dist/chunk-IKVF4XE2.mjs +94 -0
- package/dist/{chunk-KPGARKFC.mjs → chunk-MS3GNXMB.mjs} +1 -1
- package/dist/{chunk-M32QNCD3.mjs → chunk-OSSS56CB.mjs} +1 -1
- package/dist/{chunk-36IN7YRM.mjs → chunk-SCGCGVDN.mjs} +1 -1
- package/dist/{chunk-KJQ3BVTB.mjs → chunk-X2NIDXFB.mjs} +1 -1
- package/dist/components/ui/backoffice-signup-steps.js +98 -48
- package/dist/components/ui/backoffice-signup-steps.mjs +4 -4
- package/dist/components/ui/bank-statement-generate-dialog.mjs +2 -2
- package/dist/components/ui/chat-widget.js +1 -1
- package/dist/components/ui/chat-widget.mjs +2 -2
- package/dist/components/ui/contact-alert-dialog/index.mjs +2 -2
- package/dist/components/ui/field.js +1 -1
- package/dist/components/ui/field.mjs +1 -1
- package/dist/components/ui/frontend-signup-steps.js +166 -90
- package/dist/components/ui/frontend-signup-steps.mjs +4 -5
- package/dist/components/ui/password-strength-tooltip.js +81 -107
- package/dist/components/ui/password-strength-tooltip.mjs +3 -2
- package/dist/components/ui/property-report-dialog.mjs +2 -2
- package/dist/components/ui/signup-form-primitives.js +117 -114
- package/dist/components/ui/signup-form-primitives.mjs +3 -4
- package/dist/components/ui/two-fa-setup-form.js +31 -16
- package/dist/components/ui/two-fa-setup-form.mjs +2 -2
- package/dist/index.js +133 -68
- package/dist/index.mjs +10 -10
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/ui/field.tsx +12 -12
- package/src/components/ui/password-strength-tooltip.tsx +89 -47
- package/src/components/ui/signup-form-primitives.tsx +34 -16
- package/src/components/ui/two-fa-setup-form.tsx +41 -31
- package/src/styles/styles-css.ts +1 -1
- package/dist/chunk-WHIW6KOB.mjs +0 -57
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@ function FieldSet({ className, ...props }: FieldSetProps): ReactElement {
|
|
|
22
22
|
className={cn(
|
|
23
23
|
"flex flex-col gap-6",
|
|
24
24
|
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
|
25
|
-
className
|
|
25
|
+
className
|
|
26
26
|
)}
|
|
27
27
|
data-slot="field-set"
|
|
28
28
|
{...props}
|
|
@@ -45,7 +45,7 @@ function FieldLegend({
|
|
|
45
45
|
"mb-3",
|
|
46
46
|
"data-[variant=legend]:text-label-large",
|
|
47
47
|
"data-[variant=label]:text-label-medium",
|
|
48
|
-
className
|
|
48
|
+
className
|
|
49
49
|
)}
|
|
50
50
|
data-slot="field-legend"
|
|
51
51
|
data-variant={variant}
|
|
@@ -61,7 +61,7 @@ function FieldGroup({ className, ...props }: FieldGroupProps): ReactElement {
|
|
|
61
61
|
<div
|
|
62
62
|
className={cn(
|
|
63
63
|
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
|
64
|
-
className
|
|
64
|
+
className
|
|
65
65
|
)}
|
|
66
66
|
data-slot="field-group"
|
|
67
67
|
{...props}
|
|
@@ -90,7 +90,7 @@ const fieldVariants = cva(
|
|
|
90
90
|
defaultVariants: {
|
|
91
91
|
orientation: "vertical",
|
|
92
92
|
},
|
|
93
|
-
}
|
|
93
|
+
}
|
|
94
94
|
);
|
|
95
95
|
|
|
96
96
|
export type FieldProps = React.ComponentProps<"div"> &
|
|
@@ -122,7 +122,7 @@ function FieldContent({
|
|
|
122
122
|
<div
|
|
123
123
|
className={cn(
|
|
124
124
|
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
|
125
|
-
className
|
|
125
|
+
className
|
|
126
126
|
)}
|
|
127
127
|
data-slot="field-content"
|
|
128
128
|
{...props}
|
|
@@ -139,7 +139,7 @@ function FieldLabel({ className, ...props }: FieldLabelProps): ReactElement {
|
|
|
139
139
|
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
|
140
140
|
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
|
141
141
|
"has-data-checked:border-primary has-data-checked:bg-primary/5 dark:has-data-checked:bg-primary/10",
|
|
142
|
-
className
|
|
142
|
+
className
|
|
143
143
|
)}
|
|
144
144
|
data-slot="field-label"
|
|
145
145
|
{...props}
|
|
@@ -154,7 +154,7 @@ function FieldTitle({ className, ...props }: FieldTitleProps): ReactElement {
|
|
|
154
154
|
<div
|
|
155
155
|
className={cn(
|
|
156
156
|
"flex w-fit items-center gap-2 text-label-medium leading-snug group-data-[disabled=true]/field:opacity-50",
|
|
157
|
-
className
|
|
157
|
+
className
|
|
158
158
|
)}
|
|
159
159
|
data-slot="field-label"
|
|
160
160
|
{...props}
|
|
@@ -174,7 +174,7 @@ function FieldDescription({
|
|
|
174
174
|
"text-caption leading-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
|
|
175
175
|
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
|
176
176
|
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
177
|
-
className
|
|
177
|
+
className
|
|
178
178
|
)}
|
|
179
179
|
data-slot="field-description"
|
|
180
180
|
{...props}
|
|
@@ -195,7 +195,7 @@ function FieldSeparator({
|
|
|
195
195
|
<div
|
|
196
196
|
className={cn(
|
|
197
197
|
"relative -my-2 h-5 text-body-small group-data-[variant=outline]/field-group:-mb-2",
|
|
198
|
-
className
|
|
198
|
+
className
|
|
199
199
|
)}
|
|
200
200
|
data-content={Boolean(children)}
|
|
201
201
|
data-slot="field-separator"
|
|
@@ -234,7 +234,7 @@ function FieldError({
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
const uniqueErrors = Array.from(
|
|
237
|
-
new Map(errors.map((error) => [error?.message, error])).values()
|
|
237
|
+
new Map(errors.map((error) => [error?.message, error])).values()
|
|
238
238
|
);
|
|
239
239
|
|
|
240
240
|
if (uniqueErrors.length === 1) {
|
|
@@ -245,7 +245,7 @@ function FieldError({
|
|
|
245
245
|
<ul className="ml-4 flex list-disc flex-col gap-1">
|
|
246
246
|
{uniqueErrors.map(
|
|
247
247
|
(error) =>
|
|
248
|
-
error?.message && <li key={error.message}>{error.message}</li
|
|
248
|
+
error?.message && <li key={error.message}>{error.message}</li>
|
|
249
249
|
)}
|
|
250
250
|
</ul>
|
|
251
251
|
);
|
|
@@ -257,7 +257,7 @@ function FieldError({
|
|
|
257
257
|
|
|
258
258
|
return (
|
|
259
259
|
<div
|
|
260
|
-
className={cn("text-caption text-destructive", className)}
|
|
260
|
+
className={cn("text-left text-caption text-destructive", className)}
|
|
261
261
|
data-slot="field-error"
|
|
262
262
|
role="alert"
|
|
263
263
|
{...props}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { Check, X } from "lucide-react";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
PopoverTrigger,
|
|
7
|
-
} from "@/components/ui/popover";
|
|
3
|
+
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
import { useThemeVars } from "@/lib/theme-provider";
|
|
8
6
|
|
|
9
|
-
type
|
|
7
|
+
export type PasswordStrengthRule = {
|
|
8
|
+
label: string;
|
|
9
|
+
test: (p: string) => boolean;
|
|
10
|
+
};
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
+
export const PASSWORD_STRENGTH_RULES: PasswordStrengthRule[] = [
|
|
12
13
|
{ label: "Minimum 8 characters", test: (p) => p.length >= 8 },
|
|
13
14
|
{ label: "At least one uppercase letter", test: (p) => /[A-Z]/.test(p) },
|
|
14
15
|
{ label: "At least one lowercase letter", test: (p) => /[a-z]/.test(p) },
|
|
@@ -24,47 +25,88 @@ export type PasswordStrengthTooltipProps = {
|
|
|
24
25
|
password: string;
|
|
25
26
|
children: React.ReactNode;
|
|
26
27
|
side?: "top" | "right" | "bottom" | "left";
|
|
28
|
+
onRequestClose?: () => void;
|
|
27
29
|
};
|
|
28
30
|
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
side = "right",
|
|
34
|
-
|
|
31
|
+
export const PasswordStrengthTooltip = React.forwardRef<
|
|
32
|
+
HTMLDivElement,
|
|
33
|
+
PasswordStrengthTooltipProps
|
|
34
|
+
>(function PasswordStrengthTooltip(
|
|
35
|
+
{ open = false, password, children, side = "right" },
|
|
36
|
+
forwardedRef
|
|
37
|
+
) {
|
|
38
|
+
const themeVars = useThemeVars();
|
|
39
|
+
// anchorRef is used by Positioner for popup placement.
|
|
40
|
+
// forwardedRef (fieldRef from consumer) is used for contains() checks in dismiss logic.
|
|
41
|
+
const anchorRef = React.useRef<HTMLDivElement>(null);
|
|
42
|
+
|
|
43
|
+
const composedRef = React.useCallback(
|
|
44
|
+
(node: HTMLDivElement | null) => {
|
|
45
|
+
(anchorRef as React.MutableRefObject<HTMLDivElement | null>).current =
|
|
46
|
+
node;
|
|
47
|
+
if (typeof forwardedRef === "function") {
|
|
48
|
+
forwardedRef(node);
|
|
49
|
+
} else if (forwardedRef) {
|
|
50
|
+
(
|
|
51
|
+
forwardedRef as React.MutableRefObject<HTMLDivElement | null>
|
|
52
|
+
).current = node;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
56
|
+
[forwardedRef]
|
|
57
|
+
);
|
|
58
|
+
|
|
35
59
|
return (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
// PopoverPrimitive.Root with no Trigger — popup is fully controlled via `open` prop.
|
|
61
|
+
// The wrapper div has no trigger behaviors injected, so clicking the input inside
|
|
62
|
+
// works on the first click without interference.
|
|
63
|
+
<PopoverPrimitive.Root open={open}>
|
|
64
|
+
<div ref={composedRef}>{children}</div>
|
|
65
|
+
<PopoverPrimitive.Portal>
|
|
66
|
+
<PopoverPrimitive.Positioner
|
|
67
|
+
anchor={anchorRef}
|
|
68
|
+
side={side}
|
|
69
|
+
align="start"
|
|
70
|
+
sideOffset={8}
|
|
71
|
+
className="z-[200]"
|
|
72
|
+
>
|
|
73
|
+
<PopoverPrimitive.Popup
|
|
74
|
+
initialFocus={false}
|
|
75
|
+
finalFocus={false}
|
|
76
|
+
className={cn(
|
|
77
|
+
"border border-border bg-popover shadow-md outline-hidden",
|
|
78
|
+
"w-auto max-w-[280px] p-3 font-sans",
|
|
79
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
|
|
80
|
+
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
81
|
+
"data-ending-style:animate-out data-ending-style:fade-out-0 data-ending-style:zoom-out-95 data-ending-style:fill-mode-forwards",
|
|
82
|
+
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95"
|
|
83
|
+
)}
|
|
84
|
+
style={themeVars as React.CSSProperties}
|
|
85
|
+
>
|
|
86
|
+
<div className="flex flex-col gap-1.5">
|
|
87
|
+
{PASSWORD_STRENGTH_RULES.map((rule) => {
|
|
88
|
+
const valid = password ? rule.test(password) : false;
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
key={rule.label}
|
|
92
|
+
className={cn(
|
|
93
|
+
"flex items-center gap-1.5 text-[13px] leading-[18px]",
|
|
94
|
+
valid ? "text-success" : "text-destructive"
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
{valid ? (
|
|
98
|
+
<Check size={14} className="shrink-0" />
|
|
99
|
+
) : (
|
|
100
|
+
<X size={14} className="shrink-0" />
|
|
101
|
+
)}
|
|
102
|
+
<span>{rule.label}</span>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
})}
|
|
106
|
+
</div>
|
|
107
|
+
</PopoverPrimitive.Popup>
|
|
108
|
+
</PopoverPrimitive.Positioner>
|
|
109
|
+
</PopoverPrimitive.Portal>
|
|
110
|
+
</PopoverPrimitive.Root>
|
|
69
111
|
);
|
|
70
|
-
}
|
|
112
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { EyeIcon, EyeOffIcon, Trash2Icon } from "lucide-react";
|
|
3
3
|
import { Button } from "./button";
|
|
4
4
|
import { Field, FieldError, FieldLabel } from "./field";
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
SelectTrigger,
|
|
11
11
|
SelectValue,
|
|
12
12
|
} from "./select";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
PASSWORD_STRENGTH_RULES,
|
|
15
|
+
PasswordStrengthTooltip,
|
|
16
|
+
} from "./password-strength-tooltip";
|
|
14
17
|
|
|
15
18
|
// ---------------------------------------------------------------------------
|
|
16
19
|
// SectionHeading
|
|
@@ -52,14 +55,6 @@ export function FormField({ label, required, children }: FormFieldProps) {
|
|
|
52
55
|
// PasswordField — input with show/hide toggle + optional strength popover
|
|
53
56
|
// ---------------------------------------------------------------------------
|
|
54
57
|
|
|
55
|
-
const STRENGTH_RULES = [
|
|
56
|
-
{ test: (p: string) => p.length >= 8 },
|
|
57
|
-
{ test: (p: string) => /[A-Z]/.test(p) },
|
|
58
|
-
{ test: (p: string) => /[a-z]/.test(p) },
|
|
59
|
-
{ test: (p: string) => /\d/.test(p) },
|
|
60
|
-
{ test: (p: string) => /[^A-Za-z0-9]/.test(p) },
|
|
61
|
-
];
|
|
62
|
-
|
|
63
58
|
export interface PasswordFieldProps {
|
|
64
59
|
label: string;
|
|
65
60
|
placeholder: string;
|
|
@@ -79,16 +74,30 @@ export function PasswordField({
|
|
|
79
74
|
const [error, setError] = useState("");
|
|
80
75
|
const [touched, setTouched] = useState(false);
|
|
81
76
|
const [value, setValue] = useState("");
|
|
82
|
-
const [
|
|
77
|
+
const [dismissed, setDismissed] = useState(false);
|
|
78
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
83
79
|
|
|
84
|
-
const allRulesPass =
|
|
85
|
-
const isPopoverOpen =
|
|
80
|
+
const allRulesPass = PASSWORD_STRENGTH_RULES.every((r) => r.test(value));
|
|
81
|
+
const isPopoverOpen =
|
|
82
|
+
!!showStrengthPopover && !dismissed && value.length > 0 && !allRulesPass;
|
|
86
83
|
|
|
87
84
|
const validate = (v: string) => {
|
|
88
|
-
if (v
|
|
85
|
+
if (!v) return "";
|
|
86
|
+
if (v.length < 8) return "Min. 8 characters required";
|
|
87
|
+
if (!PASSWORD_STRENGTH_RULES.every((r) => r.test(v)))
|
|
88
|
+
return "Password doesn't meet all requirements";
|
|
89
89
|
return "";
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!isPopoverOpen) return;
|
|
94
|
+
const onMouseDown = (e: MouseEvent) => {
|
|
95
|
+
if (!wrapperRef.current?.contains(e.target as Node)) setDismissed(true);
|
|
96
|
+
};
|
|
97
|
+
document.addEventListener("mousedown", onMouseDown, true);
|
|
98
|
+
return () => document.removeEventListener("mousedown", onMouseDown, true);
|
|
99
|
+
}, [isPopoverOpen]);
|
|
100
|
+
|
|
92
101
|
const inputEl = (
|
|
93
102
|
<div className="relative">
|
|
94
103
|
<Input
|
|
@@ -100,8 +109,16 @@ export function PasswordField({
|
|
|
100
109
|
const v = e.target.value;
|
|
101
110
|
setValue(v);
|
|
102
111
|
onValueChange?.(v);
|
|
103
|
-
if (!typingStarted && v.length > 0) setTypingStarted(true);
|
|
104
112
|
if (touched) setError(validate(v));
|
|
113
|
+
setDismissed(false);
|
|
114
|
+
}}
|
|
115
|
+
onFocus={() => setDismissed(false)}
|
|
116
|
+
onKeyDown={(e: React.KeyboardEvent) => {
|
|
117
|
+
if (e.key === "Tab") {
|
|
118
|
+
setTouched(true);
|
|
119
|
+
setError(validate(value));
|
|
120
|
+
setDismissed(true);
|
|
121
|
+
}
|
|
105
122
|
}}
|
|
106
123
|
onBlur={() => {
|
|
107
124
|
setTouched(true);
|
|
@@ -131,6 +148,7 @@ export function PasswordField({
|
|
|
131
148
|
</FieldLabel>
|
|
132
149
|
{showStrengthPopover ? (
|
|
133
150
|
<PasswordStrengthTooltip
|
|
151
|
+
ref={wrapperRef}
|
|
134
152
|
open={isPopoverOpen}
|
|
135
153
|
password={value}
|
|
136
154
|
side="right"
|
|
@@ -141,7 +159,7 @@ export function PasswordField({
|
|
|
141
159
|
inputEl
|
|
142
160
|
)}
|
|
143
161
|
<div className="min-h-5">
|
|
144
|
-
{touched && error && <FieldError>{error}</FieldError>}
|
|
162
|
+
{touched && error && !isPopoverOpen && <FieldError>{error}</FieldError>}
|
|
145
163
|
</div>
|
|
146
164
|
</Field>
|
|
147
165
|
);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
1
|
import { useState } from "react";
|
|
3
2
|
import { Smartphone } from "lucide-react";
|
|
4
3
|
import { Button } from "@/components/ui/button";
|
|
@@ -6,19 +5,20 @@ import { Field, FieldError } from "@/components/ui/field";
|
|
|
6
5
|
import { InputGroup, InputGroupInput } from "@/components/ui/input-group";
|
|
7
6
|
import { Label } from "@/components/ui/label";
|
|
8
7
|
import { cn } from "@/lib/utils";
|
|
8
|
+
import type { ReactNode } from "react";
|
|
9
9
|
|
|
10
10
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
12
|
export type TwoFAApp = {
|
|
13
13
|
name: string;
|
|
14
|
-
icon?:
|
|
15
|
-
qrNode?:
|
|
14
|
+
icon?: ReactNode;
|
|
15
|
+
qrNode?: ReactNode;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
export type TwoFASetupFormProps = {
|
|
19
19
|
title?: string;
|
|
20
20
|
apps?: TwoFAApp[];
|
|
21
|
-
qrCodeNode?:
|
|
21
|
+
qrCodeNode?: ReactNode;
|
|
22
22
|
onVerify: (token: string) => Promise<void>;
|
|
23
23
|
onSetupLater?: () => void;
|
|
24
24
|
required?: boolean;
|
|
@@ -30,6 +30,17 @@ const DEFAULT_APPS: TwoFAApp[] = [
|
|
|
30
30
|
{ name: "Microsoft Authenticator" },
|
|
31
31
|
];
|
|
32
32
|
|
|
33
|
+
const STEP_META = {
|
|
34
|
+
1: {
|
|
35
|
+
subtitle: "We recommend Google Authenticator or Microsoft Authenticator",
|
|
36
|
+
defaultTitle: "2FA Setup",
|
|
37
|
+
},
|
|
38
|
+
2: {
|
|
39
|
+
title: "Open Auth App and Scan Code",
|
|
40
|
+
subtitle: "Open your authenticator app and scan the QR code",
|
|
41
|
+
},
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
33
44
|
// ─── AppDownloadStep ─────────────────────────────────────────────────────────
|
|
34
45
|
|
|
35
46
|
function AppDownloadStep({
|
|
@@ -45,27 +56,28 @@ function AppDownloadStep({
|
|
|
45
56
|
}) {
|
|
46
57
|
return (
|
|
47
58
|
<div className="flex flex-col gap-6">
|
|
48
|
-
<div className="flex gap-
|
|
59
|
+
<div className="flex flex-col gap-3">
|
|
49
60
|
{apps.map((app) => (
|
|
50
61
|
<div
|
|
51
62
|
key={app.name}
|
|
52
|
-
className="flex
|
|
63
|
+
className="flex items-center gap-4 border border-border bg-muted/30 p-4"
|
|
53
64
|
>
|
|
54
|
-
<div className="flex items-center
|
|
55
|
-
{app.
|
|
56
|
-
<
|
|
65
|
+
<div className="flex shrink-0 items-center justify-center">
|
|
66
|
+
{app.qrNode ?? (
|
|
67
|
+
<div className="flex h-[80px] w-[80px] items-center justify-center border border-dashed border-border bg-muted/50 text-[11px] text-muted-foreground">
|
|
68
|
+
Scan
|
|
69
|
+
</div>
|
|
57
70
|
)}
|
|
58
|
-
{app.name}
|
|
59
71
|
</div>
|
|
60
|
-
|
|
61
|
-
<div className="flex items-center
|
|
62
|
-
{app.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
Scan to download
|
|
72
|
+
<div className="flex flex-col gap-1">
|
|
73
|
+
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
|
74
|
+
{app.icon ?? (
|
|
75
|
+
<Smartphone size={16} className="text-muted-foreground" />
|
|
76
|
+
)}
|
|
77
|
+
{app.name}
|
|
67
78
|
</div>
|
|
68
|
-
|
|
79
|
+
<p className="text-xs text-muted-foreground">Scan to download</p>
|
|
80
|
+
</div>
|
|
69
81
|
</div>
|
|
70
82
|
))}
|
|
71
83
|
</div>
|
|
@@ -97,7 +109,7 @@ function VerificationStep({
|
|
|
97
109
|
onSetupLater,
|
|
98
110
|
required,
|
|
99
111
|
}: {
|
|
100
|
-
qrCodeNode?:
|
|
112
|
+
qrCodeNode?: ReactNode;
|
|
101
113
|
onVerify: (token: string) => Promise<void>;
|
|
102
114
|
onBack: () => void;
|
|
103
115
|
onSetupLater?: () => void;
|
|
@@ -116,8 +128,10 @@ function VerificationStep({
|
|
|
116
128
|
setIsLoading(true);
|
|
117
129
|
try {
|
|
118
130
|
await onVerify(token);
|
|
119
|
-
} catch {
|
|
120
|
-
setError(
|
|
131
|
+
} catch (err) {
|
|
132
|
+
setError(
|
|
133
|
+
err instanceof Error ? err.message : "Invalid code. Please try again."
|
|
134
|
+
);
|
|
121
135
|
} finally {
|
|
122
136
|
setIsLoading(false);
|
|
123
137
|
}
|
|
@@ -126,9 +140,7 @@ function VerificationStep({
|
|
|
126
140
|
return (
|
|
127
141
|
<div className="flex flex-col gap-6">
|
|
128
142
|
<div className="flex flex-col items-center gap-3">
|
|
129
|
-
{qrCodeNode
|
|
130
|
-
<div className="flex items-center justify-center">{qrCodeNode}</div>
|
|
131
|
-
) : (
|
|
143
|
+
{qrCodeNode ?? (
|
|
132
144
|
<div className="flex h-[140px] w-[140px] items-center justify-center border border-dashed border-border bg-muted/50 text-xs text-muted-foreground">
|
|
133
145
|
QR Code
|
|
134
146
|
</div>
|
|
@@ -185,7 +197,7 @@ function VerificationStep({
|
|
|
185
197
|
// ─── TwoFASetupForm ───────────────────────────────────────────────────────────
|
|
186
198
|
|
|
187
199
|
export function TwoFASetupForm({
|
|
188
|
-
title
|
|
200
|
+
title,
|
|
189
201
|
apps = DEFAULT_APPS,
|
|
190
202
|
qrCodeNode,
|
|
191
203
|
onVerify,
|
|
@@ -195,15 +207,13 @@ export function TwoFASetupForm({
|
|
|
195
207
|
}: TwoFASetupFormProps) {
|
|
196
208
|
const [step, setStep] = useState<1 | 2>(1);
|
|
197
209
|
|
|
198
|
-
const stepTitle =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
? "We recommend Google Authenticator or Microsoft Authenticator"
|
|
202
|
-
: "Open your authenticator app and scan the QR code";
|
|
210
|
+
const stepTitle =
|
|
211
|
+
step === 1 ? title ?? STEP_META[1].defaultTitle : STEP_META[2].title;
|
|
212
|
+
const stepSubtitle = STEP_META[step].subtitle;
|
|
203
213
|
|
|
204
214
|
return (
|
|
205
215
|
<div className={cn("flex flex-col gap-6", className)}>
|
|
206
|
-
<div className="flex flex-col gap-1">
|
|
216
|
+
<div className="flex flex-col items-center gap-1 text-center">
|
|
207
217
|
<h2 className="text-xl font-semibold text-foreground">{stepTitle}</h2>
|
|
208
218
|
<p className="text-sm text-muted-foreground">{stepSubtitle}</p>
|
|
209
219
|
</div>
|