extract useGoogleDriveAuthFlow from view_page.tsx

This commit is contained in:
Przemek Więch
2026-06-23 23:51:49 +02:00
parent 5037175727
commit 960853ee9f
2 changed files with 123 additions and 48 deletions

View File

@@ -0,0 +1,89 @@
import queryString from 'query-string';
import {useCallback, useState} from 'react';
import {useNavigate} from 'react-router';
import {
clearGoogleDriveCache,
googleDriveService,
} from '../datasource/google_drive_service';
import {useGoogleAuth} from './use_google_auth';
/**
* Custom React hook that encapsulates the Google Drive OAuth authorization flow.
* It manages token states, modal triggers, sign-out sessions, and navigation flows.
*/
export function useGoogleDriveAuthFlow(options: {
/** Callback triggered to clean up state when signing out. */
onSignOut: () => void;
/** Callback triggered when authorization succeeds for the failed file. */
onAuthSuccess: () => void;
}) {
const [showAuthModal, setShowAuthModal] = useState(false);
const [failedFileId, setFailedFileId] = useState<string>();
const {hasGoogleToken, setHasGoogleToken} = useGoogleAuth();
const navigate = useNavigate();
/**
* Performs Google Drive sign-out, clears active sessions, cleans up state,
* and redirects to the home page.
*/
const onGoogleSignOut = useCallback(async () => {
await googleDriveService.signOut();
setHasGoogleToken(false);
clearGoogleDriveCache();
options.onSignOut();
navigate({pathname: '/'}, {replace: true});
}, [setHasGoogleToken, navigate, options]);
/**
* Triggers the OAuth modal presentation for a failed file.
*/
const triggerAuthError = useCallback((fileId: string) => {
setFailedFileId(fileId);
setShowAuthModal(true);
}, []);
/**
* Called when Google Drive authorization succeeds.
*/
const onAuthSuccess = useCallback(
(fileId: string) => {
setShowAuthModal(false);
setHasGoogleToken(true);
if (fileId === failedFileId) {
options.onAuthSuccess();
} else {
// If a different file was selected/authorized, navigate to that one.
navigate(
{
pathname: '/view',
search: queryString.stringify({
source: 'google-drive',
fileId,
}),
},
{replace: true},
);
}
},
[failedFileId, navigate, setHasGoogleToken, options],
);
/**
* Called when the OAuth modal is cancelled.
*/
const onCancel = useCallback(() => {
setShowAuthModal(false);
navigate({pathname: '/'}, {replace: true});
}, [navigate]);
return {
showAuthModal,
failedFileId,
hasGoogleToken,
setHasGoogleToken,
onGoogleSignOut,
triggerAuthError,
onAuthSuccess,
onCancel,
};
}

View File

@@ -1,4 +1,3 @@
import queryString from 'query-string';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl'; import {useIntl} from 'react-intl';
import {useLocation, useNavigate} from 'react-router'; import {useLocation, useNavigate} from 'react-router';
@@ -20,11 +19,7 @@ import {
GoogleDriveAuthError, GoogleDriveAuthError,
GoogleDriveSourceSpec, GoogleDriveSourceSpec,
} from '../datasource/google_drive'; } from '../datasource/google_drive';
import { import {isGoogleDriveConfigured} from '../datasource/google_drive_service';
clearGoogleDriveCache,
googleDriveService,
isGoogleDriveConfigured,
} from '../datasource/google_drive_service';
import { import {
embeddedDataSource, embeddedDataSource,
gedcomUrlDataSource, gedcomUrlDataSource,
@@ -43,7 +38,7 @@ import {
WikiTreeSourceSpec, WikiTreeSourceSpec,
} from '../datasource/wikitree'; } from '../datasource/wikitree';
import {DonatsoChart} from '../donatso-chart'; import {DonatsoChart} from '../donatso-chart';
import {useGoogleAuth} from '../hooks/use_google_auth'; import {useGoogleDriveAuthFlow} from '../hooks/use_google_drive_auth_flow';
import {useUrlState} from '../hooks/use_url_state'; import {useUrlState} from '../hooks/use_url_state';
import {useWebMcpBridge} from '../hooks/use_webmcp_bridge'; import {useWebMcpBridge} from '../hooks/use_webmcp_bridge';
import {GoogleAuthModal} from '../menu/google_auth_modal'; import {GoogleAuthModal} from '../menu/google_auth_modal';
@@ -85,12 +80,25 @@ export function ViewPage() {
/** Specification of the source of the data. */ /** Specification of the source of the data. */
const [sourceSpec, setSourceSpec] = useState<DataSourceSpec>(); const [sourceSpec, setSourceSpec] = useState<DataSourceSpec>();
/** Controls the visibility of the Google Drive OAuth permission modal. */ // Manage Google Drive auth and session flows
const [showAuthModal, setShowAuthModal] = useState(false); const {
/** Stores the file ID that failed to load from Google Drive due to authorization errors. */ showAuthModal,
const [failedFileId, setFailedFileId] = useState<string>(); failedFileId,
/** Tracks whether the user has a valid cached Google Drive OAuth access token and provides a state setter. */ hasGoogleToken,
const {hasGoogleToken, setHasGoogleToken} = useGoogleAuth(); setHasGoogleToken,
onGoogleSignOut,
triggerAuthError,
onAuthSuccess,
onCancel,
} = useGoogleDriveAuthFlow({
onSignOut: useCallback(() => {
setData(undefined);
loadedSelectionRef.current = undefined;
}, []),
onAuthSuccess: useCallback(() => {
setState(AppState.INITIAL);
}, []),
});
const { const {
chartType, chartType,
@@ -257,16 +265,6 @@ export function ViewPage() {
[wikiTreeDataSource], [wikiTreeDataSource],
); );
async function onGoogleSignOut() {
await googleDriveService.signOut();
setHasGoogleToken(false);
setData(undefined);
loadedSelectionRef.current = undefined;
// Purge sessionStorage keys starting with "google-drive:"
clearGoogleDriveCache();
navigate({pathname: '/'}, {replace: true});
}
useEffect(() => { useEffect(() => {
(async () => { (async () => {
if (location.pathname !== '/view') { if (location.pathname !== '/view') {
@@ -325,8 +323,7 @@ export function ViewPage() {
} }
if (error instanceof GoogleDriveAuthError) { if (error instanceof GoogleDriveAuthError) {
if (args.sourceSpec.source === DataSourceEnum.GOOGLE_DRIVE) { if (args.sourceSpec.source === DataSourceEnum.GOOGLE_DRIVE) {
setFailedFileId(args.sourceSpec.fileId); triggerAuthError(args.sourceSpec.fileId);
setShowAuthModal(true);
} }
} else { } else {
setErrorMessage(getI18nMessage(error as Error, intl)); setErrorMessage(getI18nMessage(error as Error, intl));
@@ -382,7 +379,16 @@ export function ViewPage() {
} }
} }
})(); })();
}, [location, state, data, navigate, intl, isNewData, loadData]); }, [
location,
state,
data,
navigate,
intl,
isNewData,
loadData,
triggerAuthError,
]);
// Clean up object URLs created for uploaded images/files when the dataset // Clean up object URLs created for uploaded images/files when the dataset
// changes or the app unmounts to prevent memory leaks. // changes or the app unmounts to prevent memory leaks.
@@ -548,28 +554,8 @@ export function ViewPage() {
{showAuthModal && failedFileId && ( {showAuthModal && failedFileId && (
<GoogleAuthModal <GoogleAuthModal
failedFileId={failedFileId} failedFileId={failedFileId}
onAuthSuccess={(fileId) => { onAuthSuccess={onAuthSuccess}
setShowAuthModal(false); onCancel={onCancel}
setHasGoogleToken(true);
if (fileId === failedFileId) {
setState(AppState.INITIAL);
} else {
navigate(
{
pathname: '/view',
search: queryString.stringify({
source: 'google-drive',
fileId,
}),
},
{replace: true},
);
}
}}
onCancel={() => {
setShowAuthModal(false);
navigate({pathname: '/'}, {replace: true});
}}
/> />
)} )}
</> </>