1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-10 12:33:26 +00:00

Enable Basic Desktop Modal Support (#11484)

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>
This commit is contained in:
Anders Åberg
2025-03-11 09:03:28 +01:00
committed by GitHub
parent 3b9be21fd7
commit 7e6f2fa798
8 changed files with 248 additions and 18 deletions

View File

@@ -1,6 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as path from "path";
import * as url from "url";
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron";
import { firstValueFrom } from "rxjs";
@@ -9,6 +10,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { BiometricStateService, BiometricsService } from "@bitwarden/key-management";
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
import { cleanUserAgent, isDev } from "../utils";
import { WindowMain } from "./window.main";
@@ -49,6 +51,11 @@ export class TrayMain {
label: this.i18nService.t("showHide"),
click: () => this.toggleWindow(),
},
{
visible: isDev(),
label: "Fake Popup",
click: () => this.fakePopup(),
},
{ type: "separator" },
{
label: this.i18nService.t("exit"),
@@ -190,7 +197,7 @@ export class TrayMain {
this.hideDock();
}
} else {
this.windowMain.win.show();
this.windowMain.show();
if (this.isDarwin()) {
this.showDock();
}
@@ -203,4 +210,38 @@ export class TrayMain {
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() {
if (this.windowMain.win == null || this.windowMain.win.isDestroyed()) {
await this.windowMain.createWindow("modal-app");
return;
}
// Restyle existing
const existingWin = this.windowMain.win;
await this.desktopSettingsService.setInModalMode(true);
await existingWin.loadURL(
url.format({
protocol: "file:",
//pathname: `${__dirname}/index.html`,
pathname: path.join(__dirname, "/index.html"),
slashes: true,
hash: "/passkeys",
query: {
redirectUrl: "/passkeys",
},
}),
{
userAgent: cleanUserAgent(existingWin.webContents.userAgent),
},
);
existingWin.once("ready-to-show", () => {
existingWin.show();
});
}
}

View File

@@ -5,7 +5,7 @@ import * as path from "path";
import * as url from "url";
import { app, BrowserWindow, ipcMain, nativeTheme, screen, session } from "electron";
import { firstValueFrom } from "rxjs";
import { concatMap, firstValueFrom, pairwise } from "rxjs";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
@@ -14,6 +14,7 @@ import { processisolations } from "@bitwarden/desktop-napi";
import { BiometricStateService } from "@bitwarden/key-management";
import { WindowState } from "../platform/models/domain/window-state";
import { applyMainWindowStyles, applyPopupModalStyles } from "../platform/popup-modal-styles";
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
import { cleanUserAgent, isDev, isLinux, isMac, isMacAppStore, isWindows } from "../utils";
@@ -77,6 +78,24 @@ export class WindowMain {
}
});
this.desktopSettingsService.inModalMode$
.pipe(
pairwise(),
concatMap(async ([lastValue, newValue]) => {
if (lastValue && !newValue) {
// Reset the window state to the main window state
applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]);
// Because modal is used in front of another app, UX wise it makes sense to hide the main window when leaving modal mode.
this.win.hide();
} else if (!lastValue && newValue) {
// Apply the popup modal styles
applyPopupModalStyles(this.win);
this.win.show();
}
}),
)
.subscribe();
this.desktopSettingsService.preventScreenshots$.subscribe((prevent) => {
if (this.win == null) {
return;
@@ -182,7 +201,20 @@ export class WindowMain {
});
}
async createWindow(): Promise<void> {
/// Show the window with main window styles
show() {
if (this.win != null) {
applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]);
this.win.show();
}
}
/**
* Creates the main window. The template argument is used to determine the styling of the window and what url will be loaded.
* When the template is "modal-app", the window will be styled as a modal and the passkeys page will be loaded.
* TODO: We might want to refactor the template argument to accomodate more target pages, e.g. ssh-agent.
*/
async createWindow(template: "full-app" | "modal-app" = "full-app"): Promise<void> {
this.windowStates[mainWindowSizeKey] = await this.getWindowState(
this.defaultWidth,
this.defaultHeight,
@@ -216,6 +248,12 @@ export class WindowMain {
},
});
if (template === "modal-app") {
applyPopupModalStyles(this.win);
} else {
applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]);
}
this.win.webContents.on("dom-ready", () => {
this.win.webContents.zoomFactor = this.windowStates[mainWindowSizeKey].zoomFactor ?? 1.0;
});
@@ -225,21 +263,41 @@ export class WindowMain {
}
// Show it later since it might need to be maximized.
this.win.show();
// use once event to avoid flash on unstyled content.
this.win.once("ready-to-show", () => {
this.win.show();
});
// and load the index.html of the app.
// 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.win.loadURL(
url.format({
protocol: "file:",
pathname: path.join(__dirname, "/index.html"),
slashes: true,
}),
{
userAgent: cleanUserAgent(this.win.webContents.userAgent),
},
);
if (template === "full-app") {
// and load the index.html of the app.
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
void this.win.loadURL(
url.format({
protocol: "file:",
pathname: path.join(__dirname, "/index.html"),
slashes: true,
}),
{
userAgent: cleanUserAgent(this.win.webContents.userAgent),
},
);
} else {
// we're in modal mode - load the passkeys page
await this.win.loadURL(
url.format({
protocol: "file:",
pathname: path.join(__dirname, "/index.html"),
slashes: true,
hash: "/passkeys",
query: {
redirectUrl: "/passkeys",
},
}),
{
userAgent: cleanUserAgent(this.win.webContents.userAgent),
},
);
}
// Open the DevTools.
if (isDev()) {
@@ -336,6 +394,12 @@ export class WindowMain {
return;
}
const inModalMode = await firstValueFrom(this.desktopSettingsService.inModalMode$);
if (inModalMode) {
return;
}
try {
const bounds = win.getBounds();
@@ -346,9 +410,14 @@ export class WindowMain {
}
}
this.windowStates[configKey].isMaximized = win.isMaximized();
// We treat fullscreen as maximized (would be even better to store isFullscreen as its own flag).
this.windowStates[configKey].isMaximized = win.isMaximized() || win.isFullScreen();
this.windowStates[configKey].displayBounds = screen.getDisplayMatching(bounds).bounds;
// Maybe store these as well?
// win.isFocused();
// win.isVisible();
if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) {
this.windowStates[configKey].x = bounds.x;
this.windowStates[configKey].y = bounds.y;