import path from 'node:path'; import type { App, BrowserWindow } from 'electron'; import { ipcMain } from 'electron'; import { buildType, CLOUD_BASE_URL, isDev } from './config'; import { mainWindowOrigin } from './constants'; import { logger } from './logger'; import { getMainWindow, handleOpenUrlInHiddenWindow, setCookie, } from './windows-manager'; let protocol = buildType === 'stable' ? 'affine' : `affine-${buildType}`; if (isDev) { protocol = 'affine-dev'; } export function setupDeepLink(app: App) { if (process.defaultApp) { if (process.argv.length >= 2) { app.setAsDefaultProtocolClient(protocol, process.execPath, [ path.resolve(process.argv[1]), ]); } } else { app.setAsDefaultProtocolClient(protocol); } app.on('open-url', (event, url) => { if (url.startsWith(`${protocol}://`)) { event.preventDefault(); handleAffineUrl(url).catch(e => { logger.error('failed to handle affine url', e); }); } }); // on windows & linux, we need to listen for the second-instance event app.on('second-instance', (event, commandLine) => { getMainWindow() .then(window => { if (!window) { logger.error('main window is not ready'); return; } window.show(); const url = commandLine.pop(); if (url?.startsWith(`${protocol}://`)) { event.preventDefault(); handleAffineUrl(url).catch(e => { logger.error('failed to handle affine url', e); }); } }) .catch(e => console.error('Failed to restore or create window:', e)); }); } async function handleAffineUrl(url: string) { logger.info('open affine url', url); const urlObj = new URL(url); logger.info('handle affine schema action', urlObj.hostname); // handle more actions here // hostname is the action name if (urlObj.hostname === 'signin-redirect') { await handleOauthJwt(url); } if (urlObj.hostname === 'bring-to-front') { const mainWindow = await getMainWindow(); if (mainWindow) { mainWindow.show(); } } } async function handleOauthJwt(url: string) { const mainWindow = await getMainWindow(); if (url && mainWindow) { try { mainWindow.show(); const urlObj = new URL(url); const token = urlObj.searchParams.get('token'); if (!token) { logger.error('no token in url', url); return; } // set token to cookie await setCookie({ url: CLOUD_BASE_URL, httpOnly: true, value: token, secure: true, name: 'affine_session', expirationDate: Math.floor( Date.now() / 1000 + 3600 * 24 * 399 /* as long as possible, cookie max expires is 400 days */ ), }); let hiddenWindow: BrowserWindow | null = null; ipcMain.once('affine:login', () => { hiddenWindow?.destroy(); if (urlObj.searchParams.get('next') === 'onboarding') { // eslint-disable-next-line @typescript-eslint/no-floating-promises mainWindow.loadURL(mainWindowOrigin + '/auth/onboarding'); } }); // hacks to refresh auth state in the main window hiddenWindow = await handleOpenUrlInHiddenWindow( mainWindowOrigin + '/auth/signIn' ); } catch (e) { logger.error('failed to open url in popup', e); } } }