After sign-out or data purge, clear session state reliably, remount the splash overlay above navigation, and navigate to login instead of tabs so users no longer briefly land on the chat home screen. Co-authored-by: Cursor <cursoragent@cursor.com>
73 lines
2.3 KiB
TypeScript
73 lines
2.3 KiB
TypeScript
import React, { type PropsWithChildren } from 'react';
|
|
|
|
import { initApiClient } from '@/core/api/client';
|
|
import { AppSettingsProvider } from '@/core/app-settings-context';
|
|
import { MemoirReadingSettingsProvider } from '@/core/memoir-reading-settings-context';
|
|
import { NetworkError } from '@/core/api/types';
|
|
import { tokenManager } from '@/core/auth/token-manager';
|
|
import { config } from '@/core/config';
|
|
import { authKeys } from '@/features/auth/auth-query-keys';
|
|
import { AppQueryProvider, queryClient } from '@/core/query';
|
|
|
|
/**
|
|
* Returns false only when the refresh token is genuinely rejected
|
|
* (no token stored, or server returned non-2xx).
|
|
* Throws NetworkError on transport-level failures so the caller
|
|
* can distinguish "session dead" from "network down".
|
|
*/
|
|
async function refreshTokens(): Promise<boolean> {
|
|
const refreshToken = await tokenManager.getRefreshToken();
|
|
if (!refreshToken) return false;
|
|
|
|
let res: Response;
|
|
try {
|
|
res = await fetch(`${config.apiBaseUrl}${config.api.refreshPath}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
});
|
|
} catch (err) {
|
|
throw new NetworkError(
|
|
err instanceof Error ? err.message : 'Token refresh network failure',
|
|
err,
|
|
);
|
|
}
|
|
|
|
if (!res.ok) return false;
|
|
|
|
const data = (await res.json()) as {
|
|
access_token: string;
|
|
refresh_token: string;
|
|
};
|
|
await tokenManager.setTokens(data.access_token, data.refresh_token);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Called by the API client when token refresh is explicitly rejected.
|
|
* Must synchronously flip query caches so useSession() immediately
|
|
* sees unauthenticated — tokenManager.clearTokens() is fire-and-forget
|
|
* since the cache flip is the authoritative signal.
|
|
*/
|
|
function onAuthFailure() {
|
|
queryClient.setQueryData(authKeys.tokenCheck, false);
|
|
queryClient.setQueryData(authKeys.session, null);
|
|
tokenManager.clearTokens();
|
|
}
|
|
|
|
initApiClient({
|
|
getAccessToken: tokenManager.getAccessToken,
|
|
refreshTokens,
|
|
onAuthFailure,
|
|
});
|
|
|
|
export function AppProviders({ children }: PropsWithChildren) {
|
|
return (
|
|
<AppQueryProvider>
|
|
<AppSettingsProvider>
|
|
<MemoirReadingSettingsProvider>{children}</MemoirReadingSettingsProvider>
|
|
</AppSettingsProvider>
|
|
</AppQueryProvider>
|
|
);
|
|
}
|