diff --git a/apps/desktop-wallet/src/electron/autoUpdater.ts b/apps/desktop-wallet/src/electron/autoUpdater.ts new file mode 100644 index 000000000..10125f19e --- /dev/null +++ b/apps/desktop-wallet/src/electron/autoUpdater.ts @@ -0,0 +1,51 @@ +/* +Copyright 2018 - 2024 The Alephium Authors +This file is part of the alephium project. + +The library is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +The library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with the library. If not, see . +*/ + +import { BrowserWindow, ipcMain } from 'electron' +import { autoUpdater } from 'electron-updater' + +import { IS_RC } from '@/electron/utils' + +export const configureAutoUpdater = () => { + autoUpdater.autoDownload = false + autoUpdater.allowPrerelease = IS_RC +} + +export const setupAutoUpdaterListeners = (mainWindow: BrowserWindow) => { + autoUpdater.on('download-progress', (info) => mainWindow.webContents.send('updater:download-progress', info)) + + autoUpdater.on('error', (error) => mainWindow.webContents.send('updater:error', error)) + + autoUpdater.on('update-downloaded', (event) => mainWindow.webContents.send('updater:updateDownloaded', event)) +} + +export const handleAutoUpdaterUserActions = () => { + ipcMain.handle('updater:checkForUpdates', async () => { + try { + const result = await autoUpdater.checkForUpdates() + + return result?.updateInfo?.version + } catch (e) { + console.error(e) + } + }) + + ipcMain.handle('updater:startUpdateDownload', () => autoUpdater.downloadUpdate()) + + ipcMain.handle('updater:quitAndInstallUpdate', () => autoUpdater.quitAndInstall()) +} diff --git a/apps/desktop-wallet/public/electron.js b/apps/desktop-wallet/src/electron/index.ts similarity index 58% rename from apps/desktop-wallet/public/electron.js rename to apps/desktop-wallet/src/electron/index.ts index 0f719e82f..5f4be95f3 100644 --- a/apps/desktop-wallet/public/electron.js +++ b/apps/desktop-wallet/src/electron/index.ts @@ -16,22 +16,25 @@ You should have received a copy of the GNU Lesser General Public License along with the library. If not, see . */ -// Modules to control application life and create native browser window -const { app, BrowserWindow, dialog, ipcMain, Menu, nativeTheme, shell, nativeImage, protocol } = require('electron') -const path = require('path') -const isDev = require('electron-is-dev') -const contextMenu = require('electron-context-menu') -const { autoUpdater } = require('electron-updater') -const TransportNodeHid = require('@ledgerhq/hw-transport-node-hid').default -const AlephiumLedgerApp = require('@alephium/ledger-app').AlephiumApp -const { listen } = require('@ledgerhq/logs') -const web3 = require('@alephium/web3-wallet') +import { AlephiumApp as AlephiumLedgerApp } from '@alephium/ledger-app' +import { getHumanReadableError } from '@alephium/shared' +import web3 from '@alephium/web3-wallet' +import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' +import { listen } from '@ledgerhq/logs' +import { app, BrowserWindow, ipcMain, nativeImage, protocol, shell } from 'electron' +import contextMenu from 'electron-context-menu' +import isDev from 'electron-is-dev' +import path from 'path' + +import { configureAutoUpdater, handleAutoUpdaterUserActions, setupAutoUpdaterListeners } from '@/electron/autoUpdater' +import { setupAppMenu } from '@/electron/menu' +import { handleNativeThemeUserActions, setupNativeThemeListeners } from '@/electron/nativeTheme' +import { IS_RC, isMac, isWindows } from '@/electron/utils' + +configureAutoUpdater() let alephiumLedgerApp -const CURRENT_VERSION = app.getVersion() -const IS_RC = CURRENT_VERSION.includes('-rc.') - // Handle deep linking for alephium:// const ALEPHIUM = 'alephium' const ALEPHIUM_WALLET_CONNECT_DEEP_LINK_PREFIX = `${ALEPHIUM}://wc` @@ -55,123 +58,17 @@ if (process.defaultApp) { contextMenu() -autoUpdater.autoDownload = false -autoUpdater.allowPrerelease = IS_RC - // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. -let mainWindow +let mainWindow: Electron.BrowserWindow | null // Build menu -const isMac = process.platform === 'darwin' -const isWindows = process.platform === 'win32' - -const template = [ - ...(isMac - ? [ - { - label: app.name, - submenu: [ - { role: 'about' }, - { type: 'separator' }, - { role: 'services' }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideOthers' }, - { role: 'unhide' }, - { type: 'separator' }, - { role: 'quit' } - ] - } - ] - : []), - { - label: 'Edit', - submenu: [ - { role: 'undo' }, - { role: 'redo' }, - { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - ...(isMac - ? [ - { role: 'pasteAndMatchStyle' }, - { role: 'delete' }, - { role: 'selectAll' }, - { type: 'separator' }, - { - label: 'Speech', - submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }] - } - ] - : [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]) - ] - }, - { - label: 'View', - submenu: [ - { role: 'resetZoom' }, - { role: 'zoomIn' }, - { role: 'zoomOut' }, - { type: 'separator' }, - { role: 'togglefullscreen' }, - { type: 'separator' }, - { role: 'reload' }, - { role: 'forceReload' } - ] - }, - { - label: 'Window', - submenu: [ - { role: 'minimize' }, - ...(isMac ? [{ role: 'zoom' }, { type: 'separator' }, { role: 'front' }] : [{ role: 'close' }]) - ] - }, - { - role: 'help', - submenu: [ - ...(isMac - ? [] - : isWindows - ? [{ role: 'about' }, { type: 'separator' }] - : [ - { - label: 'About', - click: async () => { - dialog.showMessageBox(mainWindow, { - message: `Version ${CURRENT_VERSION}`, - title: 'About', - type: 'info' - }) - } - } - ]), - { - label: 'Report an issue', - click: async () => { - await shell.openExternal('https://github.com/alephium/alephium-frontend/issues/new') - } - }, - { - label: 'Get some help', - click: async () => { - await shell.openExternal('https://discord.gg/JErgRBfRSB') - } - } - ] - } -] - const appURL = isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}` -let deepLinkUri = null - -function createWindow() { - const menu = Menu.buildFromTemplate(template) - Menu.setApplicationMenu(menu) +let deepLinkUri: string | null = null +const createWindow = () => { mainWindow = new BrowserWindow({ width: 1200, height: 800, @@ -186,6 +83,8 @@ function createWindow() { } }) + setupAppMenu(mainWindow) + if (!isMac && !isWindows) { mainWindow.setIcon( nativeImage.createFromPath(path.join(__dirname, isDev ? 'icons/logo-48.png' : '../build/icons/logo-48.png')) @@ -205,17 +104,10 @@ function createWindow() { return { action: 'deny' } }) - nativeTheme.on('updated', () => - mainWindow?.webContents.send('theme:shouldUseDarkColors', nativeTheme.shouldUseDarkColors) - ) - mainWindow.on('closed', () => (mainWindow = null)) - autoUpdater.on('download-progress', (info) => mainWindow?.webContents.send('updater:download-progress', info)) - - autoUpdater.on('error', (error) => mainWindow?.webContents.send('updater:error', error)) - - autoUpdater.on('update-downloaded', (event) => mainWindow?.webContents.send('updater:updateDownloaded', event)) + setupNativeThemeListeners(mainWindow) + setupAutoUpdaterListeners(mainWindow) if (!isMac) { if (process.argv.length > 1) { @@ -230,7 +122,6 @@ function createWindow() { if (!app.requestSingleInstanceLock()) { app.quit() - return } // Activate the window of primary instance when a second instance starts @@ -258,40 +149,20 @@ app.on('second-instance', (_event, args) => { app.on('ready', async function () { if (isDev) { const { - default: { default: installExtension, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } + default: installExtension, + REACT_DEVELOPER_TOOLS, + REDUX_DEVTOOLS } = await import('electron-devtools-installer') await installExtension(REACT_DEVELOPER_TOOLS) await installExtension(REDUX_DEVTOOLS) } - ipcMain.handle('theme:setNativeTheme', (_, theme) => (nativeTheme.themeSource = theme)) - - // nativeTheme must be reassigned like this because its properties are all computed, so - // they can't be serialized to be passed over channels. - ipcMain.handle('theme:getNativeTheme', ({ sender }) => - sender.send('theme:getNativeTheme', { - shouldUseDarkColors: nativeTheme.shouldUseDarkColors, - themeSource: nativeTheme.themeSource - }) - ) - - ipcMain.handle('updater:checkForUpdates', async () => { - try { - const result = await autoUpdater.checkForUpdates() - - return result?.updateInfo?.version - } catch (e) { - console.error(e) - } - }) - - ipcMain.handle('updater:startUpdateDownload', () => autoUpdater.downloadUpdate()) - - ipcMain.handle('updater:quitAndInstallUpdate', () => autoUpdater.quitAndInstall()) + handleNativeThemeUserActions() + handleAutoUpdaterUserActions() ipcMain.handle('app:hide', () => { if (isWindows) { - mainWindow.blur() + mainWindow?.blur() } else { app.hide() } @@ -299,10 +170,10 @@ app.on('ready', async function () { ipcMain.handle('app:show', () => { if (isWindows) { - mainWindow.minimize() - mainWindow.restore() + mainWindow?.minimize() + mainWindow?.restore() } else { - mainWindow.show() + mainWindow?.show() } }) @@ -312,16 +183,14 @@ app.on('ready', async function () { if (preferedLanguages.length > 0) return preferedLanguages[0] }) - ipcMain.handle('app:getSystemRegion', () => { - return app.getSystemLocale() - }) + ipcMain.handle('app:getSystemRegion', () => app.getSystemLocale()) ipcMain.handle('app:setProxySettings', async (_, proxySettings) => { const { address, port } = proxySettings const proxyRules = !address && !port ? undefined : `socks5://${address}:${port}` try { - await mainWindow.webContents.session.setProxy({ proxyRules }) + await mainWindow?.webContents.session.setProxy({ proxyRules }) } catch (e) { console.error(e) } @@ -366,7 +235,7 @@ app.on('ready', async function () { success: true, version, initialAddress: { hash: account.address, index: 0, publicKey: account.publicKey }, - deviceModel: alephiumLedgerApp.transport.deviceModel.productName + deviceModel: alephiumLedgerApp.transport.deviceModel?.productName } transport.close() @@ -380,7 +249,7 @@ app.on('ready', async function () { console.error('🔌❌', error) // Retry one more time if the error is unknown, usually the Ledger app needs a moment - if (error.message.includes('UNKNOWN_ERROR')) { + if (getHumanReadableError(error, '').includes('UNKNOWN_ERROR')) { return new Promise((s) => setTimeout(s, 1000)).then(connect).catch((error) => ({ success: false, error })) } @@ -411,7 +280,7 @@ app.on('open-url', (_, url) => { } }) -const extractWalletConnectUri = (url) => +const extractWalletConnectUri = (url: string) => url.substring(url.indexOf(ALEPHIUM_WALLET_CONNECT_URI_PREFIX) + ALEPHIUM_WALLET_CONNECT_URI_PREFIX.length) // Handle window controls via IPC diff --git a/apps/desktop-wallet/src/electron/menu.ts b/apps/desktop-wallet/src/electron/menu.ts new file mode 100644 index 000000000..358f65f08 --- /dev/null +++ b/apps/desktop-wallet/src/electron/menu.ts @@ -0,0 +1,126 @@ +/* +Copyright 2018 - 2024 The Alephium Authors +This file is part of the alephium project. + +The library is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +The library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with the library. If not, see . +*/ + +import { app, BrowserWindow, dialog, Menu, MenuItemConstructorOptions, shell } from 'electron' + +import { CURRENT_VERSION, isMac, isWindows } from '@/electron/utils' + +export const setupAppMenu = (mainWindow: BrowserWindow) => { + const menu = Menu.buildFromTemplate(generateMenuTemplate(mainWindow)) + Menu.setApplicationMenu(menu) +} + +const generateMenuTemplate = (mainWindow: BrowserWindow): MenuItemConstructorOptions[] => [ + ...(isMac + ? ([ + { + label: app.name, + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ] + } + ] as MenuItemConstructorOptions[]) + : []), + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + ...(isMac + ? ([ + { role: 'pasteAndMatchStyle' }, + { role: 'delete' }, + { role: 'selectAll' }, + { type: 'separator' }, + { + label: 'Speech', + submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }] + } + ] as MenuItemConstructorOptions[]) + : ([{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }] as MenuItemConstructorOptions[])) + ] + }, + { + label: 'View', + submenu: [ + { role: 'resetZoom' }, + { role: 'zoomIn' }, + { role: 'zoomOut' }, + { type: 'separator' }, + { role: 'togglefullscreen' }, + { type: 'separator' }, + { role: 'reload' }, + { role: 'forceReload' } + ] + }, + { + label: 'Window', + submenu: [ + { role: 'minimize' }, + ...(isMac + ? ([{ role: 'zoom' }, { type: 'separator' }, { role: 'front' }] as MenuItemConstructorOptions[]) + : ([{ role: 'close' }] as MenuItemConstructorOptions[])) + ] + }, + { + role: 'help', + submenu: [ + ...(isMac + ? ([] as MenuItemConstructorOptions[]) + : isWindows + ? ([{ role: 'about' }, { type: 'separator' }] as MenuItemConstructorOptions[]) + : ([ + { + label: 'About', + click: async () => { + mainWindow && + dialog.showMessageBox(mainWindow, { + message: `Version ${CURRENT_VERSION}`, + title: 'About', + type: 'info' + }) + } + } + ] as MenuItemConstructorOptions[])), + { + label: 'Report an issue', + click: async () => { + await shell.openExternal('https://github.com/alephium/alephium-frontend/issues/new') + } + }, + { + label: 'Get some help', + click: async () => { + await shell.openExternal('https://discord.gg/JErgRBfRSB') + } + } + ] + } +] diff --git a/apps/desktop-wallet/src/electron/nativeTheme.ts b/apps/desktop-wallet/src/electron/nativeTheme.ts new file mode 100644 index 000000000..59ccef50d --- /dev/null +++ b/apps/desktop-wallet/src/electron/nativeTheme.ts @@ -0,0 +1,38 @@ +/* +Copyright 2018 - 2024 The Alephium Authors +This file is part of the alephium project. + +The library is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +The library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with the library. If not, see . +*/ + +import { BrowserWindow, ipcMain, nativeTheme } from 'electron' + +export const setupNativeThemeListeners = (mainWindow: BrowserWindow) => { + nativeTheme.on('updated', () => + mainWindow.webContents.send('theme:shouldUseDarkColors', nativeTheme.shouldUseDarkColors) + ) +} + +export const handleNativeThemeUserActions = () => { + ipcMain.handle('theme:setNativeTheme', (_, theme) => (nativeTheme.themeSource = theme)) + + // nativeTheme must be reassigned like this because its properties are all computed, so + // they can't be serialized to be passed over channels. + ipcMain.handle('theme:getNativeTheme', ({ sender }) => + sender.send('theme:getNativeTheme', { + shouldUseDarkColors: nativeTheme.shouldUseDarkColors, + themeSource: nativeTheme.themeSource + }) + ) +} diff --git a/apps/desktop-wallet/src/electron/utils.ts b/apps/desktop-wallet/src/electron/utils.ts new file mode 100644 index 000000000..62a0e56bd --- /dev/null +++ b/apps/desktop-wallet/src/electron/utils.ts @@ -0,0 +1,27 @@ +/* +Copyright 2018 - 2024 The Alephium Authors +This file is part of the alephium project. + +The library is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +The library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with the library. If not, see . +*/ + +import { app } from 'electron' + +export const isMac = process.platform === 'darwin' + +export const isWindows = process.platform === 'win32' + +export const CURRENT_VERSION = app.getVersion() + +export const IS_RC = CURRENT_VERSION.includes('-rc.') diff --git a/apps/desktop-wallet/tsconfig.json b/apps/desktop-wallet/tsconfig.json index 1fc55f341..dac647e7b 100644 --- a/apps/desktop-wallet/tsconfig.json +++ b/apps/desktop-wallet/tsconfig.json @@ -4,6 +4,8 @@ "exclude": ["node_modules"], "compilerOptions": { "baseUrl": ".", + "outDir": "dist", + "noEmit": false, "paths": { "@/*": ["./src/*"] }