1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-14 23:45:37 +00:00

Merge remote-tracking branch 'origin/main' into rename-tsconfig

This commit is contained in:
addisonbeck
2025-05-29 15:10:05 -04:00
32 changed files with 708 additions and 382 deletions

View File

@@ -1579,252 +1579,6 @@ export default class AutofillService implements AutofillServiceInterface {
return [expectedDateFormat, dateFormatPatterns];
}
/**
* Generates the autofill script for the specified page details and identify cipher item.
* @param {AutofillScript} fillScript
* @param {AutofillPageDetails} pageDetails
* @param {{[p: string]: AutofillField}} filledFields
* @param {GenerateFillScriptOptions} options
* @returns {AutofillScript}
* @private
*/
private async generateIdentityFillScript(
fillScript: AutofillScript,
pageDetails: AutofillPageDetails,
filledFields: { [id: string]: AutofillField },
options: GenerateFillScriptOptions,
): Promise<AutofillScript> {
if (await this.configService.getFeatureFlag(FeatureFlag.GenerateIdentityFillScriptRefactor)) {
return this._generateIdentityFillScript(fillScript, pageDetails, filledFields, options);
}
if (!options.cipher.identity) {
return null;
}
const fillFields: { [id: string]: AutofillField } = {};
pageDetails.fields.forEach((f) => {
if (
AutofillService.isExcludedFieldType(f, AutoFillConstants.ExcludedAutofillTypes) ||
["current-password", "new-password"].includes(f.autoCompleteType)
) {
return;
}
for (let i = 0; i < IdentityAutoFillConstants.IdentityAttributes.length; i++) {
const attr = IdentityAutoFillConstants.IdentityAttributes[i];
// eslint-disable-next-line
if (!f.hasOwnProperty(attr) || !f[attr] || !f.viewable) {
continue;
}
// ref https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill
// ref https://developers.google.com/web/fundamentals/design-and-ux/input/forms/
if (
!fillFields.name &&
AutofillService.isFieldMatch(
f[attr],
IdentityAutoFillConstants.FullNameFieldNames,
IdentityAutoFillConstants.FullNameFieldNameValues,
)
) {
fillFields.name = f;
break;
} else if (
!fillFields.firstName &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.FirstnameFieldNames)
) {
fillFields.firstName = f;
break;
} else if (
!fillFields.middleName &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.MiddlenameFieldNames)
) {
fillFields.middleName = f;
break;
} else if (
!fillFields.lastName &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.LastnameFieldNames)
) {
fillFields.lastName = f;
break;
} else if (
!fillFields.title &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.TitleFieldNames)
) {
fillFields.title = f;
break;
} else if (
!fillFields.email &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.EmailFieldNames)
) {
fillFields.email = f;
break;
} else if (
!fillFields.address1 &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address1FieldNames)
) {
fillFields.address1 = f;
break;
} else if (
!fillFields.address2 &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address2FieldNames)
) {
fillFields.address2 = f;
break;
} else if (
!fillFields.address3 &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.Address3FieldNames)
) {
fillFields.address3 = f;
break;
} else if (
!fillFields.address &&
AutofillService.isFieldMatch(
f[attr],
IdentityAutoFillConstants.AddressFieldNames,
IdentityAutoFillConstants.AddressFieldNameValues,
)
) {
fillFields.address = f;
break;
} else if (
!fillFields.postalCode &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.PostalCodeFieldNames)
) {
fillFields.postalCode = f;
break;
} else if (
!fillFields.city &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CityFieldNames)
) {
fillFields.city = f;
break;
} else if (
!fillFields.state &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.StateFieldNames)
) {
fillFields.state = f;
break;
} else if (
!fillFields.country &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CountryFieldNames)
) {
fillFields.country = f;
break;
} else if (
!fillFields.phone &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.PhoneFieldNames)
) {
fillFields.phone = f;
break;
} else if (
!fillFields.username &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.UserNameFieldNames)
) {
fillFields.username = f;
break;
} else if (
!fillFields.company &&
AutofillService.isFieldMatch(f[attr], IdentityAutoFillConstants.CompanyFieldNames)
) {
fillFields.company = f;
break;
}
}
});
const identity = options.cipher.identity;
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "title");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "firstName");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "middleName");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "lastName");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "address1");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "address2");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "address3");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "city");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "postalCode");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "company");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "email");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "phone");
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "username");
let filledState = false;
if (fillFields.state && identity.state && identity.state.length > 2) {
const stateLower = identity.state.toLowerCase();
const isoState =
IdentityAutoFillConstants.IsoStates[stateLower] ||
IdentityAutoFillConstants.IsoProvinces[stateLower];
if (isoState) {
filledState = true;
this.makeScriptActionWithValue(fillScript, isoState, fillFields.state, filledFields);
}
}
if (!filledState) {
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "state");
}
let filledCountry = false;
if (fillFields.country && identity.country && identity.country.length > 2) {
const countryLower = identity.country.toLowerCase();
const isoCountry = IdentityAutoFillConstants.IsoCountries[countryLower];
if (isoCountry) {
filledCountry = true;
this.makeScriptActionWithValue(fillScript, isoCountry, fillFields.country, filledFields);
}
}
if (!filledCountry) {
this.makeScriptAction(fillScript, identity, fillFields, filledFields, "country");
}
if (fillFields.name && (identity.firstName || identity.lastName)) {
let fullName = "";
if (AutofillService.hasValue(identity.firstName)) {
fullName = identity.firstName;
}
if (AutofillService.hasValue(identity.middleName)) {
if (fullName !== "") {
fullName += " ";
}
fullName += identity.middleName;
}
if (AutofillService.hasValue(identity.lastName)) {
if (fullName !== "") {
fullName += " ";
}
fullName += identity.lastName;
}
this.makeScriptActionWithValue(fillScript, fullName, fillFields.name, filledFields);
}
if (fillFields.address && AutofillService.hasValue(identity.address1)) {
let address = "";
if (AutofillService.hasValue(identity.address1)) {
address = identity.address1;
}
if (AutofillService.hasValue(identity.address2)) {
if (address !== "") {
address += ", ";
}
address += identity.address2;
}
if (AutofillService.hasValue(identity.address3)) {
if (address !== "") {
address += ", ";
}
address += identity.address3;
}
this.makeScriptActionWithValue(fillScript, address, fillFields.address, filledFields);
}
return fillScript;
}
/**
* Generates the autofill script for the specified page details and identity cipher item.
*
@@ -1833,7 +1587,7 @@ export default class AutofillService implements AutofillServiceInterface {
* @param filledFields - The fields that have already been filled, passed between method references
* @param options - Contains data used to fill cipher items
*/
private _generateIdentityFillScript(
private generateIdentityFillScript(
fillScript: AutofillScript,
pageDetails: AutofillPageDetails,
filledFields: { [id: string]: AutofillField },

View File

@@ -7,13 +7,13 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import MainBackground from "../../background/main.background";
import IconDetails from "../../vault/background/models/icon-details";
import { BrowserApi } from "../browser/browser-api";
import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service";
export type BadgeOptions = {
tab?: chrome.tabs.Tab;
@@ -28,6 +28,7 @@ export class UpdateBadge {
private badgeAction: typeof chrome.action | typeof chrome.browserAction;
private sidebarAction: OperaSidebarAction | FirefoxSidebarAction;
private win: Window & typeof globalThis;
private platformUtilsService: PlatformUtilsService;
constructor(win: Window & typeof globalThis, services: MainBackground) {
this.badgeAction = BrowserApi.getBrowserAction();
@@ -38,6 +39,7 @@ export class UpdateBadge {
this.authService = services.authService;
this.cipherService = services.cipherService;
this.accountService = services.accountService;
this.platformUtilsService = services.platformUtilsService;
}
async run(opts?: { tabId?: number; windowId?: number }): Promise<void> {
@@ -129,7 +131,7 @@ export class UpdateBadge {
38: "/images/icon38" + iconSuffix + ".png",
},
};
if (windowId && BrowserPlatformUtilsService.isFirefox()) {
if (windowId && this.platformUtilsService.isFirefox()) {
options.windowId = windowId;
}
@@ -204,9 +206,7 @@ export class UpdateBadge {
}
private get useSyncApiCalls() {
return (
BrowserPlatformUtilsService.isFirefox() || BrowserPlatformUtilsService.isSafari(this.win)
);
return this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari();
}
private isOperaSidebar(

View File

@@ -126,12 +126,11 @@ describe("Browser Utils Service", () => {
configurable: true,
value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0",
});
jest.spyOn(BrowserPlatformUtilsService, "isFirefox");
browserPlatformUtilsService.getDevice();
expect(browserPlatformUtilsService.getDevice()).toBe(DeviceType.FirefoxExtension);
expect(BrowserPlatformUtilsService.isFirefox).toHaveBeenCalledTimes(1);
expect(browserPlatformUtilsService.isFirefox()).toBe(true);
});
});

View File

@@ -60,10 +60,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
return ClientType.Browser;
}
/**
* @deprecated Do not call this directly, use getDevice() instead
*/
static isFirefox(): boolean {
private static isFirefox(): boolean {
return (
navigator.userAgent.indexOf(" Firefox/") !== -1 ||
navigator.userAgent.indexOf(" Gecko/") !== -1
@@ -74,9 +71,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
return this.getDevice() === DeviceType.FirefoxExtension;
}
/**
* @deprecated Do not call this directly, use getDevice() instead
*/
private static isChrome(globalContext: Window | ServiceWorkerGlobalScope): boolean {
return globalContext.chrome && navigator.userAgent.indexOf(" Chrome/") !== -1;
}
@@ -85,9 +79,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
return this.getDevice() === DeviceType.ChromeExtension;
}
/**
* @deprecated Do not call this directly, use getDevice() instead
*/
private static isEdge(): boolean {
return navigator.userAgent.indexOf(" Edg/") !== -1;
}
@@ -96,9 +87,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
return this.getDevice() === DeviceType.EdgeExtension;
}
/**
* @deprecated Do not call this directly, use getDevice() instead
*/
private static isOpera(globalContext: Window | ServiceWorkerGlobalScope): boolean {
return (
!!globalContext.opr?.addons ||
@@ -111,9 +99,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
return this.getDevice() === DeviceType.OperaExtension;
}
/**
* @deprecated Do not call this directly, use getDevice() instead
*/
private static isVivaldi(): boolean {
return navigator.userAgent.indexOf(" Vivaldi/") !== -1;
}
@@ -122,10 +107,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
return this.getDevice() === DeviceType.VivaldiExtension;
}
/**
* @deprecated Do not call this directly, use getDevice() instead
*/
static isSafari(globalContext: Window | ServiceWorkerGlobalScope): boolean {
private static isSafari(globalContext: Window | ServiceWorkerGlobalScope): boolean {
// Opera masquerades as Safari, so make sure we're not there first
return (
!BrowserPlatformUtilsService.isOpera(globalContext) &&
@@ -137,6 +119,10 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
return navigator.userAgent.match("Version/([0-9.]*)")?.[1];
}
isSafari(): boolean {
return this.getDevice() === DeviceType.SafariExtension;
}
/**
* Safari previous to version 16.1 had a bug which caused artifacts on hover in large extension popups.
* https://bugs.webkit.org/show_bug.cgi?id=218704
@@ -151,10 +137,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
return parts?.[0] < 16 || (parts?.[0] === 16 && parts?.[1] === 0);
}
isSafari(): boolean {
return this.getDevice() === DeviceType.SafariExtension;
}
isIE(): boolean {
return false;
}

View File

@@ -1,8 +1,8 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Component, NgZone } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormsModule } from "@angular/forms";
import { Subject, Subscription, debounceTime, filter } from "rxjs";
import { Subject, Subscription, debounceTime, distinctUntilChanged, filter } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SearchModule } from "@bitwarden/components";
@@ -22,13 +22,16 @@ export class VaultV2SearchComponent {
private searchText$ = new Subject<string>();
constructor(private vaultPopupItemsService: VaultPopupItemsService) {
constructor(
private vaultPopupItemsService: VaultPopupItemsService,
private ngZone: NgZone,
) {
this.subscribeToLatestSearchText();
this.subscribeToApplyFilter();
}
onSearchTextChanged() {
this.vaultPopupItemsService.applyFilter(this.searchText);
this.searchText$.next(this.searchText);
}
subscribeToLatestSearchText(): Subscription {
@@ -44,9 +47,13 @@ export class VaultV2SearchComponent {
subscribeToApplyFilter(): Subscription {
return this.searchText$
.pipe(debounceTime(SearchTextDebounceInterval), takeUntilDestroyed())
.pipe(debounceTime(SearchTextDebounceInterval), distinctUntilChanged(), takeUntilDestroyed())
.subscribe((data) => {
this.vaultPopupItemsService.applyFilter(data);
this.ngZone.runOutsideAngular(() => {
this.ngZone.run(() => {
this.vaultPopupItemsService.applyFilter(data);
});
});
});
}
}

View File

@@ -498,9 +498,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.9.0"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "camino"

View File

@@ -18,7 +18,7 @@ base64 = "=0.22.1"
bindgen = "=0.71.1"
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "3d48f140fd506412d186203238993163a8c4e536" }
byteorder = "=1.5.0"
bytes = "=1.9.0"
bytes = "=1.10.1"
cbc = "=0.1.2"
core-foundation = "=0.10.0"
dirs = "=6.0.0"

View File

@@ -10,7 +10,7 @@ import { autostart } from "@bitwarden/desktop-napi";
import { Main } from "../main";
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
import { isFlatpak } from "../utils";
import { isFlatpak, isLinux, isSnapStore } from "../utils";
import { MenuUpdateRequest } from "./menu/menu.updater";
@@ -26,8 +26,11 @@ export class MessagingMain {
async init() {
this.scheduleNextSync();
if (process.platform === "linux") {
await this.desktopSettingsService.setOpenAtLogin(fs.existsSync(this.linuxStartupFile()));
if (isLinux()) {
// Flatpak and snap don't have access to or use the startup file. On flatpak, the autostart portal is used
if (!isFlatpak() && !isSnapStore()) {
await this.desktopSettingsService.setOpenAtLogin(fs.existsSync(this.linuxStartupFile()));
}
} else {
const loginSettings = app.getLoginItemSettings();
await this.desktopSettingsService.setOpenAtLogin(loginSettings.openAtLogin);

View File

@@ -188,13 +188,10 @@ export class DuckDuckGoMessageHandlerService {
}
try {
let decryptedResult = await this.encryptService.decryptString(
const decryptedResult = await this.decryptDuckDuckGoEncString(
message.encryptedCommand as EncString,
this.duckduckgoSharedSecret,
);
decryptedResult = this.trimNullCharsFromMessage(decryptedResult);
return JSON.parse(decryptedResult);
} catch {
this.sendResponse({
@@ -237,7 +234,46 @@ export class DuckDuckGoMessageHandlerService {
ipc.platform.nativeMessaging.sendReply(response);
}
// Trim all null bytes padded at the end of messages. This happens with C encryption libraries.
/*
* Bitwarden type 2 (AES256-CBC-HMAC256) uses PKCS7 padding.
* DuckDuckGo does not use PKCS7 padding; and instead fills the last CBC block with null bytes.
* ref: https://github.com/duckduckgo/apple-browsers/blob/04d678b447869c3a640714718a466b36407db8b6/macOS/DuckDuckGo/PasswordManager/Bitwarden/Services/BWEncryption.m#L141
*
* This is incompatible which means the default encryptService cannot be used to decrypt the message,
* a custom EncString decrypt operation is needed.
*
* This function also trims null characters that are a result of the null-padding from the end of the message.
*/
private async decryptDuckDuckGoEncString(
encString: EncString,
key: SymmetricCryptoKey,
): Promise<string> {
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
encString.data,
encString.iv,
encString.mac,
key,
);
const computedMac = await this.cryptoFunctionService.hmacFast(
fastParams.macData,
fastParams.macKey,
"sha256",
);
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
if (!macsEqual) {
return null;
}
const decryptedPaddedString = await this.cryptoFunctionService.aesDecryptFast({
mode: "cbc",
parameters: fastParams,
});
return this.trimNullCharsFromMessage(decryptedPaddedString);
}
// DuckDuckGo does not use PKCS7 padding, but instead leaves the values as null,
// so null characters need to be trimmed from the end of the message for the last
// CBC-block.
private trimNullCharsFromMessage(message: string): string {
const charNull = 0;
const charRightCurlyBrace = 125;

View File

@@ -37,6 +37,31 @@ export function getNestedCollectionTree(
return nodes;
}
export function getNestedCollectionTree_vNext(
collections: (CollectionView | CollectionAdminView)[],
): TreeNode<CollectionView | CollectionAdminView>[] {
if (!collections) {
return [];
}
// Collections need to be cloned because ServiceUtils.nestedTraverse actively
// modifies the names of collections.
// These changes risk affecting collections store in StateService.
const clonedCollections = collections
.sort((a, b) => a.name.localeCompare(b.name))
.map(cloneCollection);
const nodes: TreeNode<CollectionView | CollectionAdminView>[] = [];
clonedCollections.forEach((collection) => {
const parts =
collection.name != null
? collection.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter)
: [];
ServiceUtils.nestedTraverse_vNext(nodes, 0, parts, collection, null, NestingDelimiter);
});
return nodes;
}
export function getFlatCollectionTree(
nodes: TreeNode<CollectionAdminView>[],
): CollectionAdminView[];

View File

@@ -125,7 +125,11 @@ import {
BulkCollectionsDialogResult,
} from "./bulk-collections-dialog";
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
import { getNestedCollectionTree, getFlatCollectionTree } from "./utils";
import {
getNestedCollectionTree,
getFlatCollectionTree,
getNestedCollectionTree_vNext,
} from "./utils";
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
@@ -420,9 +424,16 @@ export class VaultComponent implements OnInit, OnDestroy {
}),
);
const nestedCollections$ = allCollections$.pipe(
map((collections) => getNestedCollectionTree(collections)),
shareReplay({ refCount: true, bufferSize: 1 }),
const nestedCollections$ = combineLatest([
this.allCollectionsWithoutUnassigned$,
this.configService.getFeatureFlag$(FeatureFlag.OptimizeNestedTraverseTypescript),
]).pipe(
map(
([collections, shouldOptimize]) =>
(shouldOptimize
? getNestedCollectionTree_vNext(collections)
: getNestedCollectionTree(collections)) as TreeNode<CollectionAdminView>[],
),
);
const collections$ = combineLatest([

View File

@@ -110,8 +110,10 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
protected rowHeight = 69;
protected rowHeightClass = `tw-h-[69px]`;
private organizationUsersCount = 0;
get occupiedSeatCount(): number {
return this.dataSource.activeUserCount;
return this.organizationUsersCount;
}
constructor(
@@ -218,6 +220,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
);
this.orgIsOnSecretsManagerStandalone = billingMetadata.isOnSecretsManagerStandalone;
this.organizationUsersCount = billingMetadata.organizationOccupiedSeats;
await this.load();

View File

@@ -1,6 +1,4 @@
<div class="tabbed-header">
<h1>{{ "changeMasterPassword" | i18n }}</h1>
</div>
<h1 class="tw-mt-6 tw-mb-2 tw-pb-2.5">{{ "changeMasterPassword" | i18n }}</h1>
<div class="tw-max-w-lg tw-mb-12">
<bit-callout type="warning">{{ "loggedOutWarning" | i18n }}</bit-callout>

View File

@@ -49,7 +49,9 @@ import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -82,6 +84,7 @@ import {
import {
getNestedCollectionTree,
getFlatCollectionTree,
getNestedCollectionTree_vNext,
} from "../../admin-console/organizations/collections";
import {
CollectionDialogAction,
@@ -270,6 +273,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private trialFlowService: TrialFlowService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
private billingNotificationService: BillingNotificationService,
private configService: ConfigService,
) {}
async ngOnInit() {
@@ -326,8 +330,15 @@ export class VaultComponent implements OnInit, OnDestroy {
const filter$ = this.routedVaultFilterService.filter$;
const allCollections$ = this.collectionService.decryptedCollections$;
const nestedCollections$ = allCollections$.pipe(
map((collections) => getNestedCollectionTree(collections)),
const nestedCollections$ = combineLatest([
allCollections$,
this.configService.getFeatureFlag$(FeatureFlag.OptimizeNestedTraverseTypescript),
]).pipe(
map(([collections, shouldOptimize]) =>
shouldOptimize
? getNestedCollectionTree_vNext(collections)
: getNestedCollectionTree(collections),
),
);
this.searchText$