diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/metadata.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/metadata.rs index a95a86ef0e8d..28f13cd98635 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/metadata.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/metadata.rs @@ -173,6 +173,8 @@ mod tests { let map = get_supported_importers::(); let expected: HashSet = HashSet::from([ + "bravecsv".to_string(), + "chromecsv".to_string(), "chromiumcsv".to_string(), "edgecsv".to_string(), "operacsv".to_string(), @@ -192,7 +194,14 @@ mod tests { #[test] fn windows_specific_loaders_match_const_array() { let map = get_supported_importers::(); - let ids = ["chromiumcsv", "edgecsv", "operacsv", "vivaldicsv"]; + let ids = [ + "bravecsv", + "chromecsv", + "chromiumcsv", + "edgecsv", + "operacsv", + "vivaldicsv", + ]; for id in ids { let loaders = get_loaders(&map, id); diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs index b9c1c9a4cc20..096808aafb6a 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs @@ -16,7 +16,15 @@ use crate::util; // // IMPORTANT adjust array size when enabling / disabling chromium importers here -pub const SUPPORTED_BROWSERS: [BrowserConfig; 4] = [ +pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [ + BrowserConfig { + name: "Brave", + data_dir: "AppData/Local/BraveSoftware/Brave-Browser/User Data", + }, + BrowserConfig { + name: "Chrome", + data_dir: "AppData/Local/Google/Chrome/User Data", + }, BrowserConfig { name: "Chromium", data_dir: "AppData/Local/Chromium/User Data", diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index f6090c01d2bd..9aab265550f9 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -39,6 +39,7 @@ export enum FeatureFlag { /* Tools */ DesktopSendUIRefresh = "desktop-send-ui-refresh", UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators", + ChromiumImporterWithABE = "pm-25855-chromium-importer-abe", /* DIRT */ EventBasedOrganizationIntegrations = "event-based-organization-integrations", @@ -84,6 +85,7 @@ export const DefaultFeatureFlagValue = { /* Tools */ [FeatureFlag.DesktopSendUIRefresh]: FALSE, [FeatureFlag.UseSdkPasswordGenerators]: FALSE, + [FeatureFlag.ChromiumImporterWithABE]: FALSE, /* DIRT */ [FeatureFlag.EventBasedOrganizationIntegrations]: FALSE, diff --git a/libs/importer/src/services/default-import-metadata.service.ts b/libs/importer/src/services/default-import-metadata.service.ts index a9e767178aa8..05b8472869d3 100644 --- a/libs/importer/src/services/default-import-metadata.service.ts +++ b/libs/importer/src/services/default-import-metadata.service.ts @@ -1,9 +1,11 @@ -import { map, Observable } from "rxjs"; +import { combineLatest, map, Observable } from "rxjs"; +import { ClientType, DeviceType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { SemanticLogger } from "@bitwarden/common/tools/log"; import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; -import { ImporterMetadata, Importers, ImportersMetadata } from "../metadata"; +import { DataLoader, ImporterMetadata, Importers, ImportersMetadata, Loader } from "../metadata"; import { ImportType } from "../models/import-options"; import { availableLoaders } from "../util"; @@ -13,8 +15,13 @@ export class DefaultImportMetadataService implements ImportMetadataServiceAbstra protected importers: ImportersMetadata = Importers; private logger: SemanticLogger; + private chromiumWithABE$: Observable; + constructor(protected system: SystemServiceProvider) { this.logger = system.log({ type: "ImportMetadataService" }); + this.chromiumWithABE$ = this.system.configService.getFeatureFlag$( + FeatureFlag.ChromiumImporterWithABE, + ); } async init(): Promise { @@ -23,13 +30,13 @@ export class DefaultImportMetadataService implements ImportMetadataServiceAbstra metadata$(type$: Observable): Observable { const client = this.system.environment.getClientType(); - const capabilities$ = type$.pipe( - map((type) => { + const capabilities$ = combineLatest([type$, this.chromiumWithABE$]).pipe( + map(([type, enabled]) => { if (!this.importers) { return { type, loaders: [] }; } - const loaders = availableLoaders(this.importers, type, client); + const loaders = this.availableLoaders(this.importers, type, client, enabled); if (!loaders || loaders.length === 0) { return { type, loaders: [] }; @@ -48,4 +55,33 @@ export class DefaultImportMetadataService implements ImportMetadataServiceAbstra return capabilities$; } + + /** Determine the available loaders for the given import type and client, considering feature flags and environments */ + private availableLoaders( + importers: ImportersMetadata, + type: ImportType, + client: ClientType, + enabled: boolean, + ): DataLoader[] | undefined { + let loaders = availableLoaders(importers, type, client); + let includeABE = false; + + if (enabled && (type === "bravecsv" || type === "chromecsv" || type === "edgecsv")) { + try { + const device = this.system.environment.getDevice(); + const isWindowsDesktop = device === DeviceType.WindowsDesktop; + if (isWindowsDesktop) { + includeABE = true; + } + } catch { + includeABE = true; + } + } + + // If the browser is unsupported, remove the chromium loader + if (!includeABE) { + loaders = loaders?.filter((loader) => loader !== Loader.chromium); + } + return loaders; + } } diff --git a/libs/importer/src/services/import-metadata.service.spec.ts b/libs/importer/src/services/import-metadata.service.spec.ts index 25ce41251b6e..908ce6ad476e 100644 --- a/libs/importer/src/services/import-metadata.service.spec.ts +++ b/libs/importer/src/services/import-metadata.service.spec.ts @@ -1,19 +1,19 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { Subject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, Subject, firstValueFrom } from "rxjs"; import { ClientType } from "@bitwarden/client-type"; +import { DeviceType } from "@bitwarden/common/enums"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; -import { ImporterMetadata, Instructions } from "../metadata"; +import { ImporterMetadata, ImportersMetadata, Instructions, Loader } from "../metadata"; import { ImportType } from "../models"; import { DefaultImportMetadataService } from "./default-import-metadata.service"; -import { ImportMetadataServiceAbstraction } from "./import-metadata.service.abstraction"; describe("ImportMetadataService", () => { - let sut: ImportMetadataServiceAbstraction; + let sut: DefaultImportMetadataService; let systemServiceProvider: MockProxy; beforeEach(() => { @@ -34,15 +34,18 @@ describe("ImportMetadataService", () => { describe("metadata$", () => { let typeSubject: Subject; let mockLogger: { debug: jest.Mock }; + let featureFlagSubject: BehaviorSubject; + + const environment = mock(); + environment.getClientType.mockReturnValue(ClientType.Desktop); beforeEach(() => { typeSubject = new Subject(); mockLogger = { debug: jest.fn() }; + featureFlagSubject = new BehaviorSubject(false); const configService = mock(); - - const environment = mock(); - environment.getClientType.mockReturnValue(ClientType.Desktop); + configService.getFeatureFlag$.mockReturnValue(featureFlagSubject); systemServiceProvider = mock({ configService, @@ -56,6 +59,7 @@ describe("ImportMetadataService", () => { afterEach(() => { typeSubject.complete(); + featureFlagSubject.complete(); }); it("should emit metadata when type$ emits", async () => { @@ -106,5 +110,76 @@ describe("ImportMetadataService", () => { "capabilities updated", ); }); + + it("should update when feature flag changes", async () => { + const testType: ImportType = "bravecsv"; // Use bravecsv which supports chromium loader + const emissions: ImporterMetadata[] = []; + + const subscription = sut.metadata$(typeSubject).subscribe((metadata) => { + emissions.push(metadata); + }); + + typeSubject.next(testType); + featureFlagSubject.next(true); + + // Wait for emissions + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(emissions).toHaveLength(2); + expect(emissions[0].loaders).not.toContain(Loader.chromium); + expect(emissions[1].loaders).toContain(Loader.file); + + subscription.unsubscribe(); + }); + + it("should exclude chromium loader when ABE is disabled but on Windows Desktop", async () => { + environment.getDevice.mockReturnValue(DeviceType.WindowsDesktop); + const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders + featureFlagSubject.next(false); + + const metadataPromise = firstValueFrom(sut.metadata$(typeSubject)); + typeSubject.next(testType); + + const result = await metadataPromise; + + expect(result.loaders).not.toContain(Loader.chromium); + expect(result.loaders).toContain(Loader.file); + }); + + it("should exclude chromium loader when ABE is enabled but not on Windows Desktop", async () => { + environment.getDevice.mockReturnValue(DeviceType.MacOsDesktop); + const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders + featureFlagSubject.next(true); + + const metadataPromise = firstValueFrom(sut.metadata$(typeSubject)); + typeSubject.next(testType); + + const result = await metadataPromise; + + expect(result.loaders).not.toContain(Loader.chromium); + expect(result.loaders).toContain(Loader.file); + }); + + it("should include chromium loader when ABE is enabled and on Windows Desktop", async () => { + // Set up importers to include bravecsv with chromium loader + sut["importers"] = { + bravecsv: { + type: "bravecsv", + loaders: [Loader.file, Loader.chromium], + instructions: Instructions.chromium, + }, + } as ImportersMetadata; + + environment.getDevice.mockReturnValue(DeviceType.WindowsDesktop); + const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders + featureFlagSubject.next(true); + + const metadataPromise = firstValueFrom(sut.metadata$(typeSubject)); + typeSubject.next(testType); + + const result = await metadataPromise; + + expect(result.loaders).toContain(Loader.chromium); + }); }); });