121 lines
3.0 KiB
TypeScript
121 lines
3.0 KiB
TypeScript
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
type ReactNode,
|
|
} from 'react';
|
|
import {
|
|
bootstrapSession,
|
|
clearAuth,
|
|
ensureSession,
|
|
getDeviceKey,
|
|
request,
|
|
saveSession,
|
|
type SessionPayload,
|
|
type UserProfile,
|
|
} from '../lib/api';
|
|
import { touchPromoIfNeeded } from '../lib/promo';
|
|
|
|
type UserSessionContextValue = {
|
|
ready: boolean;
|
|
profile: UserProfile | null;
|
|
phoneVerified: boolean;
|
|
applySession: (session: SessionPayload) => void;
|
|
refreshProfile: () => Promise<void>;
|
|
resetSession: () => Promise<void>;
|
|
};
|
|
|
|
const UserSessionContext = createContext<UserSessionContextValue | null>(null);
|
|
|
|
export function UserSessionProvider({ children }: { children: ReactNode }) {
|
|
const [ready, setReady] = useState(false);
|
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
|
const [phoneVerified, setPhoneVerified] = useState(false);
|
|
|
|
const applySession = useCallback((session: SessionPayload) => {
|
|
saveSession(session);
|
|
if (session.user) setProfile(session.user);
|
|
setPhoneVerified(!!session.phoneVerified || !!session.user?.phoneVerified);
|
|
}, []);
|
|
|
|
const refreshProfile = useCallback(async () => {
|
|
const me = await request<UserProfile>('USER_H5', '/auth/me');
|
|
setProfile(me);
|
|
setPhoneVerified(!!me.phoneVerified);
|
|
}, []);
|
|
|
|
const resetSession = useCallback(async () => {
|
|
clearAuth();
|
|
const session = await bootstrapSession();
|
|
applySession(session);
|
|
}, [applySession]);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
try {
|
|
const session = await ensureSession();
|
|
if (cancelled) return;
|
|
applySession(session);
|
|
if (!session.user) {
|
|
await refreshProfile();
|
|
}
|
|
await touchPromoIfNeeded();
|
|
} catch {
|
|
if (!cancelled) {
|
|
try {
|
|
const session = await bootstrapSession();
|
|
applySession(session);
|
|
await refreshProfile();
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
} finally {
|
|
if (!cancelled) setReady(true);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [applySession, refreshProfile]);
|
|
|
|
const value = useMemo(
|
|
() => ({
|
|
ready,
|
|
profile,
|
|
phoneVerified,
|
|
applySession,
|
|
refreshProfile,
|
|
resetSession,
|
|
}),
|
|
[ready, profile, phoneVerified, applySession, refreshProfile, resetSession],
|
|
);
|
|
|
|
if (!ready) {
|
|
return (
|
|
<div className="session-boot">
|
|
<p className="session-boot-text">加载中...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <UserSessionContext.Provider value={value}>{children}</UserSessionContext.Provider>;
|
|
}
|
|
|
|
export function useUserSession() {
|
|
const ctx = useContext(UserSessionContext);
|
|
if (!ctx) throw new Error('useUserSession must be used within UserSessionProvider');
|
|
return ctx;
|
|
}
|
|
|
|
export function useOptionalUserSession() {
|
|
return useContext(UserSessionContext);
|
|
}
|
|
|
|
/** @deprecated use profile from useUserSession */
|
|
export { getDeviceKey };
|