diff --git a/src/app/accounts/settings.component.ts b/src/app/accounts/settings.component.ts index 2ca6c019..e390b008 100644 --- a/src/app/accounts/settings.component.ts +++ b/src/app/accounts/settings.component.ts @@ -235,6 +235,7 @@ export class SettingsComponent implements OnInit { this.noAutoPromptBiometrics = false; } this.stateService.setBiometricLocked(false); + console.debug('toggling key'); await this.cryptoService.toggleKey(); } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 6fc10beb..fbc0816a 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -55,6 +55,7 @@ import { PasswordGeneratorComponent } from './vault/password-generator.component import { ModalRef } from 'jslib-angular/components/modal/modal.ref'; import { ModalService } from 'jslib-angular/services/modal.service'; +import { MenuUpdateRequest } from 'src/main/menu.updater'; const BroadcasterSubscriptionId = 'AppComponent'; const IdleTimeout = 60000 * 10; // 10 minutes @@ -147,7 +148,7 @@ export class AppComponent implements OnInit { this.router.navigate(['login']); break; case 'logout': - this.logOut(!!message.expired, message.userId); + await this.logOut(!!message.expired, message.userId); break; case 'lockVault': await this.vaultTimeoutService.lock(true, message.userId); @@ -342,13 +343,13 @@ export class AppComponent implements OnInit { } private async updateAppMenu() { - let data: any; + let updateRequest: MenuUpdateRequest; const stateAccounts = this.stateService.accounts?.getValue(); if (stateAccounts == null || Object.keys(stateAccounts).length < 1) { - data = { + updateRequest = { accounts: null, activeUserId: null, - hideChangeMasterPass: true, + hideChangeMasterPassword: true, }; } else { const accounts: { [userId: string]: any } = {}; @@ -365,15 +366,14 @@ export class AppComponent implements OnInit { }; } } - data = { + updateRequest = { accounts: accounts, activeUserId: await this.stateService.getUserId(), - enableChangeMasterPass: !await this.keyConnectorService.getUsesKeyConnector(), - hideChangeMasterPass: await this.keyConnectorService.getUsesKeyConnector(), + hideChangeMasterPassword: await this.keyConnectorService.getUsesKeyConnector(), }; } - this.messagingService.send('updateAppMenu', data); + this.messagingService.send('updateAppMenu', { updateRequest: updateRequest }); } private async logOut(expired: boolean, userId?: string) { @@ -392,7 +392,11 @@ export class AppComponent implements OnInit { this.keyConnectorService.clear(), ]); - await this.stateService.setBiometricLocked(true); + await this.stateService.clean({ userId: userId }); + + await this.stateService.setBiometricLocked(true, { userId: userId }); + + await this.updateAppMenu(); if (userId === await this.stateService.getUserId()) { this.searchService.clearIndex(); @@ -404,8 +408,6 @@ export class AppComponent implements OnInit { this.router.navigate(['login']); }); } - - await this.stateService.clean({ userId: userId }); } private async recordActivity() { diff --git a/src/app/layout/account-switcher.component.html b/src/app/layout/account-switcher.component.html index 4528f61f..b2bbe3f3 100644 --- a/src/app/layout/account-switcher.component.html +++ b/src/app/layout/account-switcher.component.html @@ -1,4 +1,4 @@ - + {{activeAccountEmail}} diff --git a/src/app/layout/account-switcher.component.ts b/src/app/layout/account-switcher.component.ts index 816bc4ae..43cce94d 100644 --- a/src/app/layout/account-switcher.component.ts +++ b/src/app/layout/account-switcher.component.ts @@ -11,6 +11,7 @@ import { Router } from '@angular/router'; import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { StateService } from 'jslib-common/abstractions/state.service'; import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; +import { AuthenticationStatus } from 'jslib-common/enums/authenticationStatus'; import { Account } from 'jslib-common/models/domain/account'; @@ -34,12 +35,28 @@ export class AccountSwitcherComponent implements OnInit { accounts: { [userId: string]: Account }; activeAccountEmail: string; + get showSwitcher() { + return this.accounts != null && Object.keys(this.accounts).length > 0; + } + constructor(private stateService: StateService, private vaultTimeoutService: VaultTimeoutService, private messagingService: MessagingService, private router: Router) {} async ngOnInit(): Promise { this.stateService.accounts.subscribe(async accounts => { this.accounts = accounts; + + for (const userId in this.accounts) { + if (userId === await this.stateService.getUserId()) { + this.accounts[userId].profile.authenticationStatus = AuthenticationStatus.Active; + continue; + } + + this.accounts[userId].profile.authenticationStatus = await this.vaultTimeoutService.isLocked(userId) ? + AuthenticationStatus.Locked : + AuthenticationStatus.Unlocked; + } + this.activeAccountEmail = await this.stateService.getEmail(); }); } diff --git a/src/locales/menu.help.ts b/src/locales/menu.help.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/main.ts b/src/main.ts index 053f1f3f..799f739d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -75,18 +75,17 @@ export class Main { storageDefaults['global.vaultTimeout'] = -1; storageDefaults['global.vaultTimeoutAction'] = 'lock'; this.storageService = new ElectronStorageService(app.getPath('userData'), storageDefaults); + + // TODO: this state service will have access to on disk storage, but not in memory storage. + // If we could get this to work using the stateService singleton that the rest of the app uses we could save + // ourselves from some hacks, like having to manually update the app menu vs. the menu subscribing to events. this.stateService = new StateService(this.storageService, null, this.logService); this.windowMain = new WindowMain(this.stateService, this.logService, true, undefined, undefined, arg => this.processDeepLink(arg), win => this.trayMain.setupWindowListeners(win)); this.messagingMain = new MessagingMain(this, this.stateService); - this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'desktop', () => { - this.menuMain.updateMenuItem.enabled = false; - }, () => { - this.menuMain.updateMenuItem.enabled = true; - }, () => { - this.menuMain.updateMenuItem.label = this.i18nService.t('restartToUpdate'); - }, 'bitwarden'); + this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain, 'desktop', + null, null, null, 'bitwarden'); this.menuMain = new MenuMain(this); this.powerMonitorMain = new PowerMonitorMain(this); this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.stateService); @@ -98,7 +97,7 @@ export class Main { if (process.platform === 'win32') { const BiometricWindowsMain = require('jslib-electron/biometric.windows.main').default; - this.biometricMain = new BiometricWindowsMain(this.i18nService, this.windowMain, this.storageService); + this.biometricMain = new BiometricWindowsMain(this.i18nService, this.windowMain, this.stateService, this.logService); } else if (process.platform === 'darwin') { const BiometricDarwinMain = require('jslib-electron/biometric.darwin.main').default; this.biometricMain = new BiometricDarwinMain(this.i18nService, this.stateService); diff --git a/src/main/menu.about.ts b/src/main/menu.about.ts new file mode 100644 index 00000000..b2ca3352 --- /dev/null +++ b/src/main/menu.about.ts @@ -0,0 +1,96 @@ +import { IMenubarMenu } from "./menubar"; + +import { + BrowserWindow, + clipboard, + dialog, + MenuItemConstructorOptions, +} from 'electron'; + +import { I18nService } from "jslib-common/abstractions/i18n.service"; + +import { UpdaterMain } from "jslib-electron/updater.main"; +import { isMac, isSnapStore, isWindowsStore } from "jslib-electron/utils"; + +export class AboutMenu implements IMenubarMenu { + private readonly _i18nService: I18nService; + private readonly _updater: UpdaterMain; + private readonly _window: BrowserWindow; + private readonly _version: string; + + readonly id: string = 'about'; + + get visible(): boolean { + return !isMac(); + } + + get label(): string { + return this.localize('about'); + } + + get items(): Array { + return [ + this.separator, + this.checkForUpdates, + this.aboutBitwarden, + ]; + } + + constructor( + i18nService: I18nService, + version: string, + window: BrowserWindow, + updater: UpdaterMain, + ) { + this._i18nService = i18nService; + this._updater = updater; + this._version = version; + this._window = window; + } + + private get separator(): MenuItemConstructorOptions { + return { type: 'separator' }; + } + + private get checkForUpdates(): MenuItemConstructorOptions { + return { + id: 'checkForUpdates', + label: this.localize('checkForUpdates'), + visible: !isWindowsStore() && !isSnapStore(), + click: () => this.checkForUpdate(), + } + } + + private get aboutBitwarden(): MenuItemConstructorOptions { + return { + id: 'aboutBitwarden', + label: this.localize('aboutBitwarden'), + click: async () => { + const aboutInformation = this.localize('version', this._version) + + '\nShell ' + process.versions.electron + + '\nRenderer ' + process.versions.chrome + + '\nNode ' + process.versions.node + + '\nArchitecture ' + process.arch; + const result = await dialog.showMessageBox(this._window, { + title: 'Bitwarden', + message: 'Bitwarden', + detail: aboutInformation, + type: 'info', + noLink: true, + buttons: [this.localize('ok'), this.localize('copy')], + }); + if (result.response === 1) { + clipboard.writeText(aboutInformation); + } + }, + } + } + + private localize(s: string, p?: string) { + return this._i18nService.t(s, p); + } + + private async checkForUpdate() { + this._updater.checkForUpdate(true); + } +} diff --git a/src/main/menu.account.ts b/src/main/menu.account.ts new file mode 100644 index 00000000..f0e08574 --- /dev/null +++ b/src/main/menu.account.ts @@ -0,0 +1,123 @@ +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; + +import { isMacAppStore, isWindowsStore } from 'jslib-electron/utils'; + +import { + IMenubarMenu, +} from "./menubar"; + +import { + BrowserWindow, + dialog, + shell, + MenuItemConstructorOptions, +} from 'electron'; + +export class AccountMenu implements IMenubarMenu { + private readonly _i18nService: I18nService; + private readonly _messagingService: MessagingService; + private readonly _webVaultUrl: string; + private readonly _window: BrowserWindow; + private readonly _isAuthenticated: boolean; + + readonly id: string = 'accountMenu'; + + get label(): string { + return this.localize('account'); + } + + get items(): Array { + return [ + this.premiumMembership, + this.changeMasterPassword, + this.twoStepLogin, + this.fingerprintPhrase, + ]; + } + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + webVaultUrl: string, + window: BrowserWindow, + isAuthenticated: boolean, + ) { + this._i18nService = i18nService; + this._messagingService = messagingService; + this._webVaultUrl = webVaultUrl; + this._window = window; + this._isAuthenticated = isAuthenticated; + } + + private get premiumMembership(): MenuItemConstructorOptions { + return { + label: this.localize('premiumMembership'), + click: () => this.sendMessage('openPremium'), + id: 'premiumMembership', + visible: !isWindowsStore() && !isMacAppStore(), + enabled: this._isAuthenticated, + } + } + + private get changeMasterPassword(): MenuItemConstructorOptions { + return { + label: this.localize('changeMasterPass'), + id: 'changeMasterPass', + click: async () => { + const result = await dialog.showMessageBox(this._window, { + title: this.localize('changeMasterPass'), + message: this.localize('changeMasterPass'), + detail: this.localize('changeMasterPasswordConfirmation'), + buttons: [this.localize('yes'), this.localize('no')], + cancelId: 1, + defaultId: 0, + noLink: true, + }); + if (result.response === 0) { + shell.openExternal(this._webVaultUrl); + } + }, + enabled: this._isAuthenticated, + } + } + + private get twoStepLogin(): MenuItemConstructorOptions { + return { + label: this.localize('twoStepLogin'), + id: 'twoStepLogin', + click: async () => { + const result = await dialog.showMessageBox(this._window, { + title: this.localize('twoStepLogin'), + message: this.localize('twoStepLogin'), + detail: this.localize('twoStepLoginConfirmation'), + buttons: [this.localize('yes'), this.localize('no')], + cancelId: 1, + defaultId: 0, + noLink: true, + }); + if (result.response === 0) { + shell.openExternal(this._webVaultUrl); + } + }, + enabled: this._isAuthenticated, + } + } + + private get fingerprintPhrase(): MenuItemConstructorOptions { + return { + label: this.localize('fingerprintPhrase'), + id: 'fingerprintPhrase', + click: () => this.sendMessage('showFingerprintPhrase'), + enabled: this._isAuthenticated, + } + } + + private localize(s: string) { + return this._i18nService.t(s); + } + + private sendMessage(message: string, args?: any) { + this._messagingService.send(message, args); + } +} diff --git a/src/main/menu.bitwarden.ts b/src/main/menu.bitwarden.ts new file mode 100644 index 00000000..e17acbe2 --- /dev/null +++ b/src/main/menu.bitwarden.ts @@ -0,0 +1,241 @@ +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; + +import { UpdaterMain } from "jslib-electron/updater.main"; +import { isMacAppStore, isSnapStore, isWindowsStore } from "jslib-electron/utils"; + +import { IMenubarMenu } from "./menubar"; + +import { + dialog, + BrowserWindow, + MenuItem, + MenuItemConstructorOptions, +} from 'electron'; + +import { MenuAccount } from "./menu.updater"; + +// AKA: "FirstMenu" or "MacMenu" - the first menu that shows on all macOs apps +export class BitwardenMenu implements IMenubarMenu { + private readonly _i18nService: I18nService; + private readonly _updater: UpdaterMain; + private readonly _messagingService: MessagingService; + private readonly _accounts: { [userId: string]: MenuAccount } + private readonly _window: BrowserWindow; + + readonly id: string = "bitwarden"; + readonly label: string = "Bitwarden"; + get items(): Array { + console.debug('getting bitwarden menu items', { + accounts: this._accounts, + lock: this.lock.submenu, + logout: this.logOut.submenu, + }); + return [ + this.aboutBitwarden, + this.checkForUpdates, + this.separator, + this.settings, + this.lock, + this.lockAll, + this.logOut, + this.services, + this.separator, + this.hideBitwarden, + this.hideOthers, + this.showAll, + this.separator, + this.quitBitwarden, + ] + } + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + updater: UpdaterMain, + window: BrowserWindow, + accounts: { [userId: string]: MenuAccount }, + ) { + this._i18nService = i18nService; + this._updater = updater; + this._messagingService = messagingService; + this._window = window; + this._accounts = accounts; + } + + private get hasAccounts(): boolean { + return this._accounts != null && Object.keys(this._accounts).length > 0; + } + + private get aboutBitwarden(): MenuItemConstructorOptions { + return { + id: 'aboutBitwarden', + label: this.localize('aboutBitwarden'), + role: 'about', + visible: isMacAppStore(), + } + } + + private get checkForUpdates(): MenuItemConstructorOptions { + return { + id: 'checkForUpdates', + label: this.localize('checkForUpdates'), + click: (menuItem) => this.checkForUpdate(menuItem), + visible: !isMacAppStore() && !isWindowsStore() && !isSnapStore(), + } + } + + private get separator(): MenuItemConstructorOptions { + return { + type: 'separator', + } + } + + private get settings(): MenuItemConstructorOptions { + return { + id: 'settings', + label: this.localize(process.platform === 'darwin' ? + 'preferences' : + 'settings' + ), + click: () => this.sendMessage('openSettings'), + accelerator: 'CmdOrCtrl+,', + } + } + + private get lock(): MenuItemConstructorOptions { + return { + id: 'lock', + label: this.localize('lockVault'), + submenu: this.lockSubmenu, + enabled: this.hasAccounts, + } + } + + private get lockSubmenu(): Array { + const value: Array = []; + for(let userId in this._accounts) { + if (userId == null) { + continue; + } + + value.push({ + label: this._accounts[userId].email, + id: `lockNow_${this._accounts[userId].userId}`, + click: () => this.sendMessage('lockVault', { userId: this._accounts[userId].userId }), + enabled: !this._accounts[userId].isLocked, + visible: this._accounts[userId].isAuthenticated + }) + } + return value; + } + + private get lockAll(): MenuItemConstructorOptions { + return { + id: 'lockAllNow', + label: this.localize('lockAllVaults'), + click: () => this.sendMessage('lockAllVaults'), + accelerator: 'CmdOrCtrl+L', + enabled: this.hasAccounts, + } + } + + private get logOut(): MenuItemConstructorOptions { + return { + id: 'logOut', + label: this.localize('logOut'), + submenu: this.logOutSubmenu, + enabled: this.hasAccounts, + } + } + + private get logOutSubmenu(): Array { + const value: Array = []; + for(let userId in this._accounts) { + if (userId == null) { + continue; + } + + value.push({ + label: this._accounts[userId].email, + id: `logOut_${this._accounts[userId].userId}`, + click: async () => { + const result = await dialog.showMessageBox(this._window, { + title: this.localize('logOut'), + message: this.localize('logOut'), + detail: this.localize('logOutConfirmation'), + buttons: [this.localize('logOut'), this.localize('cancel')], + cancelId: 1, + defaultId: 0, + noLink: true, + }); + if (result.response === 0) { + this.sendMessage('logout', { userId: this._accounts[userId].userId }); + } + }, + visible: this._accounts[userId].isAuthenticated + }) + } + return value; + } + + private get services(): MenuItemConstructorOptions { + return { + id: 'services', + label: this.localize('services'), + role: 'services', + submenu: [], + visible: isMacAppStore(), + } + } + + private get hideBitwarden(): MenuItemConstructorOptions { + return { + id: 'hideBitwarden', + label: this.localize('hideBitwarden'), + role: 'hide', + visible: isMacAppStore(), + } + } + + private get hideOthers(): MenuItemConstructorOptions { + return { + id: 'hideOthers', + label: this.localize('hideOthers'), + role: 'hideOthers', + visible: isMacAppStore(), + } + } + + private get showAll(): MenuItemConstructorOptions { + return { + id: 'showAll', + label: this.localize('showAll'), + role: 'unhide', + visible: isMacAppStore(), + } + } + + private get quitBitwarden(): MenuItemConstructorOptions { + return { + id: 'quitBitwarden', + label: this.localize('quitBitwarden'), + role: 'quit', + visible: isMacAppStore(), + } + } + + private localize(s: string) { + return this._i18nService.t(s); + } + + private async checkForUpdate(menuItem: MenuItem) { + menuItem.enabled = false; + this._updater.checkForUpdate(true); + menuItem.enabled = true; + } + + private sendMessage(message: string, args?: any) { + this._messagingService.send(message, args); + } +} diff --git a/src/main/menu.edit.ts b/src/main/menu.edit.ts new file mode 100644 index 00000000..f1aaa61c --- /dev/null +++ b/src/main/menu.edit.ts @@ -0,0 +1,134 @@ +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; + +import { IMenubarMenu } from "./menubar"; + +import { MenuItemConstructorOptions } from 'electron'; + +export class EditMenu implements IMenubarMenu { + private readonly _i18nService: I18nService; + private readonly _messagingService: MessagingService; + private readonly _isAuthenticated: boolean; + + readonly id: string = 'editMenu'; + + get label(): string { + return this.localize('edit'); + } + + get items(): Array { + return [ + this.undo, + this.redo, + this.separator, + this.cut, + this.copy, + this.paste, + this.separator, + this.selectAll, + this.separator, + this.copyUsername, + this.copyPassword, + this.copyVerificationCodeTotp, + ]; + } + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + isAuthenticated: boolean, + ) { + this._i18nService = i18nService; + this._messagingService = messagingService; + this._isAuthenticated = isAuthenticated; + } + + private get undo(): MenuItemConstructorOptions { + return { + id: 'undo', + label: this.localize('undo'), + role: 'undo', + }; + } + + private get redo(): MenuItemConstructorOptions { + return { + id: 'redo', + label: this.localize('redo'), + role: 'redo', + }; + } + + private get separator(): MenuItemConstructorOptions { + return { type: 'separator' }; + } + + private get cut(): MenuItemConstructorOptions { + return { + id: 'cut', + label: this.localize('cut'), + role: 'cut', + }; + } + + private get copy(): MenuItemConstructorOptions { + return { + id: 'copy', + label: this.localize('copy'), + role: 'copy', + }; + } + + private get paste(): MenuItemConstructorOptions { + return { + id: 'paste', + label: this.localize('paste'), + role: 'paste', + }; + } + + private get selectAll(): MenuItemConstructorOptions { + return { + id: 'selectAll', + label: this.localize('selectAll'), + role: 'selectAll', + }; + } + + private get copyUsername(): MenuItemConstructorOptions { + return { + label: this.localize('copyUsername'), + id: 'copyUsername', + click: () => this.sendMessage('copyUsername'), + accelerator: 'CmdOrCtrl+U', + enabled: this._isAuthenticated, + }; + } + + private get copyPassword(): MenuItemConstructorOptions { + return { + label: this.localize('copyPassword'), + id: 'copyPassword', + click: () => this.sendMessage('copyPassword'), + accelerator: 'CmdOrCtrl+P', + enabled: this._isAuthenticated, + }; + } + + private get copyVerificationCodeTotp(): MenuItemConstructorOptions { + return { + label: this.localize('copyVerificationCodeTotp'), + id: 'copyTotp', + click: () => this.sendMessage('copyTotp'), + accelerator: 'CmdOrCtrl+T', + }; + } + + private localize(s: string) { + return this._i18nService.t(s); + } + + private sendMessage(message: string) { + this._messagingService.send(message); + } +} diff --git a/src/main/menu.file.ts b/src/main/menu.file.ts new file mode 100644 index 00000000..5b4f4697 --- /dev/null +++ b/src/main/menu.file.ts @@ -0,0 +1,134 @@ +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; + +import { isMacAppStore } from "jslib-electron/utils"; + +import { IMenubarMenu } from "./menubar"; + +import { MenuItemConstructorOptions } from 'electron'; + +export class FileMenu implements IMenubarMenu { + private readonly _i18nService: I18nService; + private readonly _messagingService: MessagingService; + private readonly _isAuthenticated: boolean; + + readonly id: string = 'fileMenu'; + + get label(): string { + return this.localize('file'); + } + + get items(): Array { + return [ + this.addNewLogin, + this.addNewItem, + this.addNewFolder, + this.separator, + this.syncVault, + this.exportVault, + this.quitBitwarden, + ]; + } + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + isAuthenticated: boolean, + ) { + this._i18nService = i18nService; + this._messagingService = messagingService; + this._isAuthenticated = isAuthenticated + } + + private get addNewLogin(): MenuItemConstructorOptions { + return { + label: this.localize('addNewLogin'), + click: () => this.sendMessage('newLogin'), + accelerator: 'CmdOrCtrl+N', + id: 'addNewLogin', + }; + } + + private get addNewItem(): MenuItemConstructorOptions { + return { + label: this.localize('addNewItem'), + id: 'addNewItem', + submenu: this.addNewItemSubmenu, + enabled: this._isAuthenticated, + }; + } + + private get addNewItemSubmenu(): Array { + return [ + { + id: 'typeLogin', + label: this.localize('typeLogin'), + click: () => this.sendMessage('newLogin'), + accelerator: 'CmdOrCtrl+Shift+L', + }, + { + id: 'typeCard', + label: this.localize('typeCard'), + click: () => this.sendMessage('newCard'), + accelerator: 'CmdOrCtrl+Shift+C', + }, + { + id: 'typeIdentity', + label: this.localize('typeIdentity'), + click: () => this.sendMessage('newIdentity'), + accelerator: 'CmdOrCtrl+Shift+I', + }, + { + id: 'typeSecureNote', + label: this.localize('typeSecureNote'), + click: () => this.sendMessage('newSecureNote'), + accelerator: 'CmdOrCtrl+Shift+S', + }, + ] + } + + private get addNewFolder(): MenuItemConstructorOptions { + return { + id: 'addNewFolder', + label: this.localize('addNewFolder'), + click: () => this.sendMessage('newFolder'), + }; + } + + private get separator(): MenuItemConstructorOptions { + return { type: 'separator' }; + } + + private get syncVault(): MenuItemConstructorOptions { + return { + id: 'syncVault', + label: this.localize('syncVault'), + click: () => this.sendMessage('syncVault'), + }; + } + + private get exportVault(): MenuItemConstructorOptions { + return { + id: 'exportVault', + label: this.localize('exportVault'), + click: () => this.sendMessage('exportVault'), + }; + } + + private get quitBitwarden(): MenuItemConstructorOptions { + return { + id: 'quitBitwarden', + label: this.localize('quitBitwarden'), + visible: !isMacAppStore(), + role: 'quit', + }; + } + + private localize(s: string) { + return this._i18nService.t(s); + } + + private sendMessage(message: string) { + this._messagingService.send(message); + } +} diff --git a/src/main/menu.help.ts b/src/main/menu.help.ts new file mode 100644 index 00000000..a032d087 --- /dev/null +++ b/src/main/menu.help.ts @@ -0,0 +1,223 @@ +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { IMenubarMenu } from "./menubar"; + +import { shell } from 'electron'; + +import { isMacAppStore, isWindowsStore } from 'jslib-electron/utils'; + +import { MenuItemConstructorOptions } from 'electron'; + +export class HelpMenu implements IMenubarMenu { + private readonly _i18nService: I18nService; + private readonly _webVaultUrl: string; + + readonly id: string = 'help'; + + get label(): string { + return this.localize('help'); + } + + get items(): Array { + return [ + this.emailUs, + this.visitOurWebsite, + this.fileBugReport, + this.legal, + this.separator, + this.followUs, + this.separator, + this.goToWebVault, + this.separator, + this.getMobileApp, + this.getBrowserExtension, + ]; + } + + constructor( + i18nService: I18nService, + webVaultUrl: string + ) { + this._i18nService = i18nService; + this._webVaultUrl = webVaultUrl; + } + + private get emailUs(): MenuItemConstructorOptions { + return { + id: 'emailUs', + label: this.localize('emailUs'), + click: () => shell.openExternal('mailTo:hello@bitwarden.com'), + }; + } + + private get visitOurWebsite(): MenuItemConstructorOptions { + return { + id: 'visitOurWebsite', + label: this.localize('visitOurWebsite'), + click: () => shell.openExternal('https://bitwarden.com/contact'), + }; + } + + private get fileBugReport(): MenuItemConstructorOptions { + return { + id: 'fileBugReport', + label: this.localize('fileBugReport'), + click: () => shell.openExternal('https://github.com/bitwarden/desktop/issues'), + }; + } + + private get legal(): MenuItemConstructorOptions { + return { + id: 'legal', + label: this.localize('legal'), + visible: !isMacAppStore(), + submenu: this.legalSubmenu, + }; + } + + private get legalSubmenu(): Array { + return [ + { + id: 'termsOfService', + label: this.localize('termsOfService'), + click: () => shell.openExternal('https://bitwarden.com/terms/'), + }, + { + id: 'privacyPolicy', + label: this.localize('privacyPolicy'), + click: () => shell.openExternal('https://bitwarden.com/privacy/'), + }, + ]; + } + + private get separator(): MenuItemConstructorOptions { + return { type: 'separator' }; + } + + private get followUs(): MenuItemConstructorOptions { + return { + id: 'followUs', + label: this.localize('followUs'), + submenu: this.followUsSubmenu, + }; + } + + private get followUsSubmenu(): Array { + return [ + { + id: 'blog', + label: this.localize('blog'), + click: () => shell.openExternal('https://blog.bitwarden.com'), + }, + { + id: 'twitter', + label: 'Twitter', + click: () => shell.openExternal('https://twitter.com/bitwarden'), + }, + { + id: 'facebook', + label: 'Facebook', + click: () => shell.openExternal('https://www.facebook.com/bitwarden/'), + }, + { + id: 'github', + label: 'GitHub', + click: () => shell.openExternal('https://github.com/bitwarden'), + }, + ]; + } + + private get goToWebVault(): MenuItemConstructorOptions { + return { + id: 'goToWebVault', + label: this.localize('goToWebVault'), + click: () => shell.openExternal(this._webVaultUrl), + }; + } + + private get getMobileApp(): MenuItemConstructorOptions { + return { + id: 'getMobileApp', + label: this.localize('getMobileApp'), + visible: !isWindowsStore(), + submenu: this.getMobileAppSubmenu, + }; + } + + private get getMobileAppSubmenu(): Array { + return [ + { + id: 'iOS', + label: 'iOS', + click: () => { + shell.openExternal('https://itunes.apple.com/app/' + + 'bitwarden-free-password-manager/id1137397744?mt=8'); + }, + }, + { + id: 'android', + label: 'Android', + click: () => { + shell.openExternal('https://play.google.com/store/apps/' + + 'details?id=com.x8bit.bitwarden'); + }, + }, + ]; + } + + private get getBrowserExtension(): MenuItemConstructorOptions { + return { + id: 'getBrowserExtension', + label: this.localize('getBrowserExtension'), + visible: !isWindowsStore(), + submenu: this.getBrowserExtensionSubmenu, + }; + } + + private get getBrowserExtensionSubmenu(): Array { + return [ + { + id: 'chrome', + label: 'Chrome', + click: () => { + shell.openExternal('https://chrome.google.com/webstore/detail/' + + 'bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb'); + }, + }, + { + id: 'firefox', + label: 'Firefox', + click: () => { + shell.openExternal('https://addons.mozilla.org/firefox/addon/' + + 'bitwarden-password-manager/'); + }, + }, + { + id: 'firefox', + label: 'Opera', + click: () => { + shell.openExternal('https://addons.opera.com/extensions/details/' + + 'bitwarden-free-password-manager/'); + }, + }, + { + id: 'firefox', + label: 'Edge', + click: () => { + shell.openExternal('https://microsoftedge.microsoft.com/addons/' + + 'detail/jbkfoedolllekgbhcbcoahefnbanhhlh'); + }, + }, + { + id: 'safari', + label: 'Safari', + click: () => { + shell.openExternal('https://bitwarden.com/download/'); + }, + }, + ]; + } + + private localize(s: string) { + return this._i18nService.t(s); + } +} diff --git a/src/main/menu.main.ts b/src/main/menu.main.ts index 5d9a9319..32e7d824 100644 --- a/src/main/menu.main.ts +++ b/src/main/menu.main.ts @@ -1,569 +1,54 @@ import { app, - clipboard, - dialog, Menu, - MenuItem, - MenuItemConstructorOptions, - shell, } from 'electron'; import { Main } from '../main'; import { BaseMenu } from 'jslib-electron/baseMenu'; -import { isMacAppStore, isSnapStore, isWindowsStore } from 'jslib-electron/utils'; +import { MenuUpdateRequest } from './menu.updater'; +import { Menubar } from './menubar'; + +const cloudWebVaultUrl: string = "https://vault.bitwarden.com"; export class MenuMain extends BaseMenu { - menu: Menu; - updateMenuItem: MenuItem; - addNewLogin: MenuItem; - addNewItem: MenuItem; - addNewFolder: MenuItem; - syncVault: MenuItem; - exportVault: MenuItem; - settings: MenuItem; - lockNow: MenuItem; - logOut: MenuItem; - twoStepLogin: MenuItem; - fingerprintPhrase: MenuItem; - changeMasterPass: MenuItem; - premiumMembership: MenuItem; - passwordGenerator: MenuItem; - passwordHistory: MenuItem; - searchVault: MenuItem; - copyUsername: MenuItem; - copyPassword: MenuItem; - copyTotp: MenuItem; - unlockedRequiredMenuItems: MenuItem[] = []; - constructor(private main: Main) { super(main.i18nService, main.windowMain); } - init() { - this.initProperties(); + async init() { this.initContextMenu(); - this.initApplicationMenu(); - - this.updateMenuItem = this.menu.getMenuItemById('checkForUpdates'); - this.addNewLogin = this.menu.getMenuItemById('addNewLogin'); - this.addNewItem = this.menu.getMenuItemById('addNewItem'); - this.addNewFolder = this.menu.getMenuItemById('addNewFolder'); - this.syncVault = this.menu.getMenuItemById('syncVault'); - this.exportVault = this.menu.getMenuItemById('exportVault'); - this.settings = this.menu.getMenuItemById('settings'); - this.lockNow = this.menu.getMenuItemById('lockNow'); - this.logOut = this.menu.getMenuItemById('logOut'); - this.twoStepLogin = this.menu.getMenuItemById('twoStepLogin'); - this.fingerprintPhrase = this.menu.getMenuItemById('fingerprintPhrase'); - this.changeMasterPass = this.menu.getMenuItemById('changeMasterPass'); - this.premiumMembership = this.menu.getMenuItemById('premiumMembership'); - this.passwordGenerator = this.menu.getMenuItemById('passwordGenerator'); - this.passwordHistory = this.menu.getMenuItemById('passwordHistory'); - this.searchVault = this.menu.getMenuItemById('searchVault'); - this.copyUsername = this.menu.getMenuItemById('copyUsername'); - this.copyPassword = this.menu.getMenuItemById('copyPassword'); - this.copyTotp = this.menu.getMenuItemById('copyTotp'); - - this.unlockedRequiredMenuItems = [ - this.addNewLogin, this.addNewItem, this.addNewFolder, - this.syncVault, this.exportVault, this.settings, this.twoStepLogin, this.fingerprintPhrase, - this.changeMasterPass, this.premiumMembership, this.passwordGenerator, this.passwordHistory, - this.searchVault, this.copyUsername, this.copyPassword]; - this.updateApplicationMenuState(true); + await this.setMenu(); } - updateApplicationMenuState(hideChangeMasterPass: boolean, accounts?: { [userId: string]: { isAuthenticated: boolean, isLocked: boolean, userId: string, email: string }}, activeUserId?: string) { - this.updateAuthBasedMenuState(accounts, activeUserId); - if (hideChangeMasterPass) { - this.changeMasterPass.visible = !(hideChangeMasterPass ?? false); - } - if (this.menu != null) { - Menu.setApplicationMenu(this.menu); - } + async updateApplicationMenuState(updateRequest: MenuUpdateRequest) { + await this.setMenu(updateRequest); } - private updateAuthBasedMenuState(accounts?: {[userId: string]: { isAuthenticated: boolean, isLocked: boolean, userId: string, email: string}}, activeUserId?: string) { - accounts == null ? - this.lockAuthBasedMenuItems() : - this.tryUnlockAuthBasedMenuItems(accounts, activeUserId); + private async setMenu(updateRequest?: MenuUpdateRequest) { + Menu.setApplicationMenu(new Menubar( + this.main.i18nService, + this.main.messagingService, + this.main.updaterMain, + this.windowMain, + await this.getWebVaultUrl(), + app.getVersion(), + updateRequest, + ).menu); } - private lockAuthBasedMenuItems() { - this.logOut.enabled = false; - this.lockNow.enabled = false; - this.unlockedRequiredMenuItems.forEach((mi: MenuItem) => { - if (mi != null) { - mi.enabled = false; - } - }); - } - - private tryUnlockAuthBasedMenuItems(accounts: { [userId: string]: { isAuthenticated: boolean, isLocked: boolean, userId: string, email: string} }, activeUserId: string) { - this.tryUnlockActiveAccountAuthBasedMenuItems(accounts[activeUserId]); - - this.lockNow.enabled = true; - this.logOut.enabled = true; - for (const userId in accounts) { - if (userId != null) { - if (this.lockNow.submenu.getMenuItemById(`lockNow_${accounts[userId].userId}`) == null) { - const options: MenuItemConstructorOptions = { - label: accounts[userId].email, - id: `lockNow_${accounts[userId].userId}`, - click: () => this.main.messagingService.send('lockVault', { userId: accounts[userId].userId }), - }; - this.lockNow.submenu.insert(0, new MenuItem(options)); - } - if (this.logOut.submenu.getMenuItemById(`logOut_${accounts[userId].userId}`) == null) { - const options: MenuItemConstructorOptions = { - label: accounts[userId].email, - id: `logOut_${accounts[userId].userId}`, - click: async () => { - const result = await dialog.showMessageBox(this.windowMain.win, { - title: this.i18nService.t('logOut'), - message: this.i18nService.t('logOut'), - detail: this.i18nService.t('logOutConfirmation'), - buttons: [this.i18nService.t('logOut'), this.i18nService.t('cancel')], - cancelId: 1, - defaultId: 0, - noLink: true, - }); - if (result.response === 0) { - this.main.messagingService.send('logout', { userId: accounts[userId].userId }); - } - }, - }; - this.logOut.submenu.insert(0, new MenuItem(options)); - } - } - } - } - - private tryUnlockActiveAccountAuthBasedMenuItems(activeAccount: { isAuthenticated: boolean, isLocked: boolean, userId: string, email: string}) { - this.logOut.enabled = activeAccount.isAuthenticated; - this.unlockedRequiredMenuItems.forEach((mi: MenuItem) => { - if (mi != null) { - mi.enabled = activeAccount.isAuthenticated && !activeAccount.isLocked; - } - }); - } - - private initApplicationMenu() { - const accountSubmenu: MenuItemConstructorOptions[] = [ - { - label: this.main.i18nService.t('changeMasterPass'), - id: 'changeMasterPass', - click: async () => { - const result = await dialog.showMessageBox(this.main.windowMain.win, { - title: this.main.i18nService.t('changeMasterPass'), - message: this.main.i18nService.t('changeMasterPass'), - detail: this.main.i18nService.t('changeMasterPasswordConfirmation'), - buttons: [this.main.i18nService.t('yes'), this.main.i18nService.t('no')], - cancelId: 1, - defaultId: 0, - noLink: true, - }); - if (result.response === 0) { - await this.openWebVault(); - } - }, - }, - { - label: this.main.i18nService.t('twoStepLogin'), - id: 'twoStepLogin', - click: async () => { - const result = await dialog.showMessageBox(this.main.windowMain.win, { - title: this.main.i18nService.t('twoStepLogin'), - message: this.main.i18nService.t('twoStepLogin'), - detail: this.main.i18nService.t('twoStepLoginConfirmation'), - buttons: [this.main.i18nService.t('yes'), this.main.i18nService.t('no')], - cancelId: 1, - defaultId: 0, - noLink: true, - }); - if (result.response === 0) { - await this.openWebVault(); - } - }, - }, - { - label: this.main.i18nService.t('fingerprintPhrase'), - id: 'fingerprintPhrase', - click: () => this.main.messagingService.send('showFingerprintPhrase'), - }, - ]; - - this.editMenuItemOptions.submenu = (this.editMenuItemOptions.submenu as MenuItemConstructorOptions[]).concat([ - { type: 'separator' }, - { - label: this.main.i18nService.t('copyUsername'), - id: 'copyUsername', - click: () => this.main.messagingService.send('copyUsername'), - accelerator: 'CmdOrCtrl+U', - }, - { - label: this.main.i18nService.t('copyPassword'), - id: 'copyPassword', - click: () => this.main.messagingService.send('copyPassword'), - accelerator: 'CmdOrCtrl+P', - }, - { - label: this.main.i18nService.t('copyVerificationCodeTotp'), - id: 'copyTotp', - click: () => this.main.messagingService.send('copyTotp'), - accelerator: 'CmdOrCtrl+T', - }, - ]); - - if (!isWindowsStore() && !isMacAppStore()) { - accountSubmenu.unshift({ - label: this.main.i18nService.t('premiumMembership'), - click: () => this.main.messagingService.send('openPremium'), - id: 'premiumMembership', - }); - } - - let helpSubmenu: MenuItemConstructorOptions[] = [ - { - label: this.main.i18nService.t('emailUs'), - click: () => shell.openExternal('mailTo:hello@bitwarden.com'), - }, - { - label: this.main.i18nService.t('visitOurWebsite'), - click: () => shell.openExternal('https://bitwarden.com/contact'), - }, - { - label: this.main.i18nService.t('fileBugReport'), - click: () => shell.openExternal('https://github.com/bitwarden/desktop/issues'), - }, - ]; - - if (isMacAppStore()) { - helpSubmenu.push({ - label: this.main.i18nService.t('legal'), - submenu: [ - { - label: this.main.i18nService.t('termsOfService'), - click: () => shell.openExternal('https://bitwarden.com/terms/'), - }, - { - label: this.main.i18nService.t('privacyPolicy'), - click: () => shell.openExternal('https://bitwarden.com/privacy/'), - }, - ], - }); - } - - helpSubmenu = helpSubmenu.concat([ - { type: 'separator' }, - { - label: this.main.i18nService.t('followUs'), - submenu: [ - { - label: this.main.i18nService.t('blog'), - click: () => shell.openExternal('https://blog.bitwarden.com'), - }, - { - label: 'Twitter', - click: () => shell.openExternal('https://twitter.com/bitwarden'), - }, - { - label: 'Facebook', - click: () => shell.openExternal('https://www.facebook.com/bitwarden/'), - }, - { - label: 'GitHub', - click: () => shell.openExternal('https://github.com/bitwarden'), - }, - ], - }, - { type: 'separator' }, - { - label: this.main.i18nService.t('goToWebVault'), - click: async () => await this.openWebVault(), - }, - ]); - - if (!isWindowsStore()) { - helpSubmenu.push({ - label: this.main.i18nService.t('getMobileApp'), - submenu: [ - { - label: 'iOS', - click: () => { - shell.openExternal('https://itunes.apple.com/app/' + - 'bitwarden-free-password-manager/id1137397744?mt=8'); - }, - }, - { - label: 'Android', - click: () => { - shell.openExternal('https://play.google.com/store/apps/' + - 'details?id=com.x8bit.bitwarden'); - }, - }, - ], - }); - helpSubmenu.push({ - label: this.main.i18nService.t('getBrowserExtension'), - submenu: [ - { - label: 'Chrome', - click: () => { - shell.openExternal('https://chrome.google.com/webstore/detail/' + - 'bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb'); - }, - }, - { - label: 'Firefox', - click: () => { - shell.openExternal('https://addons.mozilla.org/firefox/addon/' + - 'bitwarden-password-manager/'); - }, - }, - { - label: 'Opera', - click: () => { - shell.openExternal('https://addons.opera.com/extensions/details/' + - 'bitwarden-free-password-manager/'); - }, - }, - { - label: 'Edge', - click: () => { - shell.openExternal('https://microsoftedge.microsoft.com/addons/' + - 'detail/jbkfoedolllekgbhcbcoahefnbanhhlh'); - }, - }, - { - label: 'Safari', - click: () => { - shell.openExternal('https://bitwarden.com/download/'); - }, - }, - ], - }); - } - - const template: MenuItemConstructorOptions[] = [ - { - label: this.main.i18nService.t('file'), - submenu: [ - { - label: this.main.i18nService.t('addNewLogin'), - click: () => this.main.messagingService.send('newLogin'), - accelerator: 'CmdOrCtrl+N', - id: 'addNewLogin', - }, - { - label: this.main.i18nService.t('addNewItem'), - id: 'addNewItem', - submenu: [ - { - label: this.main.i18nService.t('typeLogin'), - click: () => this.main.messagingService.send('newLogin'), - accelerator: 'CmdOrCtrl+Shift+L', - }, - { - label: this.main.i18nService.t('typeCard'), - click: () => this.main.messagingService.send('newCard'), - accelerator: 'CmdOrCtrl+Shift+C', - }, - { - label: this.main.i18nService.t('typeIdentity'), - click: () => this.main.messagingService.send('newIdentity'), - accelerator: 'CmdOrCtrl+Shift+I', - }, - { - label: this.main.i18nService.t('typeSecureNote'), - click: () => this.main.messagingService.send('newSecureNote'), - accelerator: 'CmdOrCtrl+Shift+S', - }, - ], - }, - { - label: this.main.i18nService.t('addNewFolder'), - id: 'addNewFolder', - click: () => this.main.messagingService.send('newFolder'), - }, - { type: 'separator' }, - { - label: this.main.i18nService.t('syncVault'), - id: 'syncVault', - click: () => this.main.messagingService.send('syncVault'), - }, - { - label: this.main.i18nService.t('exportVault'), - id: 'exportVault', - click: () => this.main.messagingService.send('exportVault'), - }, - ], - }, - this.editMenuItemOptions, - { - label: this.main.i18nService.t('view'), - submenu: ([ - { - label: this.main.i18nService.t('searchVault'), - id: 'searchVault', - click: () => this.main.messagingService.send('focusSearch'), - accelerator: 'CmdOrCtrl+F', - }, - { type: 'separator' }, - { - label: this.main.i18nService.t('passwordGenerator'), - id: 'passwordGenerator', - click: () => this.main.messagingService.send('openPasswordGenerator'), - accelerator: 'CmdOrCtrl+G', - }, - { - label: this.main.i18nService.t('passwordHistory'), - id: 'passwordHistory', - click: () => this.main.messagingService.send('openPasswordHistory'), - }, - { type: 'separator' }, - ] as MenuItemConstructorOptions[]).concat(this.viewSubMenuItemOptions), - }, - { - label: this.main.i18nService.t('account'), - submenu: accountSubmenu, - }, - this.windowMenuItemOptions, - { - label: this.main.i18nService.t('help'), - role: 'help', - submenu: helpSubmenu, - }, - ]; - - const firstMenuOptions: MenuItemConstructorOptions[] = [ - { type: 'separator' }, - { - label: this.main.i18nService.t(process.platform === 'darwin' ? 'preferences' : 'settings'), - id: 'settings', - click: () => this.main.messagingService.send('openSettings'), - accelerator: 'CmdOrCtrl+,', - }, - { - label: this.main.i18nService.t('lockVault'), - id: 'lockNow', - submenu: [ - // List of vaults - ], - }, - { - label: this.main.i18nService.t('lockAllVaults'), - click: () => this.main.messagingService.send('lockAllVaults'), - id: 'lockNow', - accelerator: 'CmdOrCtrl+L', - }, - { - label: this.main.i18nService.t('logOut'), - id: 'logOut', - submenu: [ - // List of vaults - ], - }, - ]; - - const updateMenuItem = { - label: this.main.i18nService.t('checkForUpdates'), - click: () => this.main.updaterMain.checkForUpdate(true), - id: 'checkForUpdates', - }; - - if (process.platform === 'darwin') { - const firstMenuPart: MenuItemConstructorOptions[] = [ - { - label: this.main.i18nService.t('aboutBitwarden'), - role: 'about', - }, - ]; - - if (!isMacAppStore()) { - firstMenuPart.push(updateMenuItem); - } - - template.unshift({ - label: 'Bitwarden', - submenu: firstMenuPart.concat(firstMenuOptions, [ - { type: 'separator' }, - ], this.macAppMenuItemOptions), - }); - - // Window menu - template[template.length - 2].submenu = this.macWindowSubmenuOptions; - } else { - // File menu - template[0].submenu = (template[0].submenu as MenuItemConstructorOptions[]).concat( - firstMenuOptions, { - label: this.i18nService.t('quitBitwarden'), - role: 'quit', - }); - - // About menu - const aboutMenuAdditions: MenuItemConstructorOptions[] = [ - { type: 'separator' }, - ]; - - if (!isWindowsStore() && !isSnapStore()) { - aboutMenuAdditions.push(updateMenuItem); - } - - aboutMenuAdditions.push({ - label: this.i18nService.t('aboutBitwarden'), - click: async () => { - const aboutInformation = this.i18nService.t('version', app.getVersion()) + - '\nShell ' + process.versions.electron + - '\nRenderer ' + process.versions.chrome + - '\nNode ' + process.versions.node + - '\nArchitecture ' + process.arch; - const result = await dialog.showMessageBox(this.windowMain.win, { - title: 'Bitwarden', - message: 'Bitwarden', - detail: aboutInformation, - type: 'info', - noLink: true, - buttons: [this.i18nService.t('ok'), this.i18nService.t('copy')], - }); - if (result.response === 1) { - clipboard.writeText(aboutInformation); - } - }, - }); - - template[template.length - 1].submenu = - (template[template.length - 1].submenu as MenuItemConstructorOptions[]).concat(aboutMenuAdditions); - } - - (template[template.length - 2].submenu as MenuItemConstructorOptions[]).splice(1, 0, - { - label: this.main.i18nService.t(process.platform === 'darwin' ? 'hideToMenuBar' : 'hideToTray'), - click: () => this.main.messagingService.send('hideToTray'), - accelerator: 'CmdOrCtrl+Shift+M', - }, - { - type: 'checkbox', - label: this.main.i18nService.t('alwaysOnTop'), - checked: this.windowMain.win.isAlwaysOnTop(), - click: () => this.main.windowMain.toggleAlwaysOnTop(), - accelerator: 'CmdOrCtrl+Shift+T', - }); - this.menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(this.menu); - } - - private async openWebVault() { - let webUrl = 'https://vault.bitwarden.com'; + private async getWebVaultUrl() { + let webVaultUrl = cloudWebVaultUrl; const urlsObj: any = await this.main.stateService.getEnvironmentUrls(); if (urlsObj != null) { if (urlsObj.base != null) { - webUrl = urlsObj.base; + webVaultUrl = urlsObj.base; } else if (urlsObj.webVault != null) { - webUrl = urlsObj.webVault; + webVaultUrl = urlsObj.webVault; } } - shell.openExternal(webUrl); + return webVaultUrl; } + } diff --git a/src/main/menu.updater.ts b/src/main/menu.updater.ts new file mode 100644 index 00000000..9483dbcc --- /dev/null +++ b/src/main/menu.updater.ts @@ -0,0 +1,12 @@ +export class MenuUpdateRequest { + hideChangeMasterPassword: boolean; + activeUserId: string; + accounts: { [userId: string]: MenuAccount } +} + +export class MenuAccount { + isAuthenticated: boolean; + isLocked: boolean; + userId: string; + email: string; +} diff --git a/src/main/menu.view.ts b/src/main/menu.view.ts new file mode 100644 index 00000000..05871e1d --- /dev/null +++ b/src/main/menu.view.ts @@ -0,0 +1,140 @@ +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; + +import { IMenubarMenu } from "./menubar"; + +import { MenuItemConstructorOptions } from 'electron'; + +export class ViewMenu implements IMenubarMenu { + private readonly _i18nService: I18nService; + private readonly _messagingService: MessagingService; + private readonly _isAuthenticated: boolean; + + readonly id: 'viewMenu'; + + get label(): string { + return this.localize('view'); + } + + get items(): Array { + return [ + this.searchVault, + this.separator, + this.passwordGenerator, + this.passwordHistory, + this.separator, + this.zoomIn, + this.zoomOut, + this.resetZoom, + this.separator, + this.toggleFullscreen, + this.separator, + this.reload, + this.toggleDevTools, + ]; + } + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + isAuthenticated: boolean, + ) + { + this._i18nService = i18nService; + this._messagingService = messagingService; + this._isAuthenticated = isAuthenticated; + } + + private get searchVault(): MenuItemConstructorOptions { + return { + id: 'searchVault', + label: this.localize('searchVault'), + click: () => this.sendMessage('focusSearch'), + accelerator: 'CmdOrCtrl+F', + enabled: this._isAuthenticated, + }; + } + + private get separator(): MenuItemConstructorOptions { + return { type: 'separator' }; + } + + private get passwordGenerator(): MenuItemConstructorOptions { + return { + id: 'passwordGenerator', + label: this.localize('passwordGenerator'), + click: () => this.sendMessage('openPasswordGenerator'), + accelerator: 'CmdOrCtrl+G', + enabled: this._isAuthenticated, + }; + } + + private get passwordHistory(): MenuItemConstructorOptions { + return { + id: 'passwordHistory', + label: this.localize('passwordHistory'), + click: () => this.sendMessage('openPasswordHistory'), + enabled: this._isAuthenticated, + }; + } + + private get zoomIn(): MenuItemConstructorOptions { + return { + id: 'zoomIn', + label: this.localize('zoomIn'), + role: 'zoomIn', + accelerator: 'CmdOrCtrl+=', + }; + } + + private get zoomOut(): MenuItemConstructorOptions { + return { + id: 'zoomOut', + label: this.localize('zoomOut'), + role: 'zoomOut', + accelerator: 'CmdOrCtrl+-', + }; + } + + private get resetZoom(): MenuItemConstructorOptions { + return { + id: 'resetZoom', + label: this.localize('resetZoom'), + role: 'resetZoom', + accelerator: 'CmdOrCtrl+0', + }; + } + + private get toggleFullscreen(): MenuItemConstructorOptions { + return { + id: 'toggleFullScreen', + label: this.localize('toggleFullScreen'), + role: 'togglefullscreen', + }; + } + + private get reload(): MenuItemConstructorOptions { + return { + id: 'reload', + label: this.localize('reload'), + role: 'forceReload', + }; + } + + private get toggleDevTools(): MenuItemConstructorOptions { + return { + id: 'toggleDevTools', + label: this.localize('toggleDevTools'), + role: 'toggleDevTools', + accelerator: 'F12', + }; + } + + private localize(s: string) { + return this._i18nService.t(s); + } + + private sendMessage(message: string) { + this._messagingService.send(message); + } +} diff --git a/src/main/menu.window.ts b/src/main/menu.window.ts new file mode 100644 index 00000000..a8649644 --- /dev/null +++ b/src/main/menu.window.ts @@ -0,0 +1,111 @@ +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; + +import { isMacAppStore } from "jslib-electron/utils"; +import { WindowMain } from "jslib-electron/window.main"; + +import { IMenubarMenu } from "./menubar"; + +import { MenuItemConstructorOptions } from 'electron'; + +export class WindowMenu implements IMenubarMenu { + private readonly _i18nService: I18nService; + private readonly _messagingService: MessagingService; + private readonly _window: WindowMain; + + readonly id: string; + + get label(): string { + return this.localize('window'); + } + + get items(): Array { + return [ + this.minimize, + this.hideToMenu, + this.alwaysOnTop, + this.zoom, + this.separator, + this.bringAllToFront, + this.close, + ]; + } + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + windowMain: WindowMain, + ) { + this._i18nService = i18nService; + this._messagingService = messagingService; + this._window = windowMain; + } + + private get minimize(): MenuItemConstructorOptions { + return { + id: 'minimize', + label: this.localize('minimize'), + role: 'minimize', + visible: isMacAppStore(), + }; + } + + private get hideToMenu(): MenuItemConstructorOptions { + return { + id: 'hideToMenu', + label: this.localize(isMacAppStore() ? 'hideToMenuBar' : 'hideToTray'), + click: () => this.sendMessage('hideToTray'), + accelerator: 'CmdOrCtrl+Shift+M', + }; + } + + private get alwaysOnTop(): MenuItemConstructorOptions { + return { + id: 'alwaysOnTop', + label: this.localize('alwaysOnTop'), + type: 'checkbox', + checked: this._window.win.isAlwaysOnTop(), + click: () => this._window.toggleAlwaysOnTop(), + accelerator: 'CmdOrCtrl+Shift+T', + }; + } + + private get zoom(): MenuItemConstructorOptions { + return { + id: 'zoom', + label: this.localize('zoom'), + role: 'zoom', + visible: isMacAppStore(), + }; + } + + private get separator(): MenuItemConstructorOptions { + return { type: 'separator' }; + } + + private get bringAllToFront(): MenuItemConstructorOptions { + return { + id: 'bringAllToFront', + label: this.localize('bringAllToFront'), + role: 'front', + visible: isMacAppStore(), + }; + } + + private get close(): MenuItemConstructorOptions { + return { + id: 'close', + label: this.localize('close'), + role: 'close', + visible: isMacAppStore(), + }; + } + + private localize(s: string) { + return this._i18nService.t(s); + } + + private sendMessage(message: string, args?: any) { + this._messagingService.send(message, args); + } +} diff --git a/src/main/menubar.ts b/src/main/menubar.ts new file mode 100644 index 00000000..f72abe7d --- /dev/null +++ b/src/main/menubar.ts @@ -0,0 +1,99 @@ +import { AboutMenu } from './menu.about'; +import { AccountMenu } from './menu.account'; +import { BitwardenMenu } from './menu.bitwarden'; +import { EditMenu } from './menu.edit'; +import { FileMenu } from './menu.file'; +import { HelpMenu } from './menu.help'; +import { ViewMenu } from './menu.view'; +import { WindowMenu } from './menu.window'; + +import { + Menu, + MenuItemConstructorOptions, +} from 'electron'; +import { MenuUpdateRequest } from './menu.updater'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { MessagingService } from 'jslib-common/abstractions/messaging.service'; +import { UpdaterMain } from 'jslib-electron/updater.main'; +import { WindowMain } from 'jslib-electron/window.main'; + +export interface IMenubarMenu { + id: string; + label: string; + visible?: boolean; // Assumes true if null + items: Array; +} + +export class Menubar { + private readonly items: Array; + + get menu(): Menu { + const template: Array = []; + this.items.forEach((item: IMenubarMenu) => { + template.push({ + id: item.id, + label: item.label, + submenu: item.items, + visible: item.visible ?? true, + }) + }) + return Menu.buildFromTemplate(template); + } + + constructor( + i18nService: I18nService, + messagingService: MessagingService, + updaterMain: UpdaterMain, + windowMain: WindowMain, + webVaultUrl: string, + appVersion: string, + updateRequest?: MenuUpdateRequest, + ) { + this.items = [ + new BitwardenMenu( + i18nService, + messagingService, + updaterMain, + windowMain.win, + updateRequest?.accounts + ), + new FileMenu( + i18nService, + messagingService, + updateRequest?.accounts[updateRequest?.activeUserId].isLocked, + ), + new EditMenu( + i18nService, + messagingService, + updateRequest?.accounts[updateRequest?.activeUserId].isLocked, + ), + new ViewMenu( + i18nService, + messagingService, + updateRequest?.accounts[updateRequest?.activeUserId].isLocked, + ), + new AccountMenu( + i18nService, + messagingService, + webVaultUrl, + windowMain.win, + updateRequest?.accounts[updateRequest?.activeUserId].isLocked, + ), + new WindowMenu( + i18nService, + messagingService, + windowMain, + ), + new AboutMenu( + i18nService, + appVersion, + windowMain.win, + updaterMain, + ), + new HelpMenu( + i18nService, + webVaultUrl, + ), + ] + } +} diff --git a/src/main/messaging.main.ts b/src/main/messaging.main.ts index 4ff3dec1..aadfadd2 100644 --- a/src/main/messaging.main.ts +++ b/src/main/messaging.main.ts @@ -5,6 +5,7 @@ import * as path from 'path'; import { Main } from '../main'; import { StateService } from 'jslib-common/abstractions/state.service'; +import { MenuUpdateRequest } from './menu.updater'; const SyncInterval = 5 * 60 * 1000; // 5 minutes @@ -30,8 +31,8 @@ export class MessagingMain { this.scheduleNextSync(); break; case 'updateAppMenu': - this.main.menuMain.updateApplicationMenuState(message.hideChangeMasterPass, message.accounts, message.activeUserId); - this.updateTrayMenu(message.accounts ? message.accounts[message.activeUserId] : null); + this.main.menuMain.updateApplicationMenuState(message.updateRequest); + this.updateTrayMenu(message.updateRequest); break; case 'minimizeOnCopy': this.stateService.getMinimizeOnCopyToClipboard().then( @@ -90,11 +91,12 @@ export class MessagingMain { }, SyncInterval); } - private updateTrayMenu(activeAccount: { isAuthenticated: boolean, isLocked: boolean }) { - if (this.main.trayMain == null || this.main.trayMain.contextMenu == null) { + private updateTrayMenu(updateRequest: MenuUpdateRequest) { + if (this.main.trayMain == null || this.main.trayMain.contextMenu == null || updateRequest?.activeUserId == null) { return; } const lockNowTrayMenuItem = this.main.trayMain.contextMenu.getMenuItemById('lockNow'); + const activeAccount = updateRequest.accounts[updateRequest.activeUserId]; if (lockNowTrayMenuItem != null && activeAccount != null) { lockNowTrayMenuItem.enabled = activeAccount.isAuthenticated && !activeAccount.isLocked; } diff --git a/src/scss/header.scss b/src/scss/header.scss index 37c17164..2ab45222 100644 --- a/src/scss/header.scss +++ b/src/scss/header.scss @@ -152,10 +152,6 @@ font-style: italic; grid-area: status; } - - &.active { - background-color: #ececec; - } } }