Implement registration invite code flow and admin invite codes
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user