66 DownloadFileResult ,
77 DownloadZipPayload ,
88 ElectronApiRequestResponse ,
9+ GooglePickerResult ,
910 IcpResponse ,
1011 IpcEventChannel ,
1112} from '@jetstream/desktop/types' ;
@@ -23,7 +24,7 @@ import { checkForUpdates, getCurrentUpdateStatus, installUpdate } from '../confi
2324import { ENV } from '../config/environment' ;
2425import { desktopRoutes } from '../controllers/desktop.routes' ;
2526import { getOrgFromHeaderOrQuery , initApiConnection } from '../utils/route.utils' ;
26- import { AuthResponseSuccess , logout , verifyAuthToken } from './api.service' ;
27+ import { AuthResponseSuccess , fetchGoogleConfig , logout , verifyAuthToken } from './api.service' ;
2728import { deepLink } from './deep-link.service' ;
2829import { downloadAndZipFilesToDisk , downloadBulkApiFileAndSaveToDisk } from './file-download.service' ;
2930import * as dataService from './persistence.service' ;
@@ -83,6 +84,8 @@ export function registerIpc(): void {
8384 registerHandler ( 'checkForUpdates' , handleCheckForUpdatesEvent ) ;
8485 registerHandler ( 'getUpdateStatus' , handleGetUpdateStatusEvent ) ;
8586 registerHandler ( 'installUpdate' , handleInstallUpdateEvent ) ;
87+ // Handle Google Picker
88+ registerHandler ( 'openGooglePicker' , handleOpenGooglePickerEvent ) ;
8689}
8790
8891const handleSelectFolderEvent : MainIpcHandler < 'selectFolder' > = async ( ) => {
@@ -247,6 +250,114 @@ const handleAddOrgEvent: MainIpcHandler<'addOrg'> = async (event, payload) => {
247250 } , 900000 ) ; // 15 minutes in milliseconds
248251} ;
249252
253+ const handleOpenGooglePickerEvent : MainIpcHandler < 'openGooglePicker' > = async ( event , payload ) => {
254+ const { mode } = payload ;
255+ const { deviceId, accessToken } = dataService . getAppData ( ) ;
256+
257+ if ( ! accessToken || ! deviceId ) {
258+ event . sender . send ( IpcEventChannel . toastMessage , {
259+ type : 'error' ,
260+ message : 'You must be logged in to use Google Drive integration' ,
261+ } ) ;
262+ return ;
263+ }
264+
265+ let googleConfig : { appId : string ; apiKey : string ; clientId : string } ;
266+ try {
267+ googleConfig = await fetchGoogleConfig ( { accessToken, deviceId } ) ;
268+ } catch ( ex ) {
269+ logger . error ( 'Error fetching Google config' , ex ) ;
270+ event . sender . send ( IpcEventChannel . toastMessage , {
271+ type : 'error' ,
272+ message : 'Failed to load Google Drive configuration. Please try again.' ,
273+ } ) ;
274+ return ;
275+ }
276+
277+ const nonce = crypto . randomUUID ( ) ;
278+ const pickerParams = new URLSearchParams ( {
279+ mode,
280+ nonce,
281+ appId : googleConfig . appId ,
282+ apiKey : googleConfig . apiKey ,
283+ clientId : googleConfig . clientId ,
284+ } ) ;
285+ if ( payload . accessToken ) {
286+ pickerParams . set ( 'accessToken' , payload . accessToken ) ;
287+ }
288+ if ( payload . accessTokenExpiresAt ) {
289+ pickerParams . set ( 'accessTokenExpiresAt' , `${ payload . accessTokenExpiresAt } ` ) ;
290+ }
291+ const pickerUrl = `${ ENV . SERVER_URL } /desktop-app/google-picker?${ pickerParams . toString ( ) } ` ;
292+
293+ await shell . openExternal ( pickerUrl ) ;
294+
295+ const handleCallback = async ( queryParams : Record < string , string > ) => {
296+ try {
297+ const parsed = z
298+ . discriminatedUnion ( 'status' , [
299+ z . object ( {
300+ nonce : z . literal ( nonce ) ,
301+ status : z . literal ( 'success' ) ,
302+ mode : z . enum ( [ 'file' , 'folder' , 'auth' ] ) . default ( mode ) ,
303+ googleAccessToken : z . string ( ) ,
304+ googleAccessTokenExpiresAt : z . coerce . number ( ) . optional ( ) ,
305+ fileId : z . string ( ) . optional ( ) ,
306+ fileName : z . string ( ) . optional ( ) ,
307+ mimeType : z . string ( ) . optional ( ) ,
308+ folderId : z . string ( ) . optional ( ) ,
309+ folderName : z . string ( ) . optional ( ) ,
310+ } ) ,
311+ z . object ( {
312+ nonce : z . literal ( nonce ) ,
313+ status : z . literal ( 'cancelled' ) ,
314+ } ) ,
315+ z . object ( {
316+ nonce : z . literal ( nonce ) ,
317+ status : z . literal ( 'error' ) ,
318+ errorMessage : z . string ( ) . optional ( ) ,
319+ } ) ,
320+ ] )
321+ . parse ( queryParams ) ;
322+
323+ let result : GooglePickerResult ;
324+
325+ if ( parsed . status === 'cancelled' ) {
326+ result = { status : 'cancelled' } ;
327+ } else if ( parsed . status === 'error' ) {
328+ result = { status : 'error' , error : parsed . errorMessage || 'Unknown error' } ;
329+ } else {
330+ result = {
331+ status : 'success' ,
332+ mode : parsed . mode || mode ,
333+ googleAccessToken : parsed . googleAccessToken ,
334+ googleAccessTokenExpiresAt : parsed . googleAccessTokenExpiresAt ,
335+ fileId : parsed . fileId ,
336+ fileName : parsed . fileName ,
337+ mimeType : parsed . mimeType ,
338+ folderId : parsed . folderId ,
339+ folderName : parsed . folderName ,
340+ } ;
341+ }
342+
343+ event . sender . send ( IpcEventChannel . googlePickerResult , result ) ;
344+ } catch ( ex ) {
345+ logger . error ( 'Error handling Google Picker callback' , ex ) ;
346+ const result : GooglePickerResult = { status : 'error' , error : 'Invalid response from Google Picker' } ;
347+ event . sender . send ( IpcEventChannel . googlePickerResult , result ) ;
348+ } finally {
349+ clearTimeout ( timeout ) ;
350+ }
351+ } ;
352+
353+ deepLink . once ( 'googlePicker' , handleCallback ) ;
354+
355+ // Remove the listener if it was not already removed - e.g. picker flow did not complete within 15 minutes
356+ const timeout = setTimeout ( ( ) => {
357+ deepLink . remove ( 'googlePicker' , handleCallback ) ;
358+ } , 900000 ) ; // 15 minutes in milliseconds
359+ } ;
360+
250361const handleCheckAuthEvent : MainIpcHandler < 'checkAuth' > = async ( ) : Promise <
251362 { userProfile : UserProfileUi ; authInfo : DesktopAuthInfo } | undefined
252363> => {
0 commit comments