mirror of
https://github.com/bitwarden/clients.git
synced 2026-06-04 21:04:29 +08:00
feat(install-tab): Managed install check for displaying getting started tab
* Propose new managed install check. * Refactors. * Inverted the logic to be positive. * Added Development to allowList, with override.
This commit is contained in:
parent
7b47aeb974
commit
5e09251cee
@ -12,7 +12,6 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/messaging";
|
import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/messaging";
|
||||||
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||||
@ -494,9 +493,7 @@ export default class RuntimeBackground {
|
|||||||
this.onInstalledReason === "install" &&
|
this.onInstalledReason === "install" &&
|
||||||
!(await firstValueFrom(this.browserInitialInstallService.extensionInstalled$))
|
!(await firstValueFrom(this.browserInitialInstallService.extensionInstalled$))
|
||||||
) {
|
) {
|
||||||
if (!devFlagEnabled("skipWelcomeOnInstall")) {
|
await this.browserInitialInstallService.displayWelcomePage();
|
||||||
void BrowserApi.createNewTab("https://bitwarden.com/browser-start/");
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.autofillSettingsService.setInlineMenuVisibility(
|
await this.autofillSettingsService.setInlineMenuVisibility(
|
||||||
AutofillOverlayVisibility.OnFieldFocus,
|
AutofillOverlayVisibility.OnFieldFocus,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended";
|
|||||||
import { LogService } from "@bitwarden/logging";
|
import { LogService } from "@bitwarden/logging";
|
||||||
|
|
||||||
import { BrowserApi } from "./browser-api";
|
import { BrowserApi } from "./browser-api";
|
||||||
|
import { ExtensionInstallType } from "./extension-install-type";
|
||||||
|
|
||||||
type ChromeSettingsGet = chrome.types.ChromeSetting<boolean>["get"];
|
type ChromeSettingsGet = chrome.types.ChromeSetting<boolean>["get"];
|
||||||
|
|
||||||
@ -31,6 +32,88 @@ describe("BrowserApi", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getInstallType", () => {
|
||||||
|
let originalIsWebExtensionsApi: boolean;
|
||||||
|
let originalIsChromeApi: boolean;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalIsWebExtensionsApi = BrowserApi.isWebExtensionsApi;
|
||||||
|
originalIsChromeApi = BrowserApi.isChromeApi;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
BrowserApi.isWebExtensionsApi = originalIsWebExtensionsApi;
|
||||||
|
BrowserApi.isChromeApi = originalIsChromeApi;
|
||||||
|
delete (global.chrome as any).management;
|
||||||
|
delete (global as any).browser;
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["admin", ExtensionInstallType.Admin],
|
||||||
|
["development", ExtensionInstallType.Development],
|
||||||
|
["normal", ExtensionInstallType.Normal],
|
||||||
|
["sideload", ExtensionInstallType.Sideload],
|
||||||
|
["other", ExtensionInstallType.Other],
|
||||||
|
])("returns %s when chrome.management.getSelf reports it", async (raw, expected) => {
|
||||||
|
(global.chrome as any).management = {
|
||||||
|
getSelf: jest.fn().mockResolvedValue({ installType: raw }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await BrowserApi.getInstallType();
|
||||||
|
|
||||||
|
expect(result).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers browser.management.getSelf when isWebExtensionsApi is true", async () => {
|
||||||
|
BrowserApi.isWebExtensionsApi = true;
|
||||||
|
const browserGetSelf = jest.fn().mockResolvedValue({ installType: "admin" });
|
||||||
|
const chromeGetSelf = jest.fn().mockResolvedValue({ installType: "normal" });
|
||||||
|
(global as any).browser = { management: { getSelf: browserGetSelf } };
|
||||||
|
(global.chrome as any).management = { getSelf: chromeGetSelf };
|
||||||
|
|
||||||
|
const result = await BrowserApi.getInstallType();
|
||||||
|
|
||||||
|
expect(result).toBe(ExtensionInstallType.Admin);
|
||||||
|
expect(browserGetSelf).toHaveBeenCalled();
|
||||||
|
expect(chromeGetSelf).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns Unknown when management.getSelf rejects", async () => {
|
||||||
|
(global.chrome as any).management = {
|
||||||
|
getSelf: jest.fn().mockRejectedValue(new Error("not available")),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await BrowserApi.getInstallType();
|
||||||
|
|
||||||
|
expect(result).toBe(ExtensionInstallType.Unknown);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns Unknown when the result has no installType", async () => {
|
||||||
|
(global.chrome as any).management = {
|
||||||
|
getSelf: jest.fn().mockResolvedValue({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await BrowserApi.getInstallType();
|
||||||
|
|
||||||
|
expect(result).toBe(ExtensionInstallType.Unknown);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns Unknown when chrome.management is absent", async () => {
|
||||||
|
const result = await BrowserApi.getInstallType();
|
||||||
|
|
||||||
|
expect(result).toBe(ExtensionInstallType.Unknown);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns Unknown when neither chrome nor browser is available", async () => {
|
||||||
|
BrowserApi.isWebExtensionsApi = false;
|
||||||
|
BrowserApi.isChromeApi = false;
|
||||||
|
|
||||||
|
const result = await BrowserApi.getInstallType();
|
||||||
|
|
||||||
|
expect(result).toBe(ExtensionInstallType.Unknown);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("senderIsInternal", () => {
|
describe("senderIsInternal", () => {
|
||||||
const EXTENSION_ORIGIN = "chrome-extension://id";
|
const EXTENSION_ORIGIN = "chrome-extension://id";
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { TabMessage } from "../../types/tab-messages";
|
|||||||
import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service";
|
import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service";
|
||||||
|
|
||||||
import { registerContentScriptsPolyfill } from "./browser-api.register-content-scripts-polyfill";
|
import { registerContentScriptsPolyfill } from "./browser-api.register-content-scripts-polyfill";
|
||||||
|
import { ExtensionInstallType } from "./extension-install-type";
|
||||||
|
|
||||||
export class BrowserApi {
|
export class BrowserApi {
|
||||||
static isWebExtensionsApi: boolean = typeof browser !== "undefined";
|
static isWebExtensionsApi: boolean = typeof browser !== "undefined";
|
||||||
@ -33,6 +34,30 @@ export class BrowserApi {
|
|||||||
return BrowserApi.manifestVersion === expectedVersion;
|
return BrowserApi.manifestVersion === expectedVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns how this extension was installed on the current browser. Use
|
||||||
|
* {@link ExtensionInstallType.Admin} to detect enterprise-policy installs and
|
||||||
|
* {@link ExtensionInstallType.Sideload} to detect extensions installed by other software
|
||||||
|
* on the machine.
|
||||||
|
*
|
||||||
|
* `management.getSelf()` is the only `chrome.management` method that does
|
||||||
|
* not require the "management" manifest permission.
|
||||||
|
*/
|
||||||
|
static async getInstallType(): Promise<ExtensionInstallType> {
|
||||||
|
try {
|
||||||
|
if (BrowserApi.isWebExtensionsApi) {
|
||||||
|
const info = await browser.management.getSelf();
|
||||||
|
return (info?.installType as ExtensionInstallType) ?? ExtensionInstallType.Unknown;
|
||||||
|
} else if (BrowserApi.isChromeApi) {
|
||||||
|
const info = await chrome.management.getSelf();
|
||||||
|
return (info?.installType as ExtensionInstallType) ?? ExtensionInstallType.Unknown;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// management API not available on this browser (e.g. older Safari)
|
||||||
|
}
|
||||||
|
return ExtensionInstallType.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns `true` if the message sender appears to originate from within this extension.
|
* Returns `true` if the message sender appears to originate from within this extension.
|
||||||
*
|
*
|
||||||
|
|||||||
10
apps/browser/src/platform/browser/extension-install-type.ts
Normal file
10
apps/browser/src/platform/browser/extension-install-type.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export const ExtensionInstallType = Object.freeze({
|
||||||
|
Admin: "admin",
|
||||||
|
Development: "development",
|
||||||
|
Normal: "normal",
|
||||||
|
Sideload: "sideload",
|
||||||
|
Other: "other",
|
||||||
|
Unknown: "unknown",
|
||||||
|
} as const);
|
||||||
|
|
||||||
|
export type ExtensionInstallType = (typeof ExtensionInstallType)[keyof typeof ExtensionInstallType];
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
|
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||||
|
import { GlobalState, StateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../browser/browser-api";
|
||||||
|
import { ExtensionInstallType } from "../browser/extension-install-type";
|
||||||
|
|
||||||
|
import BrowserInitialInstallService from "./browser-initial-install.service";
|
||||||
|
|
||||||
|
jest.mock("@bitwarden/common/platform/misc/flags", () => ({
|
||||||
|
...jest.requireActual("@bitwarden/common/platform/misc/flags"),
|
||||||
|
devFlagEnabled: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("BrowserInitialInstallService", () => {
|
||||||
|
let service: BrowserInitialInstallService;
|
||||||
|
let getInstallTypeSpy: jest.SpyInstance;
|
||||||
|
let createNewTabSpy: jest.SpyInstance;
|
||||||
|
const devFlagEnabledMock = devFlagEnabled as jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const stateProvider = mock<StateProvider>();
|
||||||
|
const globalState = mock<GlobalState<boolean>>();
|
||||||
|
Object.defineProperty(globalState, "state$", { value: of(false) });
|
||||||
|
stateProvider.getGlobal.mockReturnValue(globalState);
|
||||||
|
|
||||||
|
getInstallTypeSpy = jest.spyOn(BrowserApi, "getInstallType");
|
||||||
|
createNewTabSpy = jest.spyOn(BrowserApi, "createNewTab").mockResolvedValue({} as any);
|
||||||
|
devFlagEnabledMock.mockReturnValue(false);
|
||||||
|
|
||||||
|
service = new BrowserInitialInstallService(stateProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("displayWelcomePage", () => {
|
||||||
|
it.each([
|
||||||
|
["Normal", ExtensionInstallType.Normal],
|
||||||
|
["Development", ExtensionInstallType.Development],
|
||||||
|
["Unknown", ExtensionInstallType.Unknown],
|
||||||
|
])("opens the welcome page for %s installs", async (_, installType) => {
|
||||||
|
getInstallTypeSpy.mockResolvedValue(installType);
|
||||||
|
|
||||||
|
await service.displayWelcomePage();
|
||||||
|
|
||||||
|
expect(createNewTabSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(createNewTabSpy).toHaveBeenCalledWith("https://bitwarden.com/browser-start/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["Admin", ExtensionInstallType.Admin],
|
||||||
|
["Sideload", ExtensionInstallType.Sideload],
|
||||||
|
["Other", ExtensionInstallType.Other],
|
||||||
|
])("does not open the welcome page for %s installs", async (_, installType) => {
|
||||||
|
getInstallTypeSpy.mockResolvedValue(installType);
|
||||||
|
|
||||||
|
await service.displayWelcomePage();
|
||||||
|
|
||||||
|
expect(createNewTabSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["Normal", ExtensionInstallType.Normal],
|
||||||
|
["Development", ExtensionInstallType.Development],
|
||||||
|
["Unknown", ExtensionInstallType.Unknown],
|
||||||
|
])(
|
||||||
|
"does not open the welcome page for %s installs when the skipWelcomeOnInstall dev flag is on",
|
||||||
|
async (_, installType) => {
|
||||||
|
getInstallTypeSpy.mockResolvedValue(installType);
|
||||||
|
devFlagEnabledMock.mockImplementation((flag) => flag === "skipWelcomeOnInstall");
|
||||||
|
|
||||||
|
await service.displayWelcomePage();
|
||||||
|
|
||||||
|
expect(createNewTabSpy).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("checks the skipWelcomeOnInstall dev flag", async () => {
|
||||||
|
getInstallTypeSpy.mockResolvedValue(ExtensionInstallType.Normal);
|
||||||
|
|
||||||
|
await service.displayWelcomePage();
|
||||||
|
|
||||||
|
expect(devFlagEnabledMock).toHaveBeenCalledWith("skipWelcomeOnInstall");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { Observable, map } from "rxjs";
|
import { Observable, map } from "rxjs";
|
||||||
|
|
||||||
|
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||||
import {
|
import {
|
||||||
GlobalState,
|
GlobalState,
|
||||||
EXTENSION_INITIAL_INSTALL_DISK,
|
EXTENSION_INITIAL_INSTALL_DISK,
|
||||||
@ -7,6 +8,11 @@ import {
|
|||||||
StateProvider,
|
StateProvider,
|
||||||
} from "@bitwarden/common/platform/state";
|
} from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../browser/browser-api";
|
||||||
|
import { ExtensionInstallType } from "../browser/extension-install-type";
|
||||||
|
|
||||||
|
const WELCOME_PAGE_URL = "https://bitwarden.com/browser-start/";
|
||||||
|
|
||||||
const EXTENSION_INSTALLED = new KeyDefinition<boolean>(
|
const EXTENSION_INSTALLED = new KeyDefinition<boolean>(
|
||||||
EXTENSION_INITIAL_INSTALL_DISK,
|
EXTENSION_INITIAL_INSTALL_DISK,
|
||||||
"extensionInstalled",
|
"extensionInstalled",
|
||||||
@ -28,4 +34,27 @@ export default class BrowserInitialInstallService {
|
|||||||
async setExtensionInstalled(value: boolean) {
|
async setExtensionInstalled(value: boolean) {
|
||||||
await this.extensionInstalled.update(() => value);
|
await this.extensionInstalled.update(() => value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the configured welcome page on initial install, if the
|
||||||
|
* install type supports it.
|
||||||
|
*/
|
||||||
|
async displayWelcomePage() {
|
||||||
|
// We use the install type here because it is available at install time, versus
|
||||||
|
// specific MDM-delivered settings, which are eventually consistent on extension load.
|
||||||
|
const installType = await BrowserApi.getInstallType();
|
||||||
|
|
||||||
|
// We only want to show the welcome page for user-initiated installs and not for
|
||||||
|
// administrative or sideloaded installs. We also enable it for Development installs
|
||||||
|
// so unpacked builds (including integration test harnesses) still exercise the
|
||||||
|
// welcome flow, and for Unknown to handle browsers that don't expose the install type.
|
||||||
|
const isUserInitiatedInstall =
|
||||||
|
installType === ExtensionInstallType.Normal ||
|
||||||
|
installType === ExtensionInstallType.Development ||
|
||||||
|
installType === ExtensionInstallType.Unknown;
|
||||||
|
|
||||||
|
if (isUserInitiatedInstall && !devFlagEnabled("skipWelcomeOnInstall")) {
|
||||||
|
void BrowserApi.createNewTab(WELCOME_PAGE_URL);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user