diff --git a/docs/APP_REFACTORING_DESIGN.md b/docs/APP_REFACTORING_DESIGN.md index d2ce23e..89406b9 100644 --- a/docs/APP_REFACTORING_DESIGN.md +++ b/docs/APP_REFACTORING_DESIGN.md @@ -103,7 +103,7 @@ We will gradually eliminate React state variables in `src/app.tsx` and derive th * [x] **Step 4.1:** Create `src/hooks/use_google_auth.ts` to manage Google Drive authentication token updates. Remove `hasGoogleToken` state from `src/app.tsx`. * [x] **Step 4.2:** Create `src/pages/view_page.tsx` containing layout rendering (`renderMainArea`), asynchronous data loaders, view state management, `revokeObjectUrls` cleanups, and `` error fallback state and rendering. * [x] **Step 4.3:** Create `src/pages/intro_page.tsx` rendering the landing screen and its own local header. -* [ ] **Step 4.4:** Refactor `src/app.tsx` to serve as a pure routing switch routing to `` or ``. In `App`, handle Google Drive redirection synchronously during the render pass by returning a `` element (preventing render flashing), and ensure the redirection parser checks both the router `location.search` and the external `window.location.search` to handle HashRouter query structure robustly. Once external parameters (like `state` or `authcode`) are detected, they must be stripped from `window.location.search` using `window.history.replaceState` to prevent redirection loops on page refresh. Remove all layout state logic from the root file, including moving the `useWebMcpBridge` hook invocation into ``. Run final verification scripts (`npm run check:all`). +* [x] **Step 4.4:** Refactor `src/app.tsx` to serve as a pure routing switch routing to `` or ``. In `App`, handle Google Drive redirection synchronously during the render pass by returning a `` element (preventing render flashing), and ensure the redirection parser checks both the router `location.search` and the external `window.location.search` to handle HashRouter query structure robustly. Once external parameters (like `state` or `authcode`) are detected, they must be stripped from `window.location.search` using `window.history.replaceState` to prevent redirection loops on page refresh. Remove all layout state logic from the root file, including moving the `useWebMcpBridge` hook invocation into ``. Run final verification scripts (`npm run check:all`). ## Future Considerations diff --git a/src/app.tsx b/src/app.tsx index 47edb4d..fcebd74 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,6 +1,6 @@ import queryString from 'query-string'; -import {useEffect, useRef} from 'react'; -import {Navigate, Route, Routes, useLocation, useNavigate} from 'react-router'; +import {useMemo} from 'react'; +import {Navigate, Route, Routes, useLocation} from 'react-router'; import {IntroPage} from './pages/intro_page'; import {ViewPage} from './pages/view_page'; import {getStaticUrl} from './util/url_args'; @@ -9,20 +9,27 @@ const staticUrl = getStaticUrl(); /** * Root App component that orchestrates top-level routing, Google Drive "Open with" - * payload interception and redirection. + * payload interception, and WikiTree auth code redirection. */ export function App() { - const navigate = useNavigate(); const location = useLocation(); - /** Prevents the Google Drive "Open with" state from being processed more than once. */ - const stateProcessed = useRef(false); + // Synchronously parse and evaluate redirect query parameters from both external window and router searches. + const redirectTarget = useMemo(() => { + if (typeof window === 'undefined') { + return null; + } - // Google Drive "Open with" flow state checking. - useEffect(() => { - const search = queryString.parse(location.search); - const stateParam = search.state; - if (typeof stateParam === 'string' && !stateProcessed.current) { + const windowSearch = queryString.parse(window.location.search); + const routerSearch = queryString.parse(location.search); + + let redirectPath: string | null = null; + const mergedParams = {...routerSearch, ...windowSearch}; + let paramsModified = false; + + // 1. Handle Google Drive "Open with" action + const stateParam = mergedParams.state; + if (typeof stateParam === 'string') { try { const parsedState = JSON.parse(stateParam); if ( @@ -31,29 +38,58 @@ export function App() { Array.isArray(parsedState.ids) && parsedState.ids.length > 0 ) { - stateProcessed.current = true; const fileId = parsedState.ids[0]; - // Soft redirect to view file - navigate( - { - pathname: '/view', - search: queryString.stringify({ - source: 'google-drive', - fileId, - }), - }, - {replace: true}, - ); + redirectPath = '/view'; + mergedParams.source = 'google-drive'; + mergedParams.fileId = fileId; + delete mergedParams.state; + paramsModified = true; } } catch (err) { - // Silently catch JSON parsing errors for state parameters not meant for us (e.g. from other auth tools) + // Silently catch JSON parsing errors for state parameters not meant for us console.warn( 'Google Drive state query parameter JSON parsing failed or action mismatch:', err, ); } } - }, [navigate, location.search]); + + // 2. Handle WikiTree authcode presence + if (windowSearch.authcode) { + redirectPath = redirectPath || location.pathname; + paramsModified = true; + } + + if (paramsModified || windowSearch.state || windowSearch.authcode) { + // Strip external state / authcode parameters from window.location.search to prevent redirect loops. + const cleanWindowSearch = {...windowSearch}; + delete cleanWindowSearch.state; + delete cleanWindowSearch.authcode; + + const cleanWindowSearchStr = queryString.stringify(cleanWindowSearch); + const newUrl = + window.location.origin + + window.location.pathname + + (cleanWindowSearchStr ? '?' + cleanWindowSearchStr : '') + + window.location.hash; + window.history.replaceState(null, '', newUrl); + + return { + pathname: redirectPath || location.pathname, + search: queryString.stringify(mergedParams), + }; + } + + return null; + }, [location.pathname, location.search]); + + if (redirectTarget) { + return ( + + } /> + + ); + } const isViewPage = location.pathname === '/view';