1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00
Files
browser/apps/desktop/src/main/tray.main.ts
Anders Åberg 8e455007c0 PM-19095: Wire passkey autofill to UI (#13051)
* Passkey stuff

Co-authored-by: Anders Åberg <github@andersaberg.com>

* Ugly hacks

* Work On Modal State Management

* Applying modalStyles

* modal

* Improved hide/show

* fixed promise

* File name

* fix prettier

* Protecting against null API's and undefined data

* Only show fake popup to devs

* cleanup mock code

* rename minmimal-app to modal-app

* Added comment

* Added comment

* removed old comment

* Avoided changing minimum size

* Add small comment

* Rename component

* adress feedback

* Fixed uppercase file

* Fixed build

* Added codeowners

* added void

* commentary

* feat: reset setting on app start

* Moved reset to be in main / process launch

* Add comment to create window

* Added a little bit of styling

* Use Messaging service to loadUrl

* Enable passkeysautofill

* Add logging

* halfbaked

* Integration working

* And now it works without extra delay

* Clean up

* add note about messaging

* lb

* removed console.logs

* Cleanup and adress review feedback

* This hides the swift UI

* pick credential, draft

* Remove logger

* a whole lot of wiring

* not working

* Improved wiring

* Cancel after 90s

* Introduced observable

* Launching bitwarden if its not running

* Passing position from native to electron

* Rename inModalMode to modalMode

* remove tap

* revert spaces

* added back isDev

* cleaned up a bit

* Cleanup swift file

* tweaked logging

* clean up

* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Update apps/desktop/src/platform/services/desktop-settings.service.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* adress position feedback

* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Removed extra logging

* Adjusted error logging

* Use .error to log errors

* remove dead code

* Update desktop-autofill.service.ts

* use parseCredentialId instead of guidToRawFormat

* Update apps/desktop/src/autofill/services/desktop-autofill.service.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Change windowXy to a Record instead of [number,number]

* Update apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts

Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>

* Remove unsued dep and comment

* changed timeout to be spec recommended maxium, 10 minutes, for now.

* Correctly assume UP

* Removed extra cancelRequest in deinint

* Add timeout and UV to confirmChoseCipher

UV is performed by UI, not the service

* Improved docs regarding undefined cipherId

* cleanup: UP is no longer undefined

* Run completeError if ipc messages conversion failed

* don't throw, instead return undefined

* Disabled passkey provider

* Throw error if no activeUserId was found

* removed comment

* Fixed lint

* removed unsued service

* reset entitlement formatting

* Update entitlements.mas.plist

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
2025-03-24 07:50:11 -04:00

222 lines
6.9 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as path from "path";
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron";
import { firstValueFrom } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { BiometricsService } from "@bitwarden/key-management";
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
import { isDev } from "../utils";
import { WindowMain } from "./window.main";
export class TrayMain {
contextMenu: Menu;
private appName: string;
private tray: Tray;
private icon: string | Electron.NativeImage;
private pressedIcon: Electron.NativeImage;
constructor(
private windowMain: WindowMain,
private i18nService: I18nService,
private desktopSettingsService: DesktopSettingsService,
private messagingService: MessagingService,
private biometricService: BiometricsService,
) {
if (process.platform === "win32") {
this.icon = path.join(__dirname, "/images/icon.ico");
} else if (process.platform === "darwin") {
const nImage = nativeImage.createFromPath(path.join(__dirname, "/images/icon-template.png"));
nImage.setTemplateImage(true);
this.icon = nImage;
this.pressedIcon = nativeImage.createFromPath(
path.join(__dirname, "/images/icon-highlight.png"),
);
} else {
this.icon = path.join(__dirname, "/images/icon.png");
}
}
async init(appName: string, additionalMenuItems: MenuItemConstructorOptions[] = null) {
this.appName = appName;
const menuItemOptions: MenuItemConstructorOptions[] = [
{
label: this.i18nService.t("showHide"),
click: () => this.toggleWindow(),
},
{
visible: isDev(),
label: "Fake Popup",
click: () => this.fakePopup(),
},
{ type: "separator" },
{
label: this.i18nService.t("exit"),
click: () => this.closeWindow(),
},
];
if (additionalMenuItems != null) {
menuItemOptions.splice(1, 0, ...additionalMenuItems);
}
this.contextMenu = Menu.buildFromTemplate(menuItemOptions);
if (await firstValueFrom(this.desktopSettingsService.trayEnabled$)) {
this.showTray();
}
}
setupWindowListeners(win: BrowserWindow) {
win.on("minimize", async () => {
if (await firstValueFrom(this.desktopSettingsService.minimizeToTray$)) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.hideToTray();
}
});
win.on("restore", async () => {
await this.biometricService.setShouldAutopromptNow(true);
});
win.on("close", async (e: Event) => {
if (await firstValueFrom(this.desktopSettingsService.closeToTray$)) {
if (!this.windowMain.isQuitting) {
e.preventDefault();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.hideToTray();
}
}
});
win.on("show", async () => {
const enableTray = await firstValueFrom(this.desktopSettingsService.trayEnabled$);
if (!enableTray) {
setTimeout(() => this.removeTray(false), 100);
}
});
}
removeTray(showWindow = true) {
// Due to https://github.com/electron/electron/issues/17622
// we cannot destroy the tray icon on linux.
if (this.tray != null && process.platform !== "linux") {
this.tray.destroy();
this.tray = null;
}
if (showWindow && this.windowMain.win != null && !this.windowMain.win.isVisible()) {
this.windowMain.win.show();
}
}
async hideToTray() {
this.showTray();
if (this.windowMain.win != null) {
this.windowMain.win.hide();
}
if (this.isDarwin() && !(await firstValueFrom(this.desktopSettingsService.alwaysShowDock$))) {
this.hideDock();
}
}
restoreFromTray() {
if (this.windowMain.win == null || !this.windowMain.win.isVisible()) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.toggleWindow();
}
}
showTray() {
if (this.tray != null) {
return;
}
this.tray = new Tray(this.icon);
this.tray.setToolTip(this.appName);
this.tray.on("click", () => this.toggleWindow());
this.tray.on("right-click", () => this.tray.popUpContextMenu(this.contextMenu));
if (this.pressedIcon != null) {
this.tray.setPressedImage(this.pressedIcon);
}
if (this.contextMenu != null && !this.isDarwin()) {
this.tray.setContextMenu(this.contextMenu);
}
}
updateContextMenu() {
if (this.tray != null && this.contextMenu != null && this.isLinux()) {
this.tray.setContextMenu(this.contextMenu);
}
}
private hideDock() {
app.dock.hide();
}
private showDock() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
app.dock.show();
}
private isDarwin() {
return process.platform === "darwin";
}
private isLinux() {
return process.platform === "linux";
}
private async toggleWindow() {
if (this.windowMain.win == null) {
if (this.isDarwin()) {
// On MacOS, closing the window via the red button destroys the BrowserWindow instance.
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.windowMain.createWindow().then(() => {
this.windowMain.win.show();
this.showDock();
});
}
return;
}
if (this.windowMain.win.isVisible()) {
this.windowMain.win.hide();
if (this.isDarwin() && !(await firstValueFrom(this.desktopSettingsService.alwaysShowDock$))) {
this.hideDock();
}
} else {
this.windowMain.show();
if (this.isDarwin()) {
this.showDock();
}
}
}
private closeWindow() {
this.windowMain.isQuitting = true;
if (this.windowMain.win != null) {
this.windowMain.win.close();
}
}
/**
* This method is used to test modal behavior during development and could be removed in the future.
* @returns
*/
private async fakePopup() {
await this.messagingService.send("loadurl", { url: "/passkeys", modal: true });
}
}