From cab0643183f7361365ecef0e1f49569d66b4b33c Mon Sep 17 00:00:00 2001 From: Tomas Martykan Date: Fri, 26 Jul 2024 13:00:48 +0200 Subject: [PATCH] feat(suite): autostart --- .../src/__tests__/api.test.ts | 6 ++ packages/suite-desktop-api/src/api.ts | 2 + packages/suite-desktop-api/src/factory.ts | 1 + .../src/modules/auto-start.ts | 58 +++++++++++++++++++ .../suite-desktop-core/src/modules/bridge.ts | 17 +++--- .../suite-desktop-core/src/modules/index.ts | 2 + .../actions/suite/constants/suiteConstants.ts | 1 + .../suite/src/actions/suite/suiteActions.ts | 14 ++++- packages/suite/src/constants/suite/anchors.ts | 1 + .../middlewares/wallet/storageMiddleware.ts | 1 + .../suite/src/reducers/suite/suiteReducer.ts | 5 ++ packages/suite/src/support/messages.ts | 8 +++ .../settings/SettingsDebug/AutoStart.tsx | 40 +++++++++++++ .../settings/SettingsDebug/SettingsDebug.tsx | 6 ++ 14 files changed, 151 insertions(+), 11 deletions(-) create mode 100644 packages/suite-desktop-core/src/modules/auto-start.ts create mode 100644 packages/suite/src/views/settings/SettingsDebug/AutoStart.tsx diff --git a/packages/suite-desktop-api/src/__tests__/api.test.ts b/packages/suite-desktop-api/src/__tests__/api.test.ts index 51d212d5be5..181c67fae7e 100644 --- a/packages/suite-desktop-api/src/__tests__/api.test.ts +++ b/packages/suite-desktop-api/src/__tests__/api.test.ts @@ -79,6 +79,12 @@ describe('DesktopApi', () => { api.appHide(true); }); + it('DesktopApi.appAutoStart', () => { + const spy = jest.spyOn(ipcRenderer, 'send'); + api.appAutoStart(true); + expect(spy).toHaveBeenCalledWith('app/auto-start', true); + }); + it('DesktopApi.checkForUpdates', () => { const spy = jest.spyOn(ipcRenderer, 'send'); api.checkForUpdates(); diff --git a/packages/suite-desktop-api/src/api.ts b/packages/suite-desktop-api/src/api.ts index 884ca137ee5..bfa9f5cd18a 100644 --- a/packages/suite-desktop-api/src/api.ts +++ b/packages/suite-desktop-api/src/api.ts @@ -23,6 +23,7 @@ export interface MainChannels { 'app/restart': void; 'app/focus': void; 'app/hide': void; + 'app/auto-start': boolean; 'store/clear': void; 'theme/change': SuiteThemeVariant; 'tor/get-status': void; @@ -101,6 +102,7 @@ export interface DesktopApi { appRestart: DesktopApiSend<'app/restart'>; appFocus: DesktopApiSend<'app/focus'>; appHide: DesktopApiSend<'app/hide'>; + appAutoStart: DesktopApiSend<'app/auto-start'>; // Auto-updater checkForUpdates: DesktopApiSend<'update/check'>; downloadUpdate: DesktopApiSend<'update/download'>; diff --git a/packages/suite-desktop-api/src/factory.ts b/packages/suite-desktop-api/src/factory.ts index 0b0c6150d85..6e963cc6dc4 100644 --- a/packages/suite-desktop-api/src/factory.ts +++ b/packages/suite-desktop-api/src/factory.ts @@ -46,6 +46,7 @@ export const factory = >( appRestart: () => ipcRenderer.send('app/restart'), appFocus: () => ipcRenderer.send('app/focus'), appHide: () => ipcRenderer.send('app/hide'), + appAutoStart: (enabled: boolean) => ipcRenderer.send('app/auto-start', enabled), // Auto-updater checkForUpdates: isManual => { diff --git a/packages/suite-desktop-core/src/modules/auto-start.ts b/packages/suite-desktop-core/src/modules/auto-start.ts new file mode 100644 index 00000000000..9185a87dd2f --- /dev/null +++ b/packages/suite-desktop-core/src/modules/auto-start.ts @@ -0,0 +1,58 @@ +/** + * Auto start handler + */ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import { app, ipcMain } from '../typed-electron'; + +import type { Module } from './index'; + +export const SERVICE_NAME = 'auto-start'; + +// Linux autostart desktop file +const LINUX_AUTOSTART_DIR = '.config/autostart/'; +const LINUX_AUTOSTART_FILE = 'Trezor-Suite.desktop'; +const LINUX_DESKTOP = `[Desktop Entry] +Type=Application +Version=1.0 +Name=Trezor Suite +Comment=Trezor Suite startup script +Exec="${process.env.APPIMAGE || process.execPath}" --bridge-daemon +StartupNotify=false +Terminal=false +`; + +const linuxAutoStart = (enabled: boolean) => { + if (enabled) { + fs.mkdirSync(path.join(os.homedir(), LINUX_AUTOSTART_DIR), { recursive: true }); + fs.writeFileSync( + path.join(os.homedir(), LINUX_AUTOSTART_DIR, LINUX_AUTOSTART_FILE), + LINUX_DESKTOP, + ); + fs.chmodSync(path.join(os.homedir(), LINUX_AUTOSTART_DIR, LINUX_AUTOSTART_FILE), 0o755); + } else { + fs.unlinkSync(path.join(os.homedir(), LINUX_AUTOSTART_DIR, LINUX_AUTOSTART_FILE)); + } +}; + +export const init: Module = () => { + const { logger } = global; + + ipcMain.on('app/auto-start', (_, enabled: boolean) => { + logger.debug(SERVICE_NAME, 'Auto start ' + (enabled ? 'enabled' : 'disabled')); + + if (process.platform === 'linux') { + // For Linux, we use a custom implementation + linuxAutoStart(enabled); + } else { + // For Windows and macOS, we use native Electron API + app.setLoginItemSettings({ + openAtLogin: enabled, + openAsHidden: true, + args: ['--bridge-daemon'], + }); + } + }); +}; diff --git a/packages/suite-desktop-core/src/modules/bridge.ts b/packages/suite-desktop-core/src/modules/bridge.ts index 900c94040c1..830f6f88eb3 100644 --- a/packages/suite-desktop-core/src/modules/bridge.ts +++ b/packages/suite-desktop-core/src/modules/bridge.ts @@ -24,17 +24,14 @@ const skipNewBridgeRollout = app.commandLine.hasSwitch('skip-new-bridge-rollout' export const SERVICE_NAME = 'bridge'; -const handleBridgeStatus = async ( - bridge: BridgeProcess | TrezordNode, - mainWindow: Dependencies['mainWindow'], -) => { +const handleBridgeStatus = async (bridge: BridgeProcess | TrezordNode) => { const { logger } = global; logger.info('bridge', `Getting status`); const status = await bridge.status(); logger.info('bridge', `Toggling bridge. Status: ${JSON.stringify(status)}`); - mainWindow.webContents.send('bridge/status', status); + ipcMain.emit('bridge/status', status); return status; }; @@ -100,7 +97,7 @@ const getBridgeInstance = (store: Dependencies['store']) => { }); }; -const load = async ({ store, mainWindow }: Dependencies) => { +const load = async ({ store }: Dependencies) => { const { logger } = global; const bridge = getBridgeInstance(store); @@ -112,7 +109,7 @@ const load = async ({ store, mainWindow }: Dependencies) => { ipcMain.handle('bridge/toggle', async ipcEvent => { validateIpcMessage(ipcEvent); - const status = await handleBridgeStatus(bridge, mainWindow); + const status = await handleBridgeStatus(bridge); try { if (status.service) { await bridge.stop(); @@ -124,7 +121,7 @@ const load = async ({ store, mainWindow }: Dependencies) => { } catch (error) { return { success: false, error }; } finally { - handleBridgeStatus(bridge, mainWindow); + handleBridgeStatus(bridge); } }); @@ -152,7 +149,7 @@ const load = async ({ store, mainWindow }: Dependencies) => { } catch (error) { return { success: false, error }; } finally { - mainWindow.webContents.send('bridge/settings', store.getBridgeSettings()); + ipcMain.emit('bridge/settings', store.getBridgeSettings()); } }, ); @@ -177,7 +174,7 @@ const load = async ({ store, mainWindow }: Dependencies) => { `Starting (Legacy dev: ${b2t(bridgeLegacyDev)}, Legacy test: ${b2t(bridgeLegacyTest)}, Legacy: ${b2t(bridgeLegacy)}, Test: ${b2t(bridgeTest)})`, ); await start(bridge); - handleBridgeStatus(bridge, mainWindow); + handleBridgeStatus(bridge); } catch (err) { logger.error(SERVICE_NAME, `Start failed: ${err.message}`); } diff --git a/packages/suite-desktop-core/src/modules/index.ts b/packages/suite-desktop-core/src/modules/index.ts index a6f27d1fec5..72a8eb91862 100644 --- a/packages/suite-desktop-core/src/modules/index.ts +++ b/packages/suite-desktop-core/src/modules/index.ts @@ -33,6 +33,7 @@ import * as requestInterceptor from './request-interceptor'; import * as coinjoin from './coinjoin'; import * as csp from './csp'; import * as fileProtocol from './file-protocol'; +import * as autoStart from './auto-start'; // General modules (both dev & prod) const MODULES = [ @@ -61,6 +62,7 @@ const MODULES = [ devTools, requestInterceptor, coinjoin, + autoStart, // Modules used only in dev/prod mode ...(isDevEnv ? [] : [csp, fileProtocol]), ]; diff --git a/packages/suite/src/actions/suite/constants/suiteConstants.ts b/packages/suite/src/actions/suite/constants/suiteConstants.ts index 805c69c74ad..1a176848789 100644 --- a/packages/suite/src/actions/suite/constants/suiteConstants.ts +++ b/packages/suite/src/actions/suite/constants/suiteConstants.ts @@ -32,3 +32,4 @@ export const LOCK_TYPE = { export const REQUEST_DEVICE_RECONNECT = '@suite/request-device-reconnect'; export const SET_EXPERIMENTAL_FEATURES = '@suite/set-experimental-features'; export const SET_SIDEBAR_WIDTH = '@suite/set-sidebar-width'; +export const SET_AUTO_START = '@suite/set-auto-start'; diff --git a/packages/suite/src/actions/suite/suiteActions.ts b/packages/suite/src/actions/suite/suiteActions.ts index f495b4b725c..bae78dfd4e5 100644 --- a/packages/suite/src/actions/suite/suiteActions.ts +++ b/packages/suite/src/actions/suite/suiteActions.ts @@ -76,7 +76,8 @@ export type SuiteAction = } | { type: typeof deviceActions.requestDeviceReconnect.type } | { type: typeof SUITE.SET_EXPERIMENTAL_FEATURES; payload?: ExperimentalFeature[] } - | { type: typeof SUITE.SET_SIDEBAR_WIDTH; payload: { width: number } }; + | { type: typeof SUITE.SET_SIDEBAR_WIDTH; payload: { width: number } } + | { type: typeof SUITE.SET_AUTO_START; enabled?: boolean }; export const appChanged = createAction( SUITE.APP_CHANGED, @@ -335,3 +336,14 @@ export const lockRouter = (payload: boolean): SuiteAction => ({ type: SUITE.LOCK_ROUTER, payload, }); + +/** + * Set auto start for Suite + * @param enabled {boolean} - true if Suite should start automatically + * @returns {SuiteAction} + */ +export const setAutoStart = (enabled: boolean) => (dispatch: Dispatch) => { + desktopApi.appAutoStart(enabled); + + dispatch({ type: SUITE.SET_AUTO_START, enabled }); +}; diff --git a/packages/suite/src/constants/suite/anchors.ts b/packages/suite/src/constants/suite/anchors.ts index a7a15df3ca7..e1907e00245 100644 --- a/packages/suite/src/constants/suite/anchors.ts +++ b/packages/suite/src/constants/suite/anchors.ts @@ -14,6 +14,7 @@ export const enum SettingsAnchor { ClearStorage = '@general-settings/clear-storage', VersionWithUpdate = '@general-settings/version-with-update', EarlyAccess = '@general-settings/early-access', + AutoStart = '@general-settings/auto-start', BackupFailed = '@device-settings/backup-failed', BackupRecoverySeed = '@device-settings/backup-recovery-seed', diff --git a/packages/suite/src/middlewares/wallet/storageMiddleware.ts b/packages/suite/src/middlewares/wallet/storageMiddleware.ts index 63ea3192368..03136b8984a 100644 --- a/packages/suite/src/middlewares/wallet/storageMiddleware.ts +++ b/packages/suite/src/middlewares/wallet/storageMiddleware.ts @@ -231,6 +231,7 @@ const storageMiddleware = (api: MiddlewareAPI) => { case SUITE.SET_DEFAULT_WALLET_LOADING: case SUITE.SET_AUTODETECT: case SUITE.SET_SIDEBAR_WIDTH: + case SUITE.SET_AUTO_START: case SUITE.DEVICE_AUTHENTICITY_OPT_OUT: case SUITE.EVM_CONFIRM_EXPLANATION_MODAL: case SUITE.EVM_CLOSE_EXPLANATION_BANNER: diff --git a/packages/suite/src/reducers/suite/suiteReducer.ts b/packages/suite/src/reducers/suite/suiteReducer.ts index 734ec9102ed..f778868cd2c 100644 --- a/packages/suite/src/reducers/suite/suiteReducer.ts +++ b/packages/suite/src/reducers/suite/suiteReducer.ts @@ -91,6 +91,7 @@ export interface SuiteSettings { defaultWalletLoading: WalletType; experimental?: ExperimentalFeature[]; sidebarWidth: number; + autoStart?: boolean; } export interface SuiteState { @@ -268,6 +269,10 @@ const suiteReducer = (state: SuiteState = initialState, action: Action): SuiteSt draft.settings.sidebarWidth = action.payload.width; break; + case SUITE.SET_AUTO_START: + draft.settings.autoStart = action.enabled; + break; + case TRANSPORT.START: draft.transport = action.payload; break; diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 1ca85803bc4..2a3fcc91909 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -9161,4 +9161,12 @@ export default defineMessages({ id: 'TR_OPEN_TREZOR_SUITE_DESKTOP', defaultMessage: 'Open Trezor Suite desktop app', }, + TR_AUTO_START: { + id: 'TR_AUTO_START', + defaultMessage: 'Run Trezor Suite automatically', + }, + TR_AUTO_START_DESCRIPTION: { + id: 'TR_AUTO_START_DESCRIPTION', + defaultMessage: 'Start Trezor Suite in the background when you log in to your computer.', + }, }); diff --git a/packages/suite/src/views/settings/SettingsDebug/AutoStart.tsx b/packages/suite/src/views/settings/SettingsDebug/AutoStart.tsx new file mode 100644 index 00000000000..bb3fb7a06b5 --- /dev/null +++ b/packages/suite/src/views/settings/SettingsDebug/AutoStart.tsx @@ -0,0 +1,40 @@ +import styled from 'styled-components'; + +import { Switch } from '@trezor/components'; + +import { SettingsSectionItem } from 'src/components/settings'; +import { ActionColumn, TextColumn, Translation } from 'src/components/suite'; +import { SettingsAnchor } from 'src/constants/suite/anchors'; +import { useDispatch, useSelector } from 'src/hooks/suite'; +import { setAutoStart } from 'src/actions/suite/suiteActions'; + +const PositionedSwitch = styled.div` + align-self: center; +`; + +export const AutoStart = () => { + const autoStartEnabled = useSelector(state => state.suite.settings.autoStart); + const dispatch = useDispatch(); + + const handleChange = () => { + dispatch(setAutoStart(!autoStartEnabled)); + }; + + return ( + + } + description={} + /> + + + + + + + ); +}; diff --git a/packages/suite/src/views/settings/SettingsDebug/SettingsDebug.tsx b/packages/suite/src/views/settings/SettingsDebug/SettingsDebug.tsx index 4f30e7b1869..c3ae20d2674 100644 --- a/packages/suite/src/views/settings/SettingsDebug/SettingsDebug.tsx +++ b/packages/suite/src/views/settings/SettingsDebug/SettingsDebug.tsx @@ -20,6 +20,7 @@ import { Backends } from './Backends'; import { selectSuiteFlags } from 'src/reducers/suite/suiteReducer'; import { useSelector } from 'src/hooks/suite'; import { PreField } from './PreField'; +import { AutoStart } from './AutoStart'; export const SettingsDebug = () => { const flags = useSelector(selectSuiteFlags); @@ -53,6 +54,11 @@ export const SettingsDebug = () => { + {!isWeb() && ( + + + + )} {!isWeb() && (