Implement registration invite code flow and admin invite codes

This commit is contained in:
2026-04-30 20:49:38 +08:00
parent 2aef81e51d
commit 42aab671ed
32 changed files with 1241 additions and 179 deletions

View File

@@ -10,7 +10,7 @@ import { getStoredLastLoginPhone } from '../../services/authService';
import { CaptchaChallengeField } from './CaptchaChallengeField';
type SmsScene = 'login' | 'reset_password';
type LoginTab = 'phone' | 'password';
type LoginTab = 'phone' | 'password' | 'register';
type LoginScreenProps = {
isOpen: boolean;
@@ -21,6 +21,8 @@ type LoginScreenProps = {
wechatLoading: boolean;
error: string;
captchaChallenge: AuthCaptchaChallenge | null;
initialMode?: 'login' | 'register';
initialInviteCode?: string;
onClose: () => void;
onSendCode: (
phone: string,
@@ -33,7 +35,11 @@ type LoginScreenProps = {
cooldownSeconds: number;
expiresInSeconds: number;
}>;
onPhoneSubmit: (phone: string, code: string) => Promise<void>;
onPhoneSubmit: (
phone: string,
code: string,
inviteCode?: string,
) => Promise<void>;
onPasswordSubmit: (phone: string, password: string) => Promise<void>;
onResetPassword: (
phone: string,
@@ -52,6 +58,8 @@ export function LoginScreen({
wechatLoading,
error,
captchaChallenge,
initialMode = 'login',
initialInviteCode = '',
onClose,
onSendCode,
onPhoneSubmit,
@@ -66,6 +74,7 @@ export function LoginScreen({
const [resetPhone, setResetPhone] = useState('');
const [resetCode, setResetCode] = useState('');
const [resetPasswordValue, setResetPasswordValue] = useState('');
const [inviteCode, setInviteCode] = useState(initialInviteCode);
const [captchaAnswer, setCaptchaAnswer] = useState('');
const [cooldownSeconds, setCooldownSeconds] = useState(0);
const [resetCooldownSeconds, setResetCooldownSeconds] = useState(0);
@@ -88,16 +97,23 @@ export function LoginScreen({
setResetPhone('');
setResetCode('');
setResetPasswordValue('');
setInviteCode(initialInviteCode);
setCaptchaAnswer('');
setCooldownSeconds(0);
setResetCooldownSeconds(0);
setHint('');
setActiveLoginTab(phoneLoginEnabled ? 'phone' : 'password');
}, [isOpen, phoneLoginEnabled]);
setActiveLoginTab(
initialMode === 'register' && phoneLoginEnabled
? 'register'
: phoneLoginEnabled
? 'phone'
: 'password',
);
}, [initialInviteCode, initialMode, isOpen, phoneLoginEnabled]);
useEffect(() => {
if (
activeLoginTab === 'phone' &&
(activeLoginTab === 'phone' || activeLoginTab === 'register') &&
!phoneLoginEnabled &&
passwordLoginEnabled
) {
@@ -196,9 +212,11 @@ export function LoginScreen({
/>
) : (
<div className="flex flex-col gap-5 px-5 py-5">
{phoneLoginEnabled && passwordLoginEnabled ? (
{phoneLoginEnabled ? (
<div
className="grid grid-cols-2 gap-2"
className={`grid gap-2 ${
passwordLoginEnabled ? 'grid-cols-3' : 'grid-cols-2'
}`}
role="tablist"
aria-label="登录方式"
>
@@ -208,11 +226,19 @@ export function LoginScreen({
>
</LoginTabButton>
{passwordLoginEnabled ? (
<LoginTabButton
active={activeLoginTab === 'password'}
onClick={() => setActiveLoginTab('password')}
>
</LoginTabButton>
) : null}
<LoginTabButton
active={activeLoginTab === 'password'}
onClick={() => setActiveLoginTab('password')}
active={activeLoginTab === 'register'}
onClick={() => setActiveLoginTab('register')}
>
</LoginTabButton>
</div>
) : null}
@@ -312,6 +338,42 @@ export function LoginScreen({
/>
) : null}
{phoneLoginEnabled && activeLoginTab === 'register' ? (
<PhoneCodeForm
phone={phone}
code={code}
inviteCode={inviteCode}
captchaAnswer={captchaAnswer}
captchaChallenge={captchaChallenge}
cooldownSeconds={cooldownSeconds}
sendingCode={sendingCode}
loggingIn={loggingIn}
error={error}
hint={hint}
submitLabel="注册"
enabled={phoneLoginEnabled}
showPhoneField
showInviteCodeField
onPhoneChange={setPhone}
onCodeChange={setCode}
onInviteCodeChange={setInviteCode}
onCaptchaAnswerChange={setCaptchaAnswer}
onSendCode={async () => {
setHint('');
const result = await onSendCode(phone, 'login', {
challengeId: captchaChallenge?.challengeId,
answer: captchaAnswer,
});
setCooldownSeconds(result.cooldownSeconds);
setHint(
`短信请求已提交,验证码有效期约 ${Math.max(1, Math.round(result.expiresInSeconds / 60))} 分钟。`,
);
setCaptchaAnswer('');
}}
onSubmit={() => onPhoneSubmit(phone, code, inviteCode)}
/>
) : null}
{!passwordLoginEnabled &&
!phoneLoginEnabled &&
!wechatLoginEnabled ? (
@@ -358,6 +420,7 @@ function LoginTabButton({
function PhoneCodeForm({
phone,
code,
inviteCode = '',
captchaAnswer,
captchaChallenge,
cooldownSeconds,
@@ -368,14 +431,17 @@ function PhoneCodeForm({
submitLabel,
enabled,
showPhoneField,
showInviteCodeField = false,
onPhoneChange,
onCodeChange,
onInviteCodeChange,
onCaptchaAnswerChange,
onSendCode,
onSubmit,
}: {
phone: string;
code: string;
inviteCode?: string;
captchaAnswer: string;
captchaChallenge: AuthCaptchaChallenge | null;
cooldownSeconds: number;
@@ -386,8 +452,10 @@ function PhoneCodeForm({
submitLabel: string;
enabled: boolean;
showPhoneField: boolean;
showInviteCodeField?: boolean;
onPhoneChange: (value: string) => void;
onCodeChange: (value: string) => void;
onInviteCodeChange?: (value: string) => void;
onCaptchaAnswerChange: (value: string) => void;
onSendCode: () => Promise<void>;
onSubmit: () => Promise<void>;
@@ -418,6 +486,19 @@ function PhoneCodeForm({
</label>
) : null}
{showInviteCodeField ? (
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
<span></span>
<input
className="platform-input"
autoComplete="off"
value={inviteCode}
onChange={(event) => onInviteCodeChange?.(event.target.value)}
placeholder="邀请码"
/>
</label>
) : null}
<label className="grid gap-2 text-sm text-[var(--platform-text-base)]">
<span></span>
<div className="flex gap-3">