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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
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 { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
@ -494,9 +493,7 @@ export default class RuntimeBackground {
|
||||
this.onInstalledReason === "install" &&
|
||||
!(await firstValueFrom(this.browserInitialInstallService.extensionInstalled$))
|
||||
) {
|
||||
if (!devFlagEnabled("skipWelcomeOnInstall")) {
|
||||
void BrowserApi.createNewTab("https://bitwarden.com/browser-start/");
|
||||
}
|
||||
await this.browserInitialInstallService.displayWelcomePage();
|
||||
|
||||
await this.autofillSettingsService.setInlineMenuVisibility(
|
||||
AutofillOverlayVisibility.OnFieldFocus,
|
||||
|
||||
@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { BrowserApi } from "./browser-api";
|
||||
import { ExtensionInstallType } from "./extension-install-type";
|
||||
|
||||
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", () => {
|
||||
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 { registerContentScriptsPolyfill } from "./browser-api.register-content-scripts-polyfill";
|
||||
import { ExtensionInstallType } from "./extension-install-type";
|
||||
|
||||
export class BrowserApi {
|
||||
static isWebExtensionsApi: boolean = typeof browser !== "undefined";
|
||||
@ -33,6 +34,30 @@ export class BrowserApi {
|
||||
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.
|
||||
*
|
||||
|
||||
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 { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||
import {
|
||||
GlobalState,
|
||||
EXTENSION_INITIAL_INSTALL_DISK,
|
||||
@ -7,6 +8,11 @@ import {
|
||||
StateProvider,
|
||||
} 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>(
|
||||
EXTENSION_INITIAL_INSTALL_DISK,
|
||||
"extensionInstalled",
|
||||
@ -28,4 +34,27 @@ export default class BrowserInitialInstallService {
|
||||
async setExtensionInstalled(value: boolean) {
|
||||
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