From 3d9201f7dc5e296837874dd8c2d4cf65ebacdc8f Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Sat, 31 Jan 2026 15:11:41 -0800 Subject: [PATCH] Drive picker: add a button to close and reset --- .../useGoogleDrivePicker.tsx | 78 ++++++++++++++++--- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/src/common/attachment-drafts/useGoogleDrivePicker.tsx b/src/common/attachment-drafts/useGoogleDrivePicker.tsx index d1be250b4..dfcc1520e 100644 --- a/src/common/attachment-drafts/useGoogleDrivePicker.tsx +++ b/src/common/attachment-drafts/useGoogleDrivePicker.tsx @@ -1,7 +1,13 @@ import * as React from 'react'; -import type { OAuthResponseEvent, PickerCanceledEvent, PickerPickedEvent } from '@googleworkspace/drive-picker-element'; +import { createPortal } from 'react-dom'; + +import type { PickerCanceledEvent, PickerPickedEvent } from '@googleworkspace/drive-picker-element'; import { DrivePicker, DrivePickerDocsView } from '@googleworkspace/drive-picker-react'; +import { IconButton } from '@mui/joy'; +import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; + +import { TooltipOutlined } from '~/common/components/TooltipOutlined'; import { addSnackbar } from '~/common/components/snackbar/useSnackbarsStore'; import type { AttachmentStoreCloudInput } from './useAttachmentDrafts'; @@ -16,23 +22,40 @@ export const hasGoogleDriveCapability = !!GOOGLE_DRIVE_CLIENT_ID; // Token provider interface, simple get/set -export interface ICloudProviderTokenValue { +export interface ICloudProviderTokenAccessor { get: () => string | null; - set: (token: string) => void; + set: (token: string | null) => void; } // in-memory token storage as default let _inMemoryToken: string | null = null; -const _inMemoryTokenStorage: ICloudProviderTokenValue = { +const _inMemoryTokenStorage: ICloudProviderTokenAccessor = { get: () => _inMemoryToken, - set: (token: string) => _inMemoryToken = token, + set: (token: string | null) => _inMemoryToken = token, }; +type _OauthResponseEvent = { + detail?: { + access_token: string; // xxxx.yyyyy.... + expires_in: string | number; // 3599 + scope: string; // 'https://www.googleapis.com/auth/drive.file' + token_type: string; // 'Bearer + }; +} + +type _OauthErrorEvent = { + detail?: object | { + type?: string; // 'popup_closed' + message?: string; // 'Popup window closed' + // stack?: string; + }; +} + export function useGoogleDrivePicker( onCloudFileSelected: (cloudFile: AttachmentStoreCloudInput) => void, isMobile: boolean, - tokenStorage: ICloudProviderTokenValue = _inMemoryTokenStorage, + tokenStorage: ICloudProviderTokenAccessor = _inMemoryTokenStorage, loginHint?: string, ) { @@ -43,14 +66,15 @@ export function useGoogleDrivePicker( const openGoogleDrivePicker = React.useCallback(() => setIsPickerOpen(true), []); - const handleOAuthResponse = React.useCallback((e: OAuthResponseEvent) => { + const handleOAuthResponse = React.useCallback((e: _OauthResponseEvent) => { if (e.detail?.access_token) tokenStorage.set(e.detail.access_token); }, [tokenStorage]); - const handleOAuthError = React.useCallback(() => { + const handleOAuthError = React.useCallback((e: _OauthErrorEvent) => { setIsPickerOpen(false); - addSnackbar({ key: 'gdrive-oauth-error', message: 'Google Drive authentication failed.', type: 'issue' }); + if (!e?.detail || !('type' in e?.detail) || e.detail.type !== 'popup_closed') + addSnackbar({ key: 'gdrive-oauth-error', message: 'Google Drive authentication failed.', type: 'issue' }); }, []); @@ -96,7 +120,39 @@ export function useGoogleDrivePicker( }, [onCloudFileSelected, tokenStorage]); - const googleDrivePickerComponent = React.useMemo(() => !isPickerOpen || !GOOGLE_DRIVE_CLIENT_ID ? null : ( + const handleCloseClick = React.useCallback(() => { + setIsPickerOpen(false); + tokenStorage.set(''); + }, [tokenStorage]); + + const googleDrivePickerComponent = React.useMemo(() => !isPickerOpen || !GOOGLE_DRIVE_CLIENT_ID ? null : <> + + {/* Top-level close button - portaled to body, above the Google Drive picker */} + {createPortal( + + + + + , + document.body, + )} + + - ), [handleCanceled, handleOAuthError, handleOAuthResponse, handlePicked, isMobile, isPickerOpen, loginHint, tokenStorage]); + , [handleCanceled, handleCloseClick, handleOAuthError, handleOAuthResponse, handlePicked, isMobile, isPickerOpen, loginHint, tokenStorage]); return { openGoogleDrivePicker,