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

@@ -59,6 +59,14 @@ type AuthStatus =
const FALLBACK_LOGIN_METHODS: AuthLoginMethod[] = ['password'];
function readInviteCodeFromLocation(): string {
const params = new URLSearchParams(window.location.search || '');
return (params.get('inviteCode') || params.get('invite_code') || '')
.trim()
.replace(/[^0-9a-z]/gi, '')
.toUpperCase();
}
function normalizeAvailableLoginMethods(
methods: AuthLoginMethod[] | null | undefined,
): AuthLoginMethod[] {
@@ -83,6 +91,10 @@ export function AuthGate({ children }: AuthGateProps) {
const [bindingPhone, setBindingPhone] = useState(false);
const [wechatLoading, setWechatLoading] = useState(false);
const [showLoginModal, setShowLoginModal] = useState(false);
const [loginInitialMode, setLoginInitialMode] = useState<
'login' | 'register'
>('login');
const [pendingInviteCode, setPendingInviteCode] = useState('');
const [showSettingsModal, setShowSettingsModal] = useState(false);
const [settingsEntryMode, setSettingsEntryMode] = useState<
'settings' | 'account'
@@ -102,6 +114,7 @@ export function AuthGate({ children }: AuthGateProps) {
const [changePhoneCaptchaChallenge, setChangePhoneCaptchaChallenge] =
useState<AuthCaptchaChallenge | null>(null);
const pendingProtectedActionRef = useRef<(() => void) | null>(null);
const autoOpenedInviteCodeRef = useRef<string | null>(null);
const hasRenderedPlatformContentRef = useRef(false);
const canKeepPlatformContentMounted =
hasRenderedPlatformContentRef.current &&
@@ -169,6 +182,8 @@ export function AuthGate({ children }: AuthGateProps) {
const closeLoginModal = useCallback(() => {
pendingProtectedActionRef.current = null;
setShowLoginModal(false);
setLoginInitialMode('login');
setPendingInviteCode('');
setLoginCaptchaChallenge(null);
setError('');
}, []);
@@ -187,6 +202,8 @@ export function AuthGate({ children }: AuthGateProps) {
}
pendingProtectedActionRef.current = postLoginAction ?? null;
setLoginInitialMode('login');
setPendingInviteCode('');
setShowLoginModal(true);
},
[readyUser],
@@ -224,6 +241,24 @@ export function AuthGate({ children }: AuthGateProps) {
openLoginModal();
}, [openLoginModal, readyUser]);
useEffect(() => {
if (status !== 'unauthenticated' || readyUser || showLoginModal) {
return;
}
const inviteCode = readInviteCodeFromLocation();
if (!inviteCode) {
return;
}
if (autoOpenedInviteCodeRef.current === inviteCode) {
return;
}
autoOpenedInviteCodeRef.current = inviteCode;
pendingProtectedActionRef.current = null;
setPendingInviteCode(inviteCode);
setLoginInitialMode('register');
setShowLoginModal(true);
}, [readyUser, showLoginModal, status]);
useEffect(() => {
let isActive = true;
@@ -703,6 +738,8 @@ export function AuthGate({ children }: AuthGateProps) {
wechatLoading={wechatLoading}
error={error}
captchaChallenge={loginCaptchaChallenge}
initialMode={loginInitialMode}
initialInviteCode={pendingInviteCode}
onClose={closeLoginModal}
onSendCode={async (phone, scene, captcha) => {
setSendingCode(true);
@@ -727,14 +764,21 @@ export function AuthGate({ children }: AuthGateProps) {
setSendingCode(false);
}
}}
onPhoneSubmit={async (phone, code) => {
onPhoneSubmit={async (phone, code, inviteCode) => {
setLoggingIn(true);
setError('');
try {
const nextUser = await loginWithPhoneCode(phone, code);
const response = await loginWithPhoneCode(
phone,
code,
inviteCode,
);
setStoredLastLoginPhone(phone);
setLoginCaptchaChallenge(null);
activateReadyUser(nextUser);
if (response.referral && !response.referral.ok) {
setError(response.referral.message || '邀请码未绑定');
}
activateReadyUser(response.user);
} catch (loginError) {
setError(
loginError instanceof Error