fluxy-bot 0.8.9 → 0.9.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 +53 -84
- package/bin/cli.js +1717 -6
- package/components.json +18 -18
- package/package.json +98 -109
- package/postcss.config.js +3 -3
- package/scripts/install.ps1 +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/postinstall.js +47 -58
- package/shared/ai.ts +118 -202
- package/shared/config.ts +37 -39
- package/shared/logger.ts +6 -14
- package/shared/paths.ts +10 -16
- package/shared/relay.ts +89 -98
- package/supervisor/backend.ts +77 -89
- package/supervisor/chat/ARCHITECTURE.md +13 -14
- package/supervisor/chat/OnboardWizard.tsx +2194 -3419
- package/supervisor/chat/fluxy-main.tsx +495 -633
- package/supervisor/chat/fluxy.html +24 -36
- package/supervisor/chat/onboard-main.tsx +8 -8
- package/supervisor/chat/onboard.html +10 -13
- package/supervisor/chat/src/components/Chat/AudioBubble.tsx +94 -108
- package/supervisor/chat/src/components/Chat/ChatView.tsx +14 -36
- package/supervisor/chat/src/components/Chat/ImageLightbox.tsx +71 -89
- package/supervisor/chat/src/components/Chat/InputBar.tsx +498 -728
- package/supervisor/chat/src/components/Chat/MessageBubble.tsx +154 -246
- package/supervisor/chat/src/components/Chat/MessageList.tsx +114 -138
- package/supervisor/chat/src/components/Chat/TypingIndicator.tsx +38 -50
- package/supervisor/chat/src/components/LoginScreen.tsx +245 -296
- package/supervisor/chat/src/hooks/useChat.ts +210 -259
- package/supervisor/chat/src/hooks/useFluxyChat.ts +232 -322
- package/supervisor/chat/src/hooks/useSpeechRecognition.ts +126 -148
- package/supervisor/chat/src/lib/auth.ts +20 -23
- package/supervisor/chat/src/lib/ws-client.ts +113 -125
- package/supervisor/chat/src/styles/globals.css +76 -85
- package/supervisor/file-saver.ts +39 -52
- package/supervisor/fluxy-agent.ts +213 -292
- package/supervisor/index.ts +895 -1263
- package/supervisor/scheduler.ts +237 -299
- package/supervisor/tunnel.ts +113 -167
- package/supervisor/vite-dev.ts +51 -61
- package/supervisor/widget.js +135 -147
- package/supervisor/worker.ts +45 -49
- package/tsconfig.json +18 -24
- package/vite.config.ts +48 -55
- package/vite.fluxy.config.ts +34 -34
- package/worker/claude-auth.ts +202 -265
- package/worker/codex-auth.ts +144 -181
- package/worker/db.ts +63 -191
- package/worker/index.ts +601 -842
- package/workspace/MYSELF.md +1 -1
- package/workspace/PULSE.json +6 -6
- package/workspace/backend/index.ts +16 -19
- package/workspace/client/index.html +47 -60
- package/workspace/client/public/manifest.json +21 -21
- package/workspace/client/public/sw.js +29 -31
- package/workspace/client/src/App.tsx +91 -136
- package/workspace/client/src/components/Dashboard/DashboardPage.tsx +68 -72
- package/workspace/client/src/components/ErrorBoundary.tsx +11 -11
- package/workspace/client/src/components/Layout/DashboardLayout.tsx +35 -35
- package/workspace/client/src/components/Layout/Footer.tsx +12 -12
- package/workspace/client/src/components/Layout/MobileNav.tsx +23 -23
- package/workspace/client/src/components/Layout/Sidebar.tsx +81 -81
- package/workspace/client/src/components/ui/avatar.tsx +85 -85
- package/workspace/client/src/components/ui/badge.tsx +40 -41
- package/workspace/client/src/components/ui/button.tsx +56 -57
- package/workspace/client/src/components/ui/card.tsx +75 -75
- package/workspace/client/src/components/ui/dialog.tsx +114 -117
- package/workspace/client/src/components/ui/dropdown-menu.tsx +191 -199
- package/workspace/client/src/components/ui/input.tsx +17 -17
- package/workspace/client/src/components/ui/scroll-area.tsx +47 -47
- package/workspace/client/src/components/ui/select.tsx +145 -151
- package/workspace/client/src/components/ui/separator.tsx +21 -21
- package/workspace/client/src/components/ui/sheet.tsx +102 -102
- package/workspace/client/src/components/ui/skeleton.tsx +10 -10
- package/workspace/client/src/components/ui/switch.tsx +26 -26
- package/workspace/client/src/components/ui/tabs.tsx +69 -69
- package/workspace/client/src/components/ui/textarea.tsx +14 -14
- package/workspace/client/src/components/ui/tooltip.tsx +37 -37
- package/workspace/client/src/lib/utils.ts +1 -1
- package/workspace/client/src/main.tsx +3 -3
- package/workspace/client/src/styles/globals.css +76 -85
- package/workspace/skills/code-reviewer/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/code-reviewer/skills/code-reviewer/SKILL.md +0 -6
- package/workspace/skills/daily-standup/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/daily-standup/skills/daily-standup/SKILL.md +0 -7
- package/workspace/skills/workspace-helper/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/workspace-helper/skills/workspace-helper/SKILL.md +0 -2
- package/bin/cli.backup.js +0 -2138
- package/cli/commands/daemon.ts +0 -42
- package/cli/commands/init.ts +0 -32
- package/cli/commands/start.ts +0 -113
- package/cli/commands/tunnel.temp.ts +0 -206
- package/cli/commands/tunnel.ts +0 -227
- package/cli/commands/update.ts +0 -163
- package/cli/core/base-adapter.ts +0 -156
- package/cli/core/cloudflared.ts +0 -113
- package/cli/core/config.ts +0 -73
- package/cli/core/os-detector.ts +0 -43
- package/cli/core/server.ts +0 -109
- package/cli/core/types.ts +0 -15
- package/cli/index.ts +0 -72
- package/cli/platforms/darwin.ts +0 -146
- package/cli/platforms/index.ts +0 -21
- package/cli/platforms/linux.ts +0 -156
- package/cli/platforms/win32.ts +0 -34
- package/cli/utils/ui.ts +0 -37
|
@@ -1,331 +1,280 @@
|
|
|
1
1
|
import { useState, useRef, useEffect, type KeyboardEvent } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Lock,
|
|
4
|
-
LoaderCircle,
|
|
5
|
-
ArrowRight,
|
|
6
|
-
ArrowLeft,
|
|
7
|
-
Shield,
|
|
8
|
-
Check
|
|
9
|
-
} from 'lucide-react';
|
|
2
|
+
import { Lock, LoaderCircle, ArrowRight, ArrowLeft, Shield, Check } from 'lucide-react';
|
|
10
3
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
11
4
|
|
|
12
5
|
const LOGIN_STORAGE_KEY = 'fluxy_login_totp';
|
|
13
6
|
|
|
14
7
|
interface Props {
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
onLogin: (token: string) => void;
|
|
9
|
+
totpEnabled?: boolean;
|
|
17
10
|
}
|
|
18
11
|
|
|
19
|
-
export default function LoginScreen({ onLogin }: Props) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
export default function LoginScreen({ onLogin, totpEnabled }: Props) {
|
|
13
|
+
const [password, setPassword] = useState('');
|
|
14
|
+
const [error, setError] = useState('');
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
23
16
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
// TOTP state
|
|
18
|
+
const [phase, setPhase] = useState<'password' | 'totp'>('password');
|
|
19
|
+
const [totpCode, setTotpCode] = useState('');
|
|
20
|
+
const [pendingToken, setPendingToken] = useState('');
|
|
21
|
+
const [trustDevice, setTrustDevice] = useState(true);
|
|
22
|
+
const [useRecovery, setUseRecovery] = useState(false);
|
|
23
|
+
const totpInputRef = useRef<HTMLInputElement>(null);
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
25
|
+
// Restore TOTP phase after page reload (mobile: OS suspends PWA on app-switch)
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
try {
|
|
28
|
+
const raw = sessionStorage.getItem(LOGIN_STORAGE_KEY);
|
|
29
|
+
if (!raw) return;
|
|
30
|
+
const saved = JSON.parse(raw);
|
|
31
|
+
if (saved.pendingToken && saved.expiresAt > Date.now()) {
|
|
32
|
+
setPendingToken(saved.pendingToken);
|
|
33
|
+
setPhase('totp');
|
|
34
|
+
} else {
|
|
35
|
+
sessionStorage.removeItem(LOGIN_STORAGE_KEY);
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
}, []);
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
}, [phase]);
|
|
53
|
-
|
|
54
|
-
function saveLoginState(token: string) {
|
|
55
|
-
try {
|
|
56
|
-
// Pending token has 5min server expiry — save with matching client expiry
|
|
57
|
-
sessionStorage.setItem(
|
|
58
|
-
LOGIN_STORAGE_KEY,
|
|
59
|
-
JSON.stringify({
|
|
60
|
-
pendingToken: token,
|
|
61
|
-
expiresAt: Date.now() + 4.5 * 60 * 1000
|
|
62
|
-
})
|
|
63
|
-
);
|
|
64
|
-
} catch {}
|
|
40
|
+
// Auto-focus TOTP input when switching to TOTP phase
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (phase === 'totp') {
|
|
43
|
+
setTimeout(() => totpInputRef.current?.focus(), 100);
|
|
65
44
|
}
|
|
45
|
+
}, [phase]);
|
|
66
46
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
47
|
+
function saveLoginState(token: string) {
|
|
48
|
+
try {
|
|
49
|
+
// Pending token has 5min server expiry — save with matching client expiry
|
|
50
|
+
sessionStorage.setItem(LOGIN_STORAGE_KEY, JSON.stringify({
|
|
51
|
+
pendingToken: token,
|
|
52
|
+
expiresAt: Date.now() + 4.5 * 60 * 1000,
|
|
53
|
+
}));
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
72
56
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
setError('');
|
|
57
|
+
function clearLoginState() {
|
|
58
|
+
try { sessionStorage.removeItem(LOGIN_STORAGE_KEY); } catch {}
|
|
59
|
+
}
|
|
77
60
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
credentials: 'include'
|
|
83
|
-
});
|
|
84
|
-
const data = await res.json();
|
|
61
|
+
const handleSubmit = async () => {
|
|
62
|
+
if (!password.trim() || loading) return;
|
|
63
|
+
setLoading(true);
|
|
64
|
+
setError('');
|
|
85
65
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
setError('');
|
|
94
|
-
} else {
|
|
95
|
-
setError(data.error || 'Invalid password');
|
|
96
|
-
}
|
|
97
|
-
} catch {
|
|
98
|
-
setError('Could not reach server');
|
|
99
|
-
} finally {
|
|
100
|
-
setLoading(false);
|
|
101
|
-
}
|
|
102
|
-
};
|
|
66
|
+
try {
|
|
67
|
+
const credentials = btoa(`admin:${password}`);
|
|
68
|
+
const res = await fetch('/api/portal/login', {
|
|
69
|
+
headers: { 'Authorization': `Basic ${credentials}` },
|
|
70
|
+
credentials: 'include',
|
|
71
|
+
});
|
|
72
|
+
const data = await res.json();
|
|
103
73
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
74
|
+
if (res.ok && data.token) {
|
|
75
|
+
clearLoginState();
|
|
76
|
+
onLogin(data.token);
|
|
77
|
+
} else if (res.ok && data.requiresTOTP) {
|
|
78
|
+
setPendingToken(data.pendingToken);
|
|
79
|
+
saveLoginState(data.pendingToken);
|
|
80
|
+
setPhase('totp');
|
|
107
81
|
setError('');
|
|
82
|
+
} else {
|
|
83
|
+
setError(data.error || 'Invalid password');
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
setError('Could not reach server');
|
|
87
|
+
} finally {
|
|
88
|
+
setLoading(false);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
108
91
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
114
|
-
const data = await res.json();
|
|
115
|
-
|
|
116
|
-
if (res.ok && data.token) {
|
|
117
|
-
clearLoginState();
|
|
118
|
-
onLogin(data.token);
|
|
119
|
-
} else {
|
|
120
|
-
setError(data.error || 'Invalid code');
|
|
121
|
-
}
|
|
122
|
-
} catch {
|
|
123
|
-
setError('Could not reach server');
|
|
124
|
-
} finally {
|
|
125
|
-
setLoading(false);
|
|
126
|
-
}
|
|
127
|
-
};
|
|
92
|
+
const handleTotpSubmit = async () => {
|
|
93
|
+
if (!totpCode.trim() || loading) return;
|
|
94
|
+
setLoading(true);
|
|
95
|
+
setError('');
|
|
128
96
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetch(
|
|
99
|
+
`/api/portal/login/totp?pending=${encodeURIComponent(pendingToken)}&code=${encodeURIComponent(totpCode)}&trust=${trustDevice ? '1' : '0'}`,
|
|
100
|
+
{ credentials: 'include' },
|
|
101
|
+
);
|
|
102
|
+
const data = await res.json();
|
|
135
103
|
|
|
136
|
-
|
|
137
|
-
setPhase('password');
|
|
138
|
-
setError('');
|
|
139
|
-
setTotpCode('');
|
|
140
|
-
setUseRecovery(false);
|
|
104
|
+
if (res.ok && data.token) {
|
|
141
105
|
clearLoginState();
|
|
142
|
-
|
|
106
|
+
onLogin(data.token);
|
|
107
|
+
} else {
|
|
108
|
+
setError(data.error || 'Invalid code');
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
setError('Could not reach server');
|
|
112
|
+
} finally {
|
|
113
|
+
setLoading(false);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
118
|
+
if (e.key === 'Enter') {
|
|
119
|
+
if (phase === 'password') handleSubmit();
|
|
120
|
+
else handleTotpSubmit();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleBackToPassword = () => {
|
|
125
|
+
setPhase('password');
|
|
126
|
+
setError('');
|
|
127
|
+
setTotpCode('');
|
|
128
|
+
setUseRecovery(false);
|
|
129
|
+
clearLoginState();
|
|
130
|
+
};
|
|
143
131
|
|
|
144
|
-
|
|
145
|
-
'w-full bg-white/[0.05] border border-white/[0.08] text-white rounded-xl px-4 py-3 text-base outline-none input-glow placeholder:text-white/20 transition-all';
|
|
132
|
+
const inputCls = 'w-full bg-white/[0.05] border border-white/[0.08] text-white rounded-xl px-4 py-3 text-base outline-none input-glow placeholder:text-white/20 transition-all';
|
|
146
133
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
134
|
+
return (
|
|
135
|
+
<div className="flex flex-col items-center justify-center h-dvh px-6">
|
|
136
|
+
<div className="w-full max-w-[320px] flex flex-col items-center">
|
|
137
|
+
<AnimatePresence mode="wait">
|
|
138
|
+
{phase === 'password' ? (
|
|
139
|
+
<motion.div
|
|
140
|
+
key="password"
|
|
141
|
+
initial={{ opacity: 0, x: -20 }}
|
|
142
|
+
animate={{ opacity: 1, x: 0 }}
|
|
143
|
+
exit={{ opacity: 0, x: -20 }}
|
|
144
|
+
transition={{ duration: 0.2 }}
|
|
145
|
+
className="w-full flex flex-col items-center"
|
|
146
|
+
>
|
|
147
|
+
<div className="w-14 h-14 rounded-2xl bg-white/[0.04] border border-white/[0.08] flex items-center justify-center mb-5">
|
|
148
|
+
<Lock className="h-6 w-6 text-white/40" />
|
|
149
|
+
</div>
|
|
163
150
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
151
|
+
<h1 className="text-xl font-bold text-white tracking-tight mb-1">
|
|
152
|
+
Welcome back
|
|
153
|
+
</h1>
|
|
154
|
+
<p className="text-white/40 text-[13px] mb-6">
|
|
155
|
+
Enter your password to continue.
|
|
156
|
+
</p>
|
|
170
157
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
</div>
|
|
177
|
-
)}
|
|
158
|
+
{error && (
|
|
159
|
+
<div className="w-full bg-red-500/8 border border-red-500/15 rounded-xl px-4 py-2.5 mb-4">
|
|
160
|
+
<p className="text-red-400/90 text-[12px]">{error}</p>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
178
163
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
164
|
+
<input
|
|
165
|
+
type="password"
|
|
166
|
+
value={password}
|
|
167
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
168
|
+
onKeyDown={handleKeyDown}
|
|
169
|
+
placeholder="Password"
|
|
170
|
+
autoFocus
|
|
171
|
+
autoComplete="current-password"
|
|
172
|
+
className={inputCls}
|
|
173
|
+
/>
|
|
188
174
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
transition={{ duration: 0.2 }}
|
|
214
|
-
className="w-full flex flex-col items-center"
|
|
215
|
-
>
|
|
216
|
-
<div className="w-14 h-14 rounded-2xl bg-[#AF27E3]/10 border border-[#AF27E3]/20 flex items-center justify-center mb-5">
|
|
217
|
-
<Shield className="h-6 w-6 text-[#AF27E3]" />
|
|
218
|
-
</div>
|
|
175
|
+
<button
|
|
176
|
+
onClick={handleSubmit}
|
|
177
|
+
disabled={!password.trim() || loading}
|
|
178
|
+
className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
|
|
179
|
+
>
|
|
180
|
+
{loading ? (
|
|
181
|
+
<><LoaderCircle className="h-4 w-4 animate-spin" />Signing in...</>
|
|
182
|
+
) : (
|
|
183
|
+
<>Sign In<ArrowRight className="h-4 w-4" /></>
|
|
184
|
+
)}
|
|
185
|
+
</button>
|
|
186
|
+
</motion.div>
|
|
187
|
+
) : (
|
|
188
|
+
<motion.div
|
|
189
|
+
key="totp"
|
|
190
|
+
initial={{ opacity: 0, x: 20 }}
|
|
191
|
+
animate={{ opacity: 1, x: 0 }}
|
|
192
|
+
exit={{ opacity: 0, x: 20 }}
|
|
193
|
+
transition={{ duration: 0.2 }}
|
|
194
|
+
className="w-full flex flex-col items-center"
|
|
195
|
+
>
|
|
196
|
+
<div className="w-14 h-14 rounded-2xl bg-[#AF27E3]/10 border border-[#AF27E3]/20 flex items-center justify-center mb-5">
|
|
197
|
+
<Shield className="h-6 w-6 text-[#AF27E3]" />
|
|
198
|
+
</div>
|
|
219
199
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
: 'Open your authenticator app and enter the 6-digit code.'}
|
|
229
|
-
</p>
|
|
200
|
+
<h1 className="text-xl font-bold text-white tracking-tight mb-1">
|
|
201
|
+
{useRecovery ? 'Recovery code' : 'Enter your 2FA code'}
|
|
202
|
+
</h1>
|
|
203
|
+
<p className="text-white/40 text-[13px] mb-6">
|
|
204
|
+
{useRecovery
|
|
205
|
+
? 'Enter one of your recovery codes.'
|
|
206
|
+
: 'Open your authenticator app and enter the 6-digit code.'}
|
|
207
|
+
</p>
|
|
230
208
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
</div>
|
|
237
|
-
)}
|
|
209
|
+
{error && (
|
|
210
|
+
<div className="w-full bg-red-500/8 border border-red-500/15 rounded-xl px-4 py-2.5 mb-4">
|
|
211
|
+
<p className="text-red-400/90 text-[12px]">{error}</p>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
238
214
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
placeholder={
|
|
256
|
-
useRecovery ? 'Recovery code' : '000000'
|
|
257
|
-
}
|
|
258
|
-
className={
|
|
259
|
-
inputCls +
|
|
260
|
-
(useRecovery
|
|
261
|
-
? ''
|
|
262
|
-
: ' tracking-[0.3em] text-center font-mono')
|
|
263
|
-
}
|
|
264
|
-
/>
|
|
215
|
+
<input
|
|
216
|
+
ref={totpInputRef}
|
|
217
|
+
type="text"
|
|
218
|
+
inputMode={useRecovery ? 'text' : 'numeric'}
|
|
219
|
+
autoComplete="one-time-code"
|
|
220
|
+
maxLength={useRecovery ? 20 : 6}
|
|
221
|
+
value={totpCode}
|
|
222
|
+
onChange={(e) => {
|
|
223
|
+
setTotpCode(useRecovery ? e.target.value : e.target.value.replace(/\D/g, ''));
|
|
224
|
+
setError('');
|
|
225
|
+
}}
|
|
226
|
+
onKeyDown={handleKeyDown}
|
|
227
|
+
placeholder={useRecovery ? 'Recovery code' : '000000'}
|
|
228
|
+
autoFocus
|
|
229
|
+
className={inputCls + (useRecovery ? '' : ' tracking-[0.3em] text-center font-mono')}
|
|
230
|
+
/>
|
|
265
231
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
</div>
|
|
281
|
-
<span className="text-[13px] text-white/50">
|
|
282
|
-
Trust this device for 90 days
|
|
283
|
-
</span>
|
|
284
|
-
</label>
|
|
232
|
+
{/* Trust device checkbox */}
|
|
233
|
+
<label className="flex items-center gap-2.5 mt-4 w-full cursor-pointer">
|
|
234
|
+
<div
|
|
235
|
+
onClick={() => setTrustDevice(v => !v)}
|
|
236
|
+
className={`w-5 h-5 rounded-md border flex items-center justify-center transition-all ${
|
|
237
|
+
trustDevice
|
|
238
|
+
? 'bg-[#AF27E3] border-[#AF27E3]'
|
|
239
|
+
: 'bg-white/[0.04] border-white/[0.12]'
|
|
240
|
+
}`}
|
|
241
|
+
>
|
|
242
|
+
{trustDevice && <Check className="h-3.5 w-3.5 text-white" />}
|
|
243
|
+
</div>
|
|
244
|
+
<span className="text-[13px] text-white/50">Trust this device for 90 days</span>
|
|
245
|
+
</label>
|
|
285
246
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
<>
|
|
298
|
-
Verify
|
|
299
|
-
<ArrowRight className="h-4 w-4" />
|
|
300
|
-
</>
|
|
301
|
-
)}
|
|
302
|
-
</button>
|
|
247
|
+
<button
|
|
248
|
+
onClick={handleTotpSubmit}
|
|
249
|
+
disabled={!totpCode.trim() || loading}
|
|
250
|
+
className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
|
|
251
|
+
>
|
|
252
|
+
{loading ? (
|
|
253
|
+
<><LoaderCircle className="h-4 w-4 animate-spin" />Verifying...</>
|
|
254
|
+
) : (
|
|
255
|
+
<>Verify<ArrowRight className="h-4 w-4" /></>
|
|
256
|
+
)}
|
|
257
|
+
</button>
|
|
303
258
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
</motion.div>
|
|
326
|
-
)}
|
|
327
|
-
</AnimatePresence>
|
|
328
|
-
</div>
|
|
329
|
-
</div>
|
|
330
|
-
);
|
|
259
|
+
<div className="flex items-center gap-4 mt-4">
|
|
260
|
+
<button
|
|
261
|
+
onClick={handleBackToPassword}
|
|
262
|
+
className="text-[12px] text-white/30 hover:text-white/50 flex items-center gap-1 transition-colors"
|
|
263
|
+
>
|
|
264
|
+
<ArrowLeft className="h-3 w-3" />
|
|
265
|
+
Back
|
|
266
|
+
</button>
|
|
267
|
+
<button
|
|
268
|
+
onClick={() => { setUseRecovery(v => !v); setTotpCode(''); setError(''); }}
|
|
269
|
+
className="text-[12px] text-white/30 hover:text-white/50 transition-colors"
|
|
270
|
+
>
|
|
271
|
+
{useRecovery ? 'Use authenticator code' : 'Use a recovery code'}
|
|
272
|
+
</button>
|
|
273
|
+
</div>
|
|
274
|
+
</motion.div>
|
|
275
|
+
)}
|
|
276
|
+
</AnimatePresence>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
331
280
|
}
|