feat(web): full auth screens rework to match design + old-app business rules
Rebuilt tenant/login/register/reset-password to match the design handoff's 01-account screens precisely (previously only loosely approximated), and ported the old app's real business rules instead of guessing: - LoginPage: desktop split branding panel (>=900px), terms-agreement checkbox gating submit, forgot-password link, password visibility toggle, full ctrl-error translation table (lib/tinode/errors.ts, ported from the old app's CTRL_ERROR_TEXT_CN/translateCtrlError -- registration/login go over native WS, not REST, so client.ts's REASON_MESSAGES never applied). - RegisterPage: rebuilt as the real unified username-OR-phone form (not phone-only) -- username optional unless phone is blank, invite code required+validated only on the username-only path, matching the server's actual resolveRegistrationAccount rules. connection.ts's registerAccountByPhone generalized into registerAccount() supporting both paths, including the Tinode.credential(null) crash the old app had to work around on the username-only path. - New ResetPasswordPage wired to /auth/password-reset/* (a different endpoint family than registration's /auth/verification-codes/*). - Shared components: PasswordInput, VerificationCodeButton, Agreement, Toast. - Fixed tinode-sdk.d.ts: the real method is getCurrentUserID(), not the invented getAuthenticatedUserID() -- caught live, login crashed past a successful auth. - Fixed a responsive bug present on all four auth pages: fixed w-[380px] forms overflowed past the viewport edge below ~430px wide. Verified end-to-end against the real backend (api.118im.com, tenant "default"): fresh phone registration, password reset, re-login with the new password, and the username-only path's invite-code rejection all round-tripped correctly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
63c1b84b41
commit
aa1c46ff22
@@ -0,0 +1,63 @@
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import { requestVerificationCode } from '@/lib/api/auth';
|
||||
import { ImApiError } from '@/lib/api/client';
|
||||
|
||||
const PHONE_RE = /^\d{11}$/;
|
||||
|
||||
interface VerificationCodeButtonProps {
|
||||
phone: string;
|
||||
scene: 'register' | 'reset_password';
|
||||
onChallenge: (challengeId: string) => void;
|
||||
onError: (message: string) => void;
|
||||
}
|
||||
|
||||
/** "获取验证码" button with resend countdown. Ported from the old app's VerificationButton. */
|
||||
export function VerificationCodeButton({ phone, scene, onChallenge, onError }: VerificationCodeButtonProps) {
|
||||
const [seconds, setSeconds] = useState(0);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const timerRef = useRef<number | undefined>(undefined);
|
||||
|
||||
const tick = (initial: number) => {
|
||||
window.clearInterval(timerRef.current);
|
||||
setSeconds(initial);
|
||||
timerRef.current = window.setInterval(() => {
|
||||
setSeconds((s) => {
|
||||
if (s <= 1) {
|
||||
window.clearInterval(timerRef.current);
|
||||
return 0;
|
||||
}
|
||||
return s - 1;
|
||||
});
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const click = async () => {
|
||||
if (busy || seconds > 0 || !PHONE_RE.test(phone)) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const data = await requestVerificationCode(scene, phone);
|
||||
onChallenge(data.challenge_id);
|
||||
tick(data.resend_after_seconds || 60);
|
||||
} catch (err) {
|
||||
onError(err instanceof ImApiError ? err.message : '获取验证码失败');
|
||||
// Rate-limited but a still-valid challenge already exists for this phone/scene -- reuse it.
|
||||
const details = err instanceof ImApiError ? (err.details as Record<string, unknown> | undefined) : undefined;
|
||||
if (typeof details?.retry_after_seconds === 'number') tick(details.retry_after_seconds);
|
||||
if (typeof details?.challenge_id === 'string') onChallenge(details.challenge_id);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={click}
|
||||
disabled={busy || seconds > 0 || !PHONE_RE.test(phone)}
|
||||
className="w-32 flex-none rounded-lg bg-accent-soft-bg text-sm text-accent disabled:bg-[#e6e9ee] disabled:text-text-tertiary-3"
|
||||
>
|
||||
{seconds > 0 ? `重新获取 (${seconds}s)` : busy ? '发送中…' : '获取验证码'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user