71a6ed5945
手机号注册提交链接中的邀请码。 系统邀请码 metadata.tags 会合并到账户标签。 更新自动兑换邀请码的前端与模块测试。
1080 lines
34 KiB
TypeScript
1080 lines
34 KiB
TypeScript
/* eslint-disable react-refresh/only-export-components */
|
|
|
|
import {
|
|
type ReactNode,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
|
|
import { useGameSettings } from '../../hooks/useGameSettings';
|
|
import {
|
|
AUTH_STATE_EVENT,
|
|
getStoredAccessToken,
|
|
refreshStoredAccessToken,
|
|
} from '../../services/apiClient';
|
|
import {
|
|
type AuthAuditLogEntry,
|
|
type AuthCaptchaChallenge,
|
|
authEntry,
|
|
type AuthLoginMethod,
|
|
type AuthRiskBlockSummary,
|
|
type AuthSessionSnapshot,
|
|
type AuthSessionSummary,
|
|
type AuthUser,
|
|
bindWechatPhone,
|
|
changePassword,
|
|
changePhoneNumber,
|
|
consumeAuthCallbackResult,
|
|
getAuthAuditLogs,
|
|
getAuthLoginOptions,
|
|
getAuthRiskBlocks,
|
|
getAuthSessions,
|
|
getCaptchaChallengeFromError,
|
|
getCurrentAuthUser,
|
|
liftAuthRiskBlock,
|
|
loginWithPhoneCode,
|
|
logoutAllAuthSessions,
|
|
logoutAuthUser,
|
|
redeemRegistrationInviteCode,
|
|
resetPassword,
|
|
revokeAuthSessions,
|
|
sendPhoneLoginCode,
|
|
setStoredLastLoginPhone,
|
|
startWechatLogin,
|
|
} from '../../services/authService';
|
|
import {
|
|
getHostRuntime,
|
|
reloadHostWebView,
|
|
requestHostLogin,
|
|
} from '../../services/host-bridge/hostBridge';
|
|
import { PlatformActionButton } from '../common/PlatformActionButton';
|
|
import { AccountModal } from './AccountModal';
|
|
import { AuthUiContext, type PlatformSettingsSection } from './AuthUiContext';
|
|
import { BindPhoneScreen } from './BindPhoneScreen';
|
|
import { LoginScreen } from './LoginScreen';
|
|
import { RegistrationInviteModal } from './RegistrationInviteModal';
|
|
|
|
type AuthGateProps = {
|
|
children: ReactNode;
|
|
};
|
|
|
|
type AuthStatus =
|
|
| 'checking'
|
|
| 'recovering'
|
|
| 'unauthenticated'
|
|
| 'pending_bind_phone'
|
|
| 'ready'
|
|
| 'error';
|
|
|
|
const REQUIRED_LOGIN_METHODS: AuthLoginMethod[] = ['phone', 'password'];
|
|
|
|
let reloadBrowserPageForAuthStateChange = () => {
|
|
window.location.reload();
|
|
};
|
|
|
|
function reloadHostPageForAuthStateChange() {
|
|
void reloadHostWebView()
|
|
.then((handled) => {
|
|
if (!handled) {
|
|
reloadBrowserPageForAuthStateChange();
|
|
}
|
|
})
|
|
.catch(() => {
|
|
reloadBrowserPageForAuthStateChange();
|
|
});
|
|
}
|
|
|
|
let reloadCurrentPageForAuthStateChange = reloadHostPageForAuthStateChange;
|
|
|
|
export function setAuthGateReloadForTest(handler: (() => void) | null) {
|
|
reloadCurrentPageForAuthStateChange =
|
|
handler ?? reloadHostPageForAuthStateChange;
|
|
}
|
|
|
|
export function setAuthGateBrowserReloadForTest(handler: (() => void) | null) {
|
|
reloadBrowserPageForAuthStateChange =
|
|
handler ??
|
|
(() => {
|
|
window.location.reload();
|
|
});
|
|
}
|
|
|
|
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[] {
|
|
const normalizedMethods = Array.from(new Set(methods ?? []));
|
|
|
|
// 登录面板的核心入口必须稳定展示,login-options 只补充微信等环境相关入口。
|
|
return Array.from(
|
|
new Set<AuthLoginMethod>([
|
|
...REQUIRED_LOGIN_METHODS,
|
|
...normalizedMethods,
|
|
]),
|
|
);
|
|
}
|
|
|
|
type AuthHydrateSessionResult =
|
|
| {
|
|
kind: 'authenticated';
|
|
session: AuthSessionSnapshot & {
|
|
user: AuthUser;
|
|
};
|
|
}
|
|
| {
|
|
kind: 'guest';
|
|
session: AuthSessionSnapshot | null;
|
|
};
|
|
|
|
export function AuthGate({ children }: AuthGateProps) {
|
|
const [status, setStatus] = useState<AuthStatus>('checking');
|
|
const [user, setUser] = useState<AuthUser | null>(null);
|
|
const [availableLoginMethods, setAvailableLoginMethods] = useState<
|
|
AuthLoginMethod[]
|
|
>([]);
|
|
const [error, setError] = useState('');
|
|
const [sendingCode, setSendingCode] = useState(false);
|
|
const [loggingIn, setLoggingIn] = useState(false);
|
|
const [bindingPhone, setBindingPhone] = useState(false);
|
|
const [wechatLoading, setWechatLoading] = useState(false);
|
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
const [pendingInviteCode, setPendingInviteCode] = useState('');
|
|
const [showRegistrationInviteModal, setShowRegistrationInviteModal] =
|
|
useState(false);
|
|
const [submittingRegistrationInvite, setSubmittingRegistrationInvite] =
|
|
useState(false);
|
|
const [registrationInviteError, setRegistrationInviteError] = useState('');
|
|
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
|
const [settingsEntryMode, setSettingsEntryMode] = useState<
|
|
'settings' | 'account'
|
|
>('settings');
|
|
const [initialSettingsSection, setInitialSettingsSection] =
|
|
useState<PlatformSettingsSection | null>(null);
|
|
const [sessions, setSessions] = useState<AuthSessionSummary[]>([]);
|
|
const [loadingSessions, setLoadingSessions] = useState(false);
|
|
const [revokingSessionIds, setRevokingSessionIds] = useState<string[]>([]);
|
|
const [auditLogs, setAuditLogs] = useState<AuthAuditLogEntry[]>([]);
|
|
const [loadingAuditLogs, setLoadingAuditLogs] = useState(false);
|
|
const [riskBlocks, setRiskBlocks] = useState<AuthRiskBlockSummary[]>([]);
|
|
const [loadingRiskBlocks, setLoadingRiskBlocks] = useState(false);
|
|
const [loginCaptchaChallenge, setLoginCaptchaChallenge] =
|
|
useState<AuthCaptchaChallenge | null>(null);
|
|
const [bindCaptchaChallenge, setBindCaptchaChallenge] =
|
|
useState<AuthCaptchaChallenge | null>(null);
|
|
const [changePhoneCaptchaChallenge, setChangePhoneCaptchaChallenge] =
|
|
useState<AuthCaptchaChallenge | null>(null);
|
|
const pendingProtectedActionRef = useRef<(() => void) | null>(null);
|
|
const autoOpenedInviteCodeRef = useRef<string | null>(null);
|
|
const hasRenderedPlatformContentRef = useRef(false);
|
|
const authHydrateVersionRef = useRef(0);
|
|
const lastStableAuthPresenceRef = useRef<boolean | null>(null);
|
|
const pendingAuthStateReloadRef = useRef(false);
|
|
const canKeepPlatformContentMounted =
|
|
hasRenderedPlatformContentRef.current &&
|
|
(status === 'checking' || status === 'recovering');
|
|
const readyUser =
|
|
status === 'ready' || canKeepPlatformContentMounted ? user : null;
|
|
const settings = useGameSettings(readyUser?.id ?? null);
|
|
const platformThemeClass = `platform-theme--${settings.platformTheme}`;
|
|
|
|
if (status === 'ready' || status === 'unauthenticated') {
|
|
hasRenderedPlatformContentRef.current = true;
|
|
}
|
|
|
|
const markAuthStateReloadIfChanged = useCallback(
|
|
(
|
|
nextUser: AuthUser | null,
|
|
options: { reloadOnChange?: boolean } = {},
|
|
) => {
|
|
const nextHasUser = Boolean(nextUser);
|
|
const previousHasUser = lastStableAuthPresenceRef.current;
|
|
if (previousHasUser === null) {
|
|
lastStableAuthPresenceRef.current = nextHasUser;
|
|
return;
|
|
}
|
|
|
|
lastStableAuthPresenceRef.current = nextHasUser;
|
|
if (
|
|
previousHasUser !== nextHasUser &&
|
|
options.reloadOnChange !== false
|
|
) {
|
|
pendingAuthStateReloadRef.current = true;
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
const activateReadyUser = useCallback((nextUser: AuthUser) => {
|
|
// 受保护业务 hook 只在 readyUser 暴露后启动,必须先保证请求层能带 Bearer token。
|
|
authHydrateVersionRef.current += 1;
|
|
markAuthStateReloadIfChanged(nextUser);
|
|
setUser(nextUser);
|
|
setStatus('ready');
|
|
}, [markAuthStateReloadIfChanged]);
|
|
|
|
const clearLocalAuthenticatedState = useCallback(
|
|
(options: { reloadOnChange?: boolean } = {}) => {
|
|
// 退出动作必须先收回前端鉴权上下文,再等待后端吊销完成。
|
|
// 否则平台壳层会在无刷新状态下继续暴露旧用户的私有作品缓存。
|
|
authHydrateVersionRef.current += 1;
|
|
markAuthStateReloadIfChanged(null, options);
|
|
pendingProtectedActionRef.current = null;
|
|
setUser(null);
|
|
setStatus('unauthenticated');
|
|
setShowLoginModal(false);
|
|
setShowRegistrationInviteModal(false);
|
|
setShowSettingsModal(false);
|
|
setSettingsEntryMode('settings');
|
|
setInitialSettingsSection(null);
|
|
setSessions([]);
|
|
setRevokingSessionIds([]);
|
|
setAuditLogs([]);
|
|
setRiskBlocks([]);
|
|
setLoginCaptchaChallenge(null);
|
|
setBindCaptchaChallenge(null);
|
|
setChangePhoneCaptchaChallenge(null);
|
|
setPendingInviteCode('');
|
|
setRegistrationInviteError('');
|
|
setError('');
|
|
},
|
|
[markAuthStateReloadIfChanged],
|
|
);
|
|
|
|
const restoreAuthSession = useCallback(async () => {
|
|
const hadLocalAccessToken = Boolean(getStoredAccessToken());
|
|
|
|
if (hadLocalAccessToken) {
|
|
try {
|
|
const session = await getCurrentAuthUser();
|
|
if (session.user) {
|
|
const confirmedUser = session.user;
|
|
// 中文注释:已有 access token 能确认当前账号时,refresh 只作为续期和每日登录埋点补强。
|
|
// refresh cookie 临时失效或代理抖动不能反向抹掉这次已确认的登录态。
|
|
void refreshStoredAccessToken({ clearOnFailure: false }).catch(
|
|
() => undefined,
|
|
);
|
|
return {
|
|
kind: 'authenticated',
|
|
session: {
|
|
...session,
|
|
user: confirmedUser,
|
|
},
|
|
} satisfies AuthHydrateSessionResult;
|
|
}
|
|
|
|
return {
|
|
kind: 'guest',
|
|
session,
|
|
} satisfies AuthHydrateSessionResult;
|
|
} catch {
|
|
// 本地 token 可能已过期或被吊销,再尝试通过 refresh cookie 补票。
|
|
}
|
|
}
|
|
|
|
await refreshStoredAccessToken({ clearOnFailure: true });
|
|
const session = await getCurrentAuthUser();
|
|
if (session.user) {
|
|
const confirmedUser = session.user;
|
|
return {
|
|
kind: 'authenticated',
|
|
session: {
|
|
...session,
|
|
user: confirmedUser,
|
|
},
|
|
} satisfies AuthHydrateSessionResult;
|
|
}
|
|
|
|
return {
|
|
kind: 'guest',
|
|
session,
|
|
} satisfies AuthHydrateSessionResult;
|
|
}, []);
|
|
|
|
const logoutCurrentSession = useCallback(async () => {
|
|
clearLocalAuthenticatedState({ reloadOnChange: false });
|
|
try {
|
|
await logoutAuthUser();
|
|
} catch (logoutError) {
|
|
setError(
|
|
logoutError instanceof Error
|
|
? logoutError.message
|
|
: '退出登录失败,请刷新页面确认状态。',
|
|
);
|
|
} finally {
|
|
reloadCurrentPageForAuthStateChange();
|
|
}
|
|
}, [clearLocalAuthenticatedState]);
|
|
|
|
const logoutAllSessions = useCallback(async () => {
|
|
clearLocalAuthenticatedState({ reloadOnChange: false });
|
|
try {
|
|
await logoutAllAuthSessions();
|
|
} catch (logoutError) {
|
|
setError(
|
|
logoutError instanceof Error
|
|
? logoutError.message
|
|
: '退出全部设备失败,请刷新页面确认状态。',
|
|
);
|
|
} finally {
|
|
reloadCurrentPageForAuthStateChange();
|
|
}
|
|
}, [clearLocalAuthenticatedState]);
|
|
|
|
const closeLoginModal = useCallback(() => {
|
|
pendingProtectedActionRef.current = null;
|
|
setShowLoginModal(false);
|
|
setLoginCaptchaChallenge(null);
|
|
setError('');
|
|
}, []);
|
|
|
|
const closeRegistrationInviteModal = useCallback(() => {
|
|
setShowRegistrationInviteModal(false);
|
|
setRegistrationInviteError('');
|
|
setPendingInviteCode('');
|
|
}, []);
|
|
|
|
const closeSettingsModal = useCallback(() => {
|
|
setShowSettingsModal(false);
|
|
setSettingsEntryMode('settings');
|
|
setInitialSettingsSection(null);
|
|
}, []);
|
|
|
|
const requestMiniProgramLogin = useCallback(() => {
|
|
setWechatLoading(true);
|
|
setError('');
|
|
void requestHostLogin()
|
|
.catch((miniProgramError) => {
|
|
setError(
|
|
miniProgramError instanceof Error
|
|
? miniProgramError.message
|
|
: '请在微信小程序内完成登录。',
|
|
);
|
|
})
|
|
.finally(() => {
|
|
setWechatLoading(false);
|
|
});
|
|
}, []);
|
|
|
|
const openLoginModal = useCallback(
|
|
(postLoginAction?: (() => void) | null) => {
|
|
if (readyUser) {
|
|
postLoginAction?.();
|
|
return;
|
|
}
|
|
|
|
pendingProtectedActionRef.current = postLoginAction ?? null;
|
|
if (getHostRuntime().kind === 'wechat_mini_program') {
|
|
setShowLoginModal(false);
|
|
requestMiniProgramLogin();
|
|
return;
|
|
}
|
|
|
|
setShowLoginModal(true);
|
|
},
|
|
[readyUser, requestMiniProgramLogin],
|
|
);
|
|
|
|
const requireAuth = useCallback(
|
|
(action: () => void) => {
|
|
openLoginModal(action);
|
|
},
|
|
[openLoginModal],
|
|
);
|
|
|
|
const openSettingsModal = useCallback(
|
|
(section?: PlatformSettingsSection) => {
|
|
if (readyUser) {
|
|
setSettingsEntryMode('settings');
|
|
setInitialSettingsSection(section ?? null);
|
|
setShowSettingsModal(true);
|
|
return;
|
|
}
|
|
|
|
openLoginModal();
|
|
},
|
|
[openLoginModal, readyUser],
|
|
);
|
|
|
|
const openAccountModal = useCallback(() => {
|
|
if (readyUser) {
|
|
setSettingsEntryMode('account');
|
|
setInitialSettingsSection('account');
|
|
setShowSettingsModal(true);
|
|
return;
|
|
}
|
|
|
|
openLoginModal();
|
|
}, [openLoginModal, readyUser]);
|
|
|
|
useEffect(() => {
|
|
if (status !== 'unauthenticated' || readyUser || showLoginModal) {
|
|
return;
|
|
}
|
|
const inviteCode = readInviteCodeFromLocation();
|
|
if (!inviteCode) {
|
|
return;
|
|
}
|
|
if (autoOpenedInviteCodeRef.current === inviteCode) {
|
|
return;
|
|
}
|
|
autoOpenedInviteCodeRef.current = inviteCode;
|
|
setPendingInviteCode(inviteCode);
|
|
}, [readyUser, showLoginModal, status]);
|
|
|
|
useEffect(() => {
|
|
let isActive = true;
|
|
|
|
const hydrate = async (hydrateToken: number) => {
|
|
const isCurrentHydrate = () =>
|
|
isActive && hydrateToken === authHydrateVersionRef.current;
|
|
const callbackResult = consumeAuthCallbackResult();
|
|
const loadLoginOptions = async () => {
|
|
const options = await getAuthLoginOptions();
|
|
if (!isCurrentHydrate()) {
|
|
return null;
|
|
}
|
|
|
|
setAvailableLoginMethods(
|
|
normalizeAvailableLoginMethods(options.availableLoginMethods),
|
|
);
|
|
return options;
|
|
};
|
|
|
|
const resolveGuestFallback = async () => {
|
|
try {
|
|
await loadLoginOptions();
|
|
if (!isCurrentHydrate()) {
|
|
return;
|
|
}
|
|
|
|
markAuthStateReloadIfChanged(null);
|
|
setUser(null);
|
|
setStatus('unauthenticated');
|
|
} catch (optionsError) {
|
|
if (!isCurrentHydrate()) {
|
|
return;
|
|
}
|
|
|
|
setAvailableLoginMethods(REQUIRED_LOGIN_METHODS);
|
|
markAuthStateReloadIfChanged(null);
|
|
setUser(null);
|
|
// 中文注释:登录方式接口失败时按产品约定保留验证码和密码登录入口;
|
|
// 这里不展示接口读取错误,避免用户误以为登录本身不可用。
|
|
setError(callbackResult?.error ?? '');
|
|
setStatus('unauthenticated');
|
|
}
|
|
};
|
|
|
|
if (callbackResult?.error && isCurrentHydrate()) {
|
|
setError(callbackResult.error);
|
|
setShowLoginModal(true);
|
|
}
|
|
|
|
try {
|
|
const restoredSession = await restoreAuthSession();
|
|
if (!isCurrentHydrate()) {
|
|
return;
|
|
}
|
|
if (restoredSession.kind === 'guest') {
|
|
markAuthStateReloadIfChanged(null);
|
|
setAvailableLoginMethods(
|
|
normalizeAvailableLoginMethods(
|
|
restoredSession.session?.availableLoginMethods,
|
|
),
|
|
);
|
|
await resolveGuestFallback();
|
|
return;
|
|
}
|
|
|
|
const nextSession = restoredSession.session;
|
|
markAuthStateReloadIfChanged(nextSession.user);
|
|
setUser(nextSession.user);
|
|
setAvailableLoginMethods(
|
|
normalizeAvailableLoginMethods(nextSession.availableLoginMethods),
|
|
);
|
|
setStatus(
|
|
nextSession.user.bindingStatus === 'pending_bind_phone'
|
|
? 'pending_bind_phone'
|
|
: 'ready',
|
|
);
|
|
setError(callbackResult?.error ?? '');
|
|
} catch {
|
|
if (!isCurrentHydrate()) {
|
|
return;
|
|
}
|
|
|
|
await resolveGuestFallback();
|
|
}
|
|
};
|
|
|
|
void hydrate(++authHydrateVersionRef.current);
|
|
|
|
const handleAuthStateChange = () => {
|
|
setStatus('checking');
|
|
void hydrate(++authHydrateVersionRef.current);
|
|
};
|
|
|
|
const handleAuthHashChange = () => {
|
|
const callbackResult = consumeAuthCallbackResult();
|
|
if (!callbackResult) {
|
|
return;
|
|
}
|
|
if (callbackResult.error) {
|
|
setError(callbackResult.error);
|
|
return;
|
|
}
|
|
setStatus('checking');
|
|
void hydrate(++authHydrateVersionRef.current);
|
|
};
|
|
|
|
window.addEventListener(AUTH_STATE_EVENT, handleAuthStateChange);
|
|
window.addEventListener('hashchange', handleAuthHashChange);
|
|
|
|
return () => {
|
|
isActive = false;
|
|
window.removeEventListener(AUTH_STATE_EVENT, handleAuthStateChange);
|
|
window.removeEventListener('hashchange', handleAuthHashChange);
|
|
};
|
|
}, [markAuthStateReloadIfChanged, restoreAuthSession]);
|
|
|
|
useEffect(() => {
|
|
if (!readyUser) {
|
|
setShowSettingsModal(false);
|
|
} else {
|
|
setShowLoginModal(false);
|
|
|
|
const pendingAction = pendingProtectedActionRef.current;
|
|
pendingProtectedActionRef.current = null;
|
|
pendingAction?.();
|
|
}
|
|
|
|
if (pendingAuthStateReloadRef.current) {
|
|
pendingAuthStateReloadRef.current = false;
|
|
reloadCurrentPageForAuthStateChange();
|
|
}
|
|
}, [readyUser]);
|
|
|
|
useEffect(() => {
|
|
if (!showSettingsModal || status !== 'ready') {
|
|
return;
|
|
}
|
|
|
|
let isActive = true;
|
|
setLoadingRiskBlocks(true);
|
|
setLoadingSessions(true);
|
|
setLoadingAuditLogs(true);
|
|
void getAuthRiskBlocks()
|
|
.then((nextBlocks) => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setRiskBlocks(nextBlocks);
|
|
})
|
|
.catch((blockError) => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setError(
|
|
blockError instanceof Error
|
|
? blockError.message
|
|
: '读取安全状态失败,请稍后再试。',
|
|
);
|
|
})
|
|
.finally(() => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setLoadingRiskBlocks(false);
|
|
});
|
|
void getAuthSessions()
|
|
.then((nextSessions) => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setSessions(nextSessions);
|
|
})
|
|
.catch((sessionError) => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setError(
|
|
sessionError instanceof Error
|
|
? sessionError.message
|
|
: '读取登录设备失败,请稍后再试。',
|
|
);
|
|
})
|
|
.finally(() => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setLoadingSessions(false);
|
|
});
|
|
|
|
void getAuthAuditLogs()
|
|
.then((nextLogs) => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setAuditLogs(nextLogs);
|
|
})
|
|
.catch((auditError) => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setError(
|
|
auditError instanceof Error
|
|
? auditError.message
|
|
: '读取账号操作记录失败,请稍后再试。',
|
|
);
|
|
})
|
|
.finally(() => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
setLoadingAuditLogs(false);
|
|
});
|
|
|
|
return () => {
|
|
isActive = false;
|
|
};
|
|
}, [showSettingsModal, status]);
|
|
|
|
const authUiValue = useMemo(
|
|
() => ({
|
|
user: readyUser,
|
|
// 平台内容在 checking/recovering 阶段可以继续挂载,避免闪烁;
|
|
// 但受保护请求只能在真实 ready 且存在用户时再启动。
|
|
canAccessProtectedData: status === 'ready' && Boolean(readyUser),
|
|
openLoginModal,
|
|
requireAuth,
|
|
openSettingsModal,
|
|
openAccountModal,
|
|
setCurrentUser: setUser,
|
|
logout: logoutCurrentSession,
|
|
musicVolume: settings.musicVolume,
|
|
setMusicVolume: settings.setMusicVolume,
|
|
platformTheme: settings.platformTheme,
|
|
setPlatformTheme: settings.setPlatformTheme,
|
|
isHydratingSettings: settings.isHydratingSettings,
|
|
isPersistingSettings: settings.isPersistingSettings,
|
|
settingsError: settings.settingsError,
|
|
}),
|
|
[
|
|
openAccountModal,
|
|
openLoginModal,
|
|
openSettingsModal,
|
|
readyUser,
|
|
requireAuth,
|
|
logoutCurrentSession,
|
|
status,
|
|
settings.isHydratingSettings,
|
|
settings.isPersistingSettings,
|
|
settings.musicVolume,
|
|
settings.platformTheme,
|
|
settings.setMusicVolume,
|
|
settings.setPlatformTheme,
|
|
settings.settingsError,
|
|
],
|
|
);
|
|
|
|
if (status === 'checking' && !canKeepPlatformContentMounted) {
|
|
return (
|
|
<div
|
|
className={`platform-theme ${platformThemeClass} flex min-h-screen items-center justify-center bg-[var(--platform-body-fill)] text-sm text-[var(--platform-text-base)]`}
|
|
>
|
|
正在校验登录状态...
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (status === 'recovering' && !canKeepPlatformContentMounted) {
|
|
return (
|
|
<div
|
|
className={`platform-theme ${platformThemeClass} flex min-h-screen items-center justify-center bg-[var(--platform-body-fill)] text-sm text-[var(--platform-text-base)]`}
|
|
>
|
|
正在恢复登录状态...
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (status === 'pending_bind_phone' && user) {
|
|
return (
|
|
<BindPhoneScreen
|
|
user={user}
|
|
platformTheme={settings.platformTheme}
|
|
sendingCode={sendingCode}
|
|
binding={bindingPhone}
|
|
error={error}
|
|
captchaChallenge={bindCaptchaChallenge}
|
|
onSendCode={async (phone, captcha) => {
|
|
setSendingCode(true);
|
|
setError('');
|
|
try {
|
|
const result = await sendPhoneLoginCode(
|
|
phone,
|
|
'bind_phone',
|
|
captcha,
|
|
);
|
|
setBindCaptchaChallenge(null);
|
|
return result;
|
|
} catch (sendError) {
|
|
const captchaChallenge = getCaptchaChallengeFromError(sendError);
|
|
if (captchaChallenge) {
|
|
setBindCaptchaChallenge(captchaChallenge);
|
|
}
|
|
setError(
|
|
sendError instanceof Error
|
|
? sendError.message
|
|
: '发送验证码失败,请稍后再试。',
|
|
);
|
|
throw sendError;
|
|
} finally {
|
|
setSendingCode(false);
|
|
}
|
|
}}
|
|
onSubmit={async (phone, code) => {
|
|
setBindingPhone(true);
|
|
setError('');
|
|
try {
|
|
const nextUser = await bindWechatPhone(phone, code);
|
|
setBindCaptchaChallenge(null);
|
|
activateReadyUser(nextUser);
|
|
} catch (bindError) {
|
|
setError(
|
|
bindError instanceof Error
|
|
? bindError.message
|
|
: '绑定手机号失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setBindingPhone(false);
|
|
}
|
|
}}
|
|
onLogout={async () => {
|
|
await logoutCurrentSession();
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (
|
|
status !== 'ready' &&
|
|
status !== 'unauthenticated' &&
|
|
!canKeepPlatformContentMounted
|
|
) {
|
|
return (
|
|
<div
|
|
className={`platform-theme ${platformThemeClass} flex min-h-screen items-center justify-center bg-[var(--platform-body-fill)] px-6 text-[var(--platform-text-base)]`}
|
|
>
|
|
<div className="platform-auth-card max-w-md rounded-3xl px-6 py-7 text-center">
|
|
<div className="text-base font-medium text-[var(--platform-text-strong)]">
|
|
登录状态异常
|
|
</div>
|
|
<div className="mt-3 text-sm leading-6 text-[var(--platform-text-base)]">
|
|
{error || '账号恢复失败,请刷新页面后重试。'}
|
|
</div>
|
|
<PlatformActionButton
|
|
className="mt-5"
|
|
onClick={() => {
|
|
reloadCurrentPageForAuthStateChange();
|
|
}}
|
|
>
|
|
重新尝试
|
|
</PlatformActionButton>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AuthUiContext.Provider value={authUiValue}>
|
|
<div className="relative">
|
|
<div className={`platform-theme ${platformThemeClass}`}>
|
|
{readyUser ? (
|
|
<AccountModal
|
|
user={readyUser}
|
|
isOpen={showSettingsModal}
|
|
entryMode={settingsEntryMode}
|
|
initialSection={initialSettingsSection}
|
|
platformTheme={settings.platformTheme}
|
|
riskBlocks={riskBlocks}
|
|
sessions={sessions}
|
|
auditLogs={auditLogs}
|
|
loadingRiskBlocks={loadingRiskBlocks}
|
|
loadingSessions={loadingSessions}
|
|
loadingAuditLogs={loadingAuditLogs}
|
|
revokingSessionIds={revokingSessionIds}
|
|
isHydratingSettings={settings.isHydratingSettings}
|
|
isPersistingSettings={settings.isPersistingSettings}
|
|
settingsError={settings.settingsError}
|
|
onClose={closeSettingsModal}
|
|
onPlatformThemeChange={settings.setPlatformTheme}
|
|
onLogout={logoutCurrentSession}
|
|
onRefreshRiskBlocks={async () => {
|
|
setLoadingRiskBlocks(true);
|
|
try {
|
|
setRiskBlocks(await getAuthRiskBlocks());
|
|
} catch (blockError) {
|
|
setError(
|
|
blockError instanceof Error
|
|
? blockError.message
|
|
: '读取安全状态失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setLoadingRiskBlocks(false);
|
|
}
|
|
}}
|
|
onLiftRiskBlock={async (scopeType) => {
|
|
try {
|
|
await liftAuthRiskBlock(scopeType);
|
|
setRiskBlocks(await getAuthRiskBlocks());
|
|
setAuditLogs(await getAuthAuditLogs());
|
|
} catch (liftError) {
|
|
setError(
|
|
liftError instanceof Error
|
|
? liftError.message
|
|
: '解除保护失败,请稍后再试。',
|
|
);
|
|
}
|
|
}}
|
|
onRefreshSessions={async () => {
|
|
setLoadingSessions(true);
|
|
try {
|
|
setSessions(await getAuthSessions());
|
|
} catch (sessionError) {
|
|
setError(
|
|
sessionError instanceof Error
|
|
? sessionError.message
|
|
: '读取登录设备失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setLoadingSessions(false);
|
|
}
|
|
}}
|
|
onRefreshAuditLogs={async () => {
|
|
setLoadingAuditLogs(true);
|
|
try {
|
|
setAuditLogs(await getAuthAuditLogs());
|
|
} catch (auditError) {
|
|
setError(
|
|
auditError instanceof Error
|
|
? auditError.message
|
|
: '读取账号操作记录失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setLoadingAuditLogs(false);
|
|
}
|
|
}}
|
|
onRevokeSession={async (session) => {
|
|
const sessionIds =
|
|
session.sessionIds.length > 0
|
|
? session.sessionIds
|
|
: [session.sessionId];
|
|
setRevokingSessionIds((current) =>
|
|
Array.from(new Set([...current, session.sessionId])),
|
|
);
|
|
try {
|
|
await revokeAuthSessions(sessionIds);
|
|
setSessions(await getAuthSessions());
|
|
setAuditLogs(await getAuthAuditLogs());
|
|
} catch (revokeError) {
|
|
setError(
|
|
revokeError instanceof Error
|
|
? revokeError.message
|
|
: '移除登录设备失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setRevokingSessionIds((current) =>
|
|
current.filter((id) => id !== session.sessionId),
|
|
);
|
|
}
|
|
}}
|
|
onLogoutAll={logoutAllSessions}
|
|
changePhoneCaptchaChallenge={changePhoneCaptchaChallenge}
|
|
onSendChangePhoneCode={async (phone, captcha) => {
|
|
try {
|
|
const result = await sendPhoneLoginCode(
|
|
phone,
|
|
'change_phone',
|
|
captcha,
|
|
);
|
|
setChangePhoneCaptchaChallenge(null);
|
|
return result;
|
|
} catch (sendError) {
|
|
const captchaChallenge =
|
|
getCaptchaChallengeFromError(sendError);
|
|
if (captchaChallenge) {
|
|
setChangePhoneCaptchaChallenge(captchaChallenge);
|
|
}
|
|
throw sendError;
|
|
}
|
|
}}
|
|
onChangePhone={async (phone, code) => {
|
|
const nextUser = await changePhoneNumber(phone, code);
|
|
setChangePhoneCaptchaChallenge(null);
|
|
setUser(nextUser);
|
|
}}
|
|
onChangePassword={async (currentPassword, newPassword) => {
|
|
await changePassword(currentPassword, newPassword);
|
|
clearLocalAuthenticatedState();
|
|
}}
|
|
/>
|
|
) : null}
|
|
<LoginScreen
|
|
isOpen={showLoginModal}
|
|
platformTheme={settings.platformTheme}
|
|
availableLoginMethods={availableLoginMethods}
|
|
sendingCode={sendingCode}
|
|
loggingIn={loggingIn}
|
|
wechatLoading={wechatLoading}
|
|
error={error}
|
|
captchaChallenge={loginCaptchaChallenge}
|
|
onClose={closeLoginModal}
|
|
onSendCode={async (phone, scene, captcha) => {
|
|
setSendingCode(true);
|
|
setError('');
|
|
try {
|
|
const result = await sendPhoneLoginCode(phone, scene, captcha);
|
|
setLoginCaptchaChallenge(null);
|
|
return result;
|
|
} catch (sendError) {
|
|
const captchaChallenge =
|
|
getCaptchaChallengeFromError(sendError);
|
|
if (captchaChallenge) {
|
|
setLoginCaptchaChallenge(captchaChallenge);
|
|
}
|
|
setError(
|
|
sendError instanceof Error
|
|
? sendError.message
|
|
: '发送验证码失败,请稍后再试。',
|
|
);
|
|
throw sendError;
|
|
} finally {
|
|
setSendingCode(false);
|
|
}
|
|
}}
|
|
onPhoneSubmit={async (phone, code) => {
|
|
setLoggingIn(true);
|
|
setError('');
|
|
try {
|
|
const registrationInviteCode =
|
|
pendingInviteCode || readInviteCodeFromLocation();
|
|
const response = registrationInviteCode
|
|
? await loginWithPhoneCode(phone, code, registrationInviteCode)
|
|
: await loginWithPhoneCode(phone, code);
|
|
const autoRedeemedInvite = response.referral?.ok === true;
|
|
setStoredLastLoginPhone(phone);
|
|
setLoginCaptchaChallenge(null);
|
|
setShowRegistrationInviteModal(
|
|
response.created && !autoRedeemedInvite,
|
|
);
|
|
setRegistrationInviteError(
|
|
response.referral?.ok === false
|
|
? (response.referral.message ??
|
|
'邀请码绑定失败,请确认后重试。')
|
|
: '',
|
|
);
|
|
if (autoRedeemedInvite) {
|
|
setPendingInviteCode('');
|
|
}
|
|
activateReadyUser(response.user);
|
|
} catch (loginError) {
|
|
setError(
|
|
loginError instanceof Error
|
|
? loginError.message
|
|
: '登录失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setLoggingIn(false);
|
|
}
|
|
}}
|
|
onPasswordSubmit={async (phone, password) => {
|
|
setLoggingIn(true);
|
|
setError('');
|
|
try {
|
|
const nextUser = await authEntry(phone, password);
|
|
setStoredLastLoginPhone(phone);
|
|
activateReadyUser(nextUser);
|
|
} catch (loginError) {
|
|
setError(
|
|
loginError instanceof Error
|
|
? loginError.message
|
|
: '登录失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setLoggingIn(false);
|
|
}
|
|
}}
|
|
onResetPassword={async (phone, code, newPassword) => {
|
|
setLoggingIn(true);
|
|
setError('');
|
|
try {
|
|
const nextUser = await resetPassword(phone, code, newPassword);
|
|
setStoredLastLoginPhone(phone);
|
|
activateReadyUser(nextUser);
|
|
} catch (resetError) {
|
|
setError(
|
|
resetError instanceof Error
|
|
? resetError.message
|
|
: '重置密码失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setLoggingIn(false);
|
|
}
|
|
}}
|
|
onStartWechatLogin={async () => {
|
|
setWechatLoading(true);
|
|
setError('');
|
|
try {
|
|
await startWechatLogin();
|
|
} catch (wechatError) {
|
|
setError(
|
|
wechatError instanceof Error
|
|
? wechatError.message
|
|
: '微信登录暂不可用,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setWechatLoading(false);
|
|
}
|
|
}}
|
|
/>
|
|
<RegistrationInviteModal
|
|
isOpen={showRegistrationInviteModal}
|
|
platformTheme={settings.platformTheme}
|
|
initialInviteCode={pendingInviteCode}
|
|
submitting={submittingRegistrationInvite}
|
|
error={registrationInviteError}
|
|
onClose={closeRegistrationInviteModal}
|
|
onSubmit={async (inviteCode) => {
|
|
setSubmittingRegistrationInvite(true);
|
|
setRegistrationInviteError('');
|
|
try {
|
|
await redeemRegistrationInviteCode(inviteCode);
|
|
closeRegistrationInviteModal();
|
|
} catch (inviteError) {
|
|
setRegistrationInviteError(
|
|
inviteError instanceof Error
|
|
? inviteError.message
|
|
: '填写邀请码失败,请稍后再试。',
|
|
);
|
|
} finally {
|
|
setSubmittingRegistrationInvite(false);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
{children}
|
|
</div>
|
|
</AuthUiContext.Provider>
|
|
);
|
|
}
|