mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-07-17 17:21:48 +00:00
extract useGoogleDriveAuthFlow from view_page.tsx
This commit is contained in:
89
src/hooks/use_google_drive_auth_flow.ts
Normal file
89
src/hooks/use_google_drive_auth_flow.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user