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;
- }
}
}