chordia-ui 3.2.0 → 3.2.2
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/dist/index.cjs2.js +2 -2
- package/dist/index.cjs2.js.map +1 -1
- package/dist/index.es2.js +1006 -906
- package/dist/index.es2.js.map +1 -1
- package/package.json +1 -1
- package/src/components/login/LoginPage.jsx +112 -12
package/package.json
CHANGED
|
@@ -157,6 +157,7 @@ export default function LoginPage({
|
|
|
157
157
|
onGoToLogin,
|
|
158
158
|
loading: externalLoading,
|
|
159
159
|
error: externalError,
|
|
160
|
+
codeError,
|
|
160
161
|
defaultView = 'signin',
|
|
161
162
|
}) {
|
|
162
163
|
const [email, setEmail] = useState('');
|
|
@@ -166,8 +167,9 @@ export default function LoginPage({
|
|
|
166
167
|
const [internalError, setInternalError] = useState(null);
|
|
167
168
|
const [view, setView] = useState(defaultView);
|
|
168
169
|
const [codeEmail, setCodeEmail] = useState('');
|
|
170
|
+
const [codeFullName, setCodeFullName] = useState('');
|
|
169
171
|
const [verifyEmail, setVerifyEmail] = useState('');
|
|
170
|
-
const [otpDigits, setOtpDigits] = useState(['', '', '', '']);
|
|
172
|
+
const [otpDigits, setOtpDigits] = useState(['', '', '', '', '', '']);
|
|
171
173
|
const otpRefs = useRef([]);
|
|
172
174
|
const [firstName, setFirstName] = useState('');
|
|
173
175
|
const [lastName, setLastName] = useState('');
|
|
@@ -185,9 +187,56 @@ export default function LoginPage({
|
|
|
185
187
|
return () => clearInterval(t);
|
|
186
188
|
}, []);
|
|
187
189
|
|
|
190
|
+
// Sync React state with browser autofill (autofill doesn't fire onChange)
|
|
191
|
+
const emailRef = useRef(null);
|
|
192
|
+
const passwordRef = useRef(null);
|
|
193
|
+
const [autofilled, setAutofilled] = useState(false);
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
const sync = () => {
|
|
196
|
+
const e = emailRef.current;
|
|
197
|
+
const p = passwordRef.current;
|
|
198
|
+
if (e && e.value) setEmail(e.value);
|
|
199
|
+
if (p && p.value) setPassword(p.value);
|
|
200
|
+
};
|
|
201
|
+
// Poll for autofill values at multiple intervals
|
|
202
|
+
const timers = [100, 300, 600, 1000, 2000].map(ms => setTimeout(sync, ms));
|
|
203
|
+
|
|
204
|
+
// Detect Chrome/Safari autofill via the :-webkit-autofill animation
|
|
205
|
+
const handleAnimationStart = (e) => {
|
|
206
|
+
if (e.animationName === 'onAutoFillStart' || e.target.matches(':-webkit-autofill')) {
|
|
207
|
+
setAutofilled(true);
|
|
208
|
+
setTimeout(sync, 50);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const eEl = emailRef.current;
|
|
212
|
+
const pEl = passwordRef.current;
|
|
213
|
+
eEl?.addEventListener('animationstart', handleAnimationStart);
|
|
214
|
+
pEl?.addEventListener('animationstart', handleAnimationStart);
|
|
215
|
+
// Also listen for input/change events that some password managers fire
|
|
216
|
+
const onInput = () => setTimeout(sync, 0);
|
|
217
|
+
eEl?.addEventListener('input', onInput);
|
|
218
|
+
pEl?.addEventListener('input', onInput);
|
|
219
|
+
|
|
220
|
+
return () => {
|
|
221
|
+
timers.forEach(clearTimeout);
|
|
222
|
+
eEl?.removeEventListener('animationstart', handleAnimationStart);
|
|
223
|
+
pEl?.removeEventListener('animationstart', handleAnimationStart);
|
|
224
|
+
eEl?.removeEventListener('input', onInput);
|
|
225
|
+
pEl?.removeEventListener('input', onInput);
|
|
226
|
+
};
|
|
227
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
228
|
+
|
|
229
|
+
// Switch to full-name screen when host app signals unregistered email
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
if (codeError && view === 'verifycode') {
|
|
232
|
+
setOtpDigits(['', '', '', '', '', '']);
|
|
233
|
+
setView('getcodewithname');
|
|
234
|
+
}
|
|
235
|
+
}, [codeError]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
236
|
+
|
|
188
237
|
const loading = externalLoading ?? internalLoading;
|
|
189
238
|
const error = externalError ?? internalError;
|
|
190
|
-
const canSubmit = email && password && !loading;
|
|
239
|
+
const canSubmit = (email && password && !loading) || (autofilled && !loading);
|
|
191
240
|
|
|
192
241
|
// Error-aware focus handlers for sign-in fields
|
|
193
242
|
const focusErr = (e) => {
|
|
@@ -204,7 +253,7 @@ export default function LoginPage({
|
|
|
204
253
|
const next = [...otpDigits];
|
|
205
254
|
next[index] = digit;
|
|
206
255
|
setOtpDigits(next);
|
|
207
|
-
if (digit && index <
|
|
256
|
+
if (digit && index < 5) otpRefs.current[index + 1]?.focus();
|
|
208
257
|
};
|
|
209
258
|
|
|
210
259
|
const handleOtpKeyDown = (index, e) => {
|
|
@@ -216,10 +265,14 @@ export default function LoginPage({
|
|
|
216
265
|
const handleSubmit = async (e) => {
|
|
217
266
|
e.preventDefault();
|
|
218
267
|
if (!canSubmit) return;
|
|
268
|
+
// Read directly from DOM if React state missed autofill
|
|
269
|
+
const submitEmail = email || emailRef.current?.value || '';
|
|
270
|
+
const submitPassword = password || passwordRef.current?.value || '';
|
|
271
|
+
if (!submitEmail || !submitPassword) return;
|
|
219
272
|
setInternalError(null);
|
|
220
273
|
setInternalLoading(true);
|
|
221
274
|
try {
|
|
222
|
-
await onLogin?.(
|
|
275
|
+
await onLogin?.(submitEmail, submitPassword);
|
|
223
276
|
} catch (err) {
|
|
224
277
|
setInternalError(err?.message || 'Something went wrong. Please try again.');
|
|
225
278
|
} finally {
|
|
@@ -462,7 +515,7 @@ export default function LoginPage({
|
|
|
462
515
|
|
|
463
516
|
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', gap: 16 }}>
|
|
464
517
|
<Field label="Enter one-time code" gap={12}>
|
|
465
|
-
<div style={{ display: 'flex', gap:
|
|
518
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
466
519
|
{otpDigits.map((digit, i) => (
|
|
467
520
|
<input key={i} ref={(el) => { otpRefs.current[i] = el; }}
|
|
468
521
|
type="text" inputMode="numeric" maxLength={1} value={digit}
|
|
@@ -470,9 +523,9 @@ export default function LoginPage({
|
|
|
470
523
|
onKeyDown={(e) => handleOtpKeyDown(i, e)}
|
|
471
524
|
autoFocus={i === 0}
|
|
472
525
|
style={{
|
|
473
|
-
|
|
526
|
+
flex: 1, minWidth: 0, height: 40, textAlign: 'center', fontSize: 16, fontWeight: 600,
|
|
474
527
|
fontFamily: FF, color: 'var(--color-text)', border: '1px solid var(--color-input-border)',
|
|
475
|
-
borderRadius:
|
|
528
|
+
borderRadius: 4, outline: 'none', background: 'white', boxSizing: 'border-box',
|
|
476
529
|
transition: 'border-color 0.15s, box-shadow 0.15s', caretColor: GREEN,
|
|
477
530
|
}}
|
|
478
531
|
onFocus={focusGreen} onBlur={blurGray}
|
|
@@ -491,7 +544,7 @@ export default function LoginPage({
|
|
|
491
544
|
</>
|
|
492
545
|
|
|
493
546
|
) : view === 'getcode' ? (
|
|
494
|
-
/* ── Get Code ── */
|
|
547
|
+
/* ── Get Code (original — email only) ── */
|
|
495
548
|
<>
|
|
496
549
|
<ViewHeading title="Get code" subtitle="We'll send a one time pass-code to your email" />
|
|
497
550
|
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', gap: 16 }}>
|
|
@@ -501,7 +554,7 @@ export default function LoginPage({
|
|
|
501
554
|
style={inputBase} onFocus={focusGreen} onBlur={blurGray} />
|
|
502
555
|
</Field>
|
|
503
556
|
|
|
504
|
-
<GreenButton onClick={() => { setVerifyEmail(codeEmail); setOtpDigits(['', '', '', '']); setView('verifycode'); onOneTimeCode?.(codeEmail); }} disabled={!codeEmail}>
|
|
557
|
+
<GreenButton onClick={() => { setVerifyEmail(codeEmail); setOtpDigits(['', '', '', '', '', '']); setView('verifycode'); onOneTimeCode?.(codeEmail); }} disabled={!codeEmail}>
|
|
505
558
|
Send One-time Code
|
|
506
559
|
</GreenButton>
|
|
507
560
|
<Divider />
|
|
@@ -510,6 +563,53 @@ export default function LoginPage({
|
|
|
510
563
|
</div>
|
|
511
564
|
</>
|
|
512
565
|
|
|
566
|
+
) : view === 'getcodewithname' ? (
|
|
567
|
+
/* ── Get Code with Full Name (unregistered email) ── */
|
|
568
|
+
<>
|
|
569
|
+
<ViewHeading title="Complete your sign in" subtitle="We’ve sent a one-time code to your email. Please enter the code and your full name to sign in" />
|
|
570
|
+
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', gap: 16 }}>
|
|
571
|
+
|
|
572
|
+
<Field label="Email" gap={12}>
|
|
573
|
+
<input type="email" value={codeEmail} onChange={(e) => setCodeEmail(e.target.value)}
|
|
574
|
+
placeholder="workemail@example.com" autoComplete="email"
|
|
575
|
+
style={inputBase} onFocus={focusGreen} onBlur={blurGray} />
|
|
576
|
+
</Field>
|
|
577
|
+
|
|
578
|
+
<Field label="Full Name" gap={12}>
|
|
579
|
+
<input type="text" value={codeFullName} onChange={(e) => setCodeFullName(e.target.value)}
|
|
580
|
+
placeholder="Enter your full name" autoComplete="name"
|
|
581
|
+
style={inputBase} onFocus={focusGreen} onBlur={blurGray} />
|
|
582
|
+
</Field>
|
|
583
|
+
|
|
584
|
+
<Field label="Enter one-time code" gap={12}>
|
|
585
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
586
|
+
{otpDigits.map((digit, i) => (
|
|
587
|
+
<input key={i} ref={(el) => { otpRefs.current[i] = el; }}
|
|
588
|
+
type="text" inputMode="numeric" maxLength={1} value={digit}
|
|
589
|
+
onChange={(e) => handleOtpChange(i, e.target.value)}
|
|
590
|
+
onKeyDown={(e) => handleOtpKeyDown(i, e)}
|
|
591
|
+
autoFocus={i === 0}
|
|
592
|
+
style={{
|
|
593
|
+
flex: 1, minWidth: 0, height: 40, textAlign: 'center', fontSize: 16, fontWeight: 600,
|
|
594
|
+
fontFamily: FF, color: 'var(--color-text)', border: '1px solid var(--color-input-border)',
|
|
595
|
+
borderRadius: 4, outline: 'none', background: 'white', boxSizing: 'border-box',
|
|
596
|
+
transition: 'border-color 0.15s, box-shadow 0.15s', caretColor: GREEN,
|
|
597
|
+
}}
|
|
598
|
+
onFocus={focusGreen} onBlur={blurGray}
|
|
599
|
+
/>
|
|
600
|
+
))}
|
|
601
|
+
</div>
|
|
602
|
+
</Field>
|
|
603
|
+
|
|
604
|
+
<GreenButton onClick={() => { setVerifyEmail(codeEmail); onOneTimeCode?.(codeEmail, codeFullName, otpDigits.join('')); }} disabled={!codeEmail || !codeFullName || otpDigits.some(d => !d)}>
|
|
605
|
+
Verify & Sign In
|
|
606
|
+
</GreenButton>
|
|
607
|
+
<Divider />
|
|
608
|
+
<NavRow text="I have my password" linkText="Back to Sign in" onClick={() => { setView('signin'); onGoToLogin?.(); }} />
|
|
609
|
+
<TermsFooter onTerms={onTerms} onPrivacyPolicy={onPrivacyPolicy} paddingTop={40} />
|
|
610
|
+
</div>
|
|
611
|
+
</>
|
|
612
|
+
|
|
513
613
|
) : view === 'resetpassword' ? (
|
|
514
614
|
/* ── Reset Password ── */
|
|
515
615
|
<>
|
|
@@ -539,7 +639,7 @@ export default function LoginPage({
|
|
|
539
639
|
|
|
540
640
|
<form onSubmit={handleSubmit} style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
541
641
|
<Field label="Email" gap={12}>
|
|
542
|
-
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
|
|
642
|
+
<input ref={emailRef} type="email" value={email} onChange={(e) => setEmail(e.target.value)}
|
|
543
643
|
placeholder="workemail@example.com" required autoComplete="email"
|
|
544
644
|
style={{ ...inputBase, border: `1px solid ${error ? 'var(--color-text)' : 'var(--color-input-border)'}` }}
|
|
545
645
|
onFocus={focusErr} onBlur={blurErr} />
|
|
@@ -547,7 +647,7 @@ export default function LoginPage({
|
|
|
547
647
|
|
|
548
648
|
<Field label="Password" gap={12}>
|
|
549
649
|
<div style={{ position: 'relative' }}>
|
|
550
|
-
<input type={showPassword ? 'text' : 'password'} value={password}
|
|
650
|
+
<input ref={passwordRef} type={showPassword ? 'text' : 'password'} value={password}
|
|
551
651
|
onChange={(e) => setPassword(e.target.value)}
|
|
552
652
|
placeholder="Enter password" required autoComplete="current-password"
|
|
553
653
|
style={{ ...inputBase, padding: '10px 44px 10px 16px', border: `1px solid ${error ? 'var(--color-text)' : 'var(--color-input-border)'}` }}
|
|
@@ -588,7 +688,7 @@ export default function LoginPage({
|
|
|
588
688
|
</div>
|
|
589
689
|
|
|
590
690
|
<NavRow text="Not a member yet?" linkText="Sign Up" onClick={() => { setView('signup'); onSignUp?.(); }} />
|
|
591
|
-
<TermsFooter onTerms={onTerms} onPrivacyPolicy={onPrivacyPolicy} paddingTop={
|
|
691
|
+
<TermsFooter onTerms={onTerms} onPrivacyPolicy={onPrivacyPolicy} paddingTop={16} />
|
|
592
692
|
</div>
|
|
593
693
|
</div>
|
|
594
694
|
)}
|