From b00987180d8702ecf7908470a538e83cfcd00ce1 Mon Sep 17 00:00:00 2001
From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Date: Thu, 20 Nov 2025 08:26:47 -0600
Subject: [PATCH 1/8] [PM-26688][PM-27710] Delay skeletons from showing +
search (#17394)
* add custom operator for loading skeleton delays
* add `isCipherSearching$` observable to search service
* prevent vault skeleton from showing immediately
* add skeleton for search + delay to sends
* update fade-in-out component selector
* add fade-in-out component for generic use
* address memory leak by using defer to encapsulate `skeletonShownAt`
* add missing provider
---
.../popup/send-v2/send-v2.component.html | 4 +-
.../tools/popup/send-v2/send-v2.component.ts | 13 ++-
.../vault-fade-in-out.component.html | 1 +
.../vault-fade-in-out.component.ts | 20 ++++
.../vault-v2/vault-v2.component.html | 82 ++++++++-----
.../vault-v2/vault-v2.component.spec.ts | 5 +
.../components/vault-v2/vault-v2.component.ts | 22 +++-
.../src/vault/abstractions/search.service.ts | 3 +
.../src/vault/services/search.service.ts | 15 ++-
.../utils/skeleton-loading.operator.spec.ts | 109 ++++++++++++++++++
.../vault/utils/skeleton-loading.operator.ts | 59 ++++++++++
11 files changed, 295 insertions(+), 38 deletions(-)
create mode 100644 apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.html
create mode 100644 apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.ts
create mode 100644 libs/common/src/vault/utils/skeleton-loading.operator.spec.ts
create mode 100644 libs/common/src/vault/utils/skeleton-loading.operator.ts
diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.html b/apps/browser/src/tools/popup/send-v2/send-v2.component.html
index 0bcbd47a145..47ecd7564dc 100644
--- a/apps/browser/src/tools/popup/send-v2/send-v2.component.html
+++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.html
@@ -47,8 +47,8 @@
@if (showSkeletonsLoaders$ | async) {
-
+
-
+
}
diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts
index 43a1119deca..e3baba53c42 100644
--- a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts
+++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts
@@ -15,6 +15,8 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
+import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
+import { skeletonLoadingDelay } from "@bitwarden/common/vault/utils/skeleton-loading.operator";
import {
ButtonModule,
CalloutModule,
@@ -95,8 +97,16 @@ export class SendV2Component implements OnDestroy {
/** Skeleton Loading State */
protected showSkeletonsLoaders$ = combineLatest([
this.sendsLoading$,
+ this.searchService.isSendSearching$,
this.skeletonFeatureFlag$,
- ]).pipe(map(([loading, skeletonsEnabled]) => loading && skeletonsEnabled));
+ ]).pipe(
+ map(
+ ([loading, cipherSearching, skeletonsEnabled]) =>
+ (loading || cipherSearching) && skeletonsEnabled,
+ ),
+ distinctUntilChanged(),
+ skeletonLoadingDelay(),
+ );
protected title: string = "allSends";
protected noItemIcon = NoSendsIcon;
@@ -110,6 +120,7 @@ export class SendV2Component implements OnDestroy {
private policyService: PolicyService,
private accountService: AccountService,
private configService: ConfigService,
+ private searchService: SearchService,
) {
combineLatest([
this.sendItemsService.emptyList$,
diff --git a/apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.html b/apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.html
new file mode 100644
index 00000000000..6dbc7430638
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.html
@@ -0,0 +1 @@
+
diff --git a/apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.ts b/apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.ts
new file mode 100644
index 00000000000..a30a447833b
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/vault-fade-in-out/vault-fade-in-out.component.ts
@@ -0,0 +1,20 @@
+import { animate, style, transition, trigger } from "@angular/animations";
+import { ChangeDetectionStrategy, Component, HostBinding } from "@angular/core";
+
+@Component({
+ selector: "vault-fade-in-out",
+ templateUrl: "./vault-fade-in-out.component.html",
+ animations: [
+ trigger("fadeInOut", [
+ transition(":enter", [
+ style({ opacity: 0 }),
+ animate("100ms ease-in", style({ opacity: 1 })),
+ ]),
+ transition(":leave", [animate("300ms ease-out", style({ opacity: 0 }))]),
+ ]),
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class VaultFadeInOutComponent {
+ @HostBinding("@fadeInOut") fadeInOut = true;
+}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html
index faaa6a40e98..7a5a99c8100 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html
@@ -8,20 +8,32 @@
-
+
+
+
+
+ @if (skeletonFeatureFlag$ | async) {
+
+
+
+ } @else {
+
+ }
-
-
-
-
-
+
+
+
+
+
+
+
+
+ @if (skeletonFeatureFlag$ | async) {
+
+
+
+ } @else {
+
+ }
@if (showSkeletonsLoaders$ | async) {
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts
index 563ec5f9709..5563cd3033b 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts
@@ -23,6 +23,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
+import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { TaskService } from "@bitwarden/common/vault/tasks";
import { DialogService } from "@bitwarden/components";
@@ -259,6 +260,10 @@ describe("VaultV2Component", () => {
getFeatureFlag$: (_: string) => of(false),
},
},
+ {
+ provide: SearchService,
+ useValue: { isCipherSearching$: of(false) },
+ },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts
index 499e9b76757..471e6e70601 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts
@@ -32,8 +32,10 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
+import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
+import { skeletonLoadingDelay } from "@bitwarden/common/vault/utils/skeleton-loading.operator";
import {
ButtonModule,
DialogService,
@@ -54,6 +56,7 @@ import { VaultPopupListFiltersService } from "../../services/vault-popup-list-fi
import { VaultPopupLoadingService } from "../../services/vault-popup-loading.service";
import { VaultPopupScrollPositionService } from "../../services/vault-popup-scroll-position.service";
import { AtRiskPasswordCalloutComponent } from "../at-risk-callout/at-risk-password-callout.component";
+import { VaultFadeInOutComponent } from "../vault-fade-in-out/vault-fade-in-out.component";
import { VaultFadeInOutSkeletonComponent } from "../vault-fade-in-out-skeleton/vault-fade-in-out-skeleton.component";
import { VaultLoadingSkeletonComponent } from "../vault-loading-skeleton/vault-loading-skeleton.component";
@@ -100,6 +103,7 @@ type VaultState = UnionOfValues;
TypographyModule,
VaultLoadingSkeletonComponent,
VaultFadeInOutSkeletonComponent,
+ VaultFadeInOutComponent,
],
})
export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
@@ -129,7 +133,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
}),
);
- private skeletonFeatureFlag$ = this.configService.getFeatureFlag$(
+ protected skeletonFeatureFlag$ = this.configService.getFeatureFlag$(
FeatureFlag.VaultLoadingSkeletons,
);
@@ -183,9 +187,18 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
map(([loading, skeletonsEnabled]) => loading && !skeletonsEnabled),
);
- /** When true, show skeleton loading state */
- protected showSkeletonsLoaders$ = combineLatest([this.loading$, this.skeletonFeatureFlag$]).pipe(
- map(([loading, skeletonsEnabled]) => loading && skeletonsEnabled),
+ /** When true, show skeleton loading state with debouncing to prevent flicker */
+ protected showSkeletonsLoaders$ = combineLatest([
+ this.loading$,
+ this.searchService.isCipherSearching$,
+ this.skeletonFeatureFlag$,
+ ]).pipe(
+ map(
+ ([loading, cipherSearching, skeletonsEnabled]) =>
+ (loading || cipherSearching) && skeletonsEnabled,
+ ),
+ distinctUntilChanged(),
+ skeletonLoadingDelay(),
);
protected newItemItemValues$: Observable =
@@ -228,6 +241,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
private liveAnnouncer: LiveAnnouncer,
private i18nService: I18nService,
private configService: ConfigService,
+ private searchService: SearchService,
) {
combineLatest([
this.vaultPopupItemsService.emptyVault$,
diff --git a/libs/common/src/vault/abstractions/search.service.ts b/libs/common/src/vault/abstractions/search.service.ts
index 233dee9ec75..29575ec3af9 100644
--- a/libs/common/src/vault/abstractions/search.service.ts
+++ b/libs/common/src/vault/abstractions/search.service.ts
@@ -6,6 +6,9 @@ import { CipherView } from "../models/view/cipher.view";
import { CipherViewLike } from "../utils/cipher-view-like-utils";
export abstract class SearchService {
+ abstract isCipherSearching$: Observable;
+ abstract isSendSearching$: Observable;
+
abstract indexedEntityId$(userId: UserId): Observable;
abstract clearIndex(userId: UserId): Promise;
diff --git a/libs/common/src/vault/services/search.service.ts b/libs/common/src/vault/services/search.service.ts
index 80fddda42d5..0b34bd3863f 100644
--- a/libs/common/src/vault/services/search.service.ts
+++ b/libs/common/src/vault/services/search.service.ts
@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as lunr from "lunr";
-import { Observable, firstValueFrom, map } from "rxjs";
+import { BehaviorSubject, Observable, firstValueFrom, map } from "rxjs";
import { Jsonify } from "type-fest";
import { perUserCache$ } from "@bitwarden/common/vault/utils/observable-utilities";
@@ -81,6 +81,12 @@ export class SearchService implements SearchServiceAbstraction {
private readonly defaultSearchableMinLength: number = 2;
private searchableMinLength: number = this.defaultSearchableMinLength;
+ private _isCipherSearching$ = new BehaviorSubject(false);
+ isCipherSearching$: Observable = this._isCipherSearching$.asObservable();
+
+ private _isSendSearching$ = new BehaviorSubject(false);
+ isSendSearching$: Observable = this._isSendSearching$.asObservable();
+
constructor(
private logService: LogService,
private i18nService: I18nService,
@@ -223,6 +229,7 @@ export class SearchService implements SearchServiceAbstraction {
filter: ((cipher: C) => boolean) | ((cipher: C) => boolean)[] = null,
ciphers: C[],
): Promise {
+ this._isCipherSearching$.next(true);
const results: C[] = [];
const searchStartTime = performance.now();
if (query != null) {
@@ -243,6 +250,7 @@ export class SearchService implements SearchServiceAbstraction {
}
if (!(await this.isSearchable(userId, query))) {
+ this._isCipherSearching$.next(false);
return ciphers;
}
@@ -258,6 +266,7 @@ export class SearchService implements SearchServiceAbstraction {
// Fall back to basic search if index is not available
const basicResults = this.searchCiphersBasic(ciphers, query);
this.logService.measure(searchStartTime, "Vault", "SearchService", "basic search complete");
+ this._isCipherSearching$.next(false);
return basicResults;
}
@@ -293,6 +302,7 @@ export class SearchService implements SearchServiceAbstraction {
});
}
this.logService.measure(searchStartTime, "Vault", "SearchService", "search complete");
+ this._isCipherSearching$.next(false);
return results;
}
@@ -335,8 +345,10 @@ export class SearchService implements SearchServiceAbstraction {
}
searchSends(sends: SendView[], query: string) {
+ this._isSendSearching$.next(true);
query = SearchService.normalizeSearchQuery(query.trim().toLocaleLowerCase());
if (query === null) {
+ this._isSendSearching$.next(false);
return sends;
}
const sendsMatched: SendView[] = [];
@@ -359,6 +371,7 @@ export class SearchService implements SearchServiceAbstraction {
lowPriorityMatched.push(s);
}
});
+ this._isSendSearching$.next(false);
return sendsMatched.concat(lowPriorityMatched);
}
diff --git a/libs/common/src/vault/utils/skeleton-loading.operator.spec.ts b/libs/common/src/vault/utils/skeleton-loading.operator.spec.ts
new file mode 100644
index 00000000000..3ba790f64cb
--- /dev/null
+++ b/libs/common/src/vault/utils/skeleton-loading.operator.spec.ts
@@ -0,0 +1,109 @@
+import { BehaviorSubject } from "rxjs";
+
+import { skeletonLoadingDelay } from "./skeleton-loading.operator";
+
+describe("skeletonLoadingDelay", () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.clearAllTimers();
+ jest.useRealTimers();
+ });
+
+ it("returns false immediately when starting with false", () => {
+ const source$ = new BehaviorSubject(false);
+ const results: boolean[] = [];
+
+ source$.pipe(skeletonLoadingDelay()).subscribe((value) => results.push(value));
+
+ expect(results).toEqual([false]);
+ });
+
+ it("waits 1 second before returning true when starting with true", () => {
+ const source$ = new BehaviorSubject(true);
+ const results: boolean[] = [];
+
+ source$.pipe(skeletonLoadingDelay()).subscribe((value) => results.push(value));
+
+ expect(results).toEqual([]);
+
+ jest.advanceTimersByTime(999);
+ expect(results).toEqual([]);
+
+ jest.advanceTimersByTime(1);
+ expect(results).toEqual([true]);
+ });
+
+ it("cancels if source becomes false before show delay completes", () => {
+ const source$ = new BehaviorSubject(true);
+ const results: boolean[] = [];
+
+ source$.pipe(skeletonLoadingDelay()).subscribe((value) => results.push(value));
+
+ jest.advanceTimersByTime(500);
+ source$.next(false);
+
+ expect(results).toEqual([false]);
+
+ jest.advanceTimersByTime(1000);
+ expect(results).toEqual([false]);
+ });
+
+ it("delays hiding if minimum display time has not elapsed", () => {
+ const source$ = new BehaviorSubject(true);
+ const results: boolean[] = [];
+
+ source$.pipe(skeletonLoadingDelay()).subscribe((value) => results.push(value));
+
+ jest.advanceTimersByTime(1000);
+ expect(results).toEqual([true]);
+
+ source$.next(false);
+
+ expect(results).toEqual([true]);
+
+ jest.advanceTimersByTime(1000);
+ expect(results).toEqual([true, false]);
+ });
+
+ it("handles rapid true->false->true transitions", () => {
+ const source$ = new BehaviorSubject(true);
+ const results: boolean[] = [];
+
+ source$.pipe(skeletonLoadingDelay()).subscribe((value) => results.push(value));
+
+ jest.advanceTimersByTime(500);
+ expect(results).toEqual([]);
+
+ source$.next(false);
+ expect(results).toEqual([false]);
+
+ source$.next(true);
+
+ jest.advanceTimersByTime(999);
+ expect(results).toEqual([false]);
+
+ jest.advanceTimersByTime(1);
+ expect(results).toEqual([false, true]);
+ });
+
+ it("allows for custom timings", () => {
+ const source$ = new BehaviorSubject(true);
+ const results: boolean[] = [];
+
+ source$.pipe(skeletonLoadingDelay(1000, 2000)).subscribe((value) => results.push(value));
+
+ jest.advanceTimersByTime(1000);
+ expect(results).toEqual([true]);
+
+ source$.next(false);
+
+ jest.advanceTimersByTime(1999);
+ expect(results).toEqual([true]);
+
+ jest.advanceTimersByTime(1);
+ expect(results).toEqual([true, false]);
+ });
+});
diff --git a/libs/common/src/vault/utils/skeleton-loading.operator.ts b/libs/common/src/vault/utils/skeleton-loading.operator.ts
new file mode 100644
index 00000000000..b9ff28f64b5
--- /dev/null
+++ b/libs/common/src/vault/utils/skeleton-loading.operator.ts
@@ -0,0 +1,59 @@
+import { defer, Observable, of, timer } from "rxjs";
+import { map, switchMap, tap } from "rxjs/operators";
+
+/**
+ * RxJS operator that adds skeleton loading delay behavior.
+ *
+ * - Waits 1 second before showing (prevents flashing for quick loads)
+ * - Ensures skeleton stays visible for at least 1 second once shown regardless of the source observable emissions
+ * - After the minimum display time, if the source is still true, continues to emit true until the source becomes false
+ * - False can only be emitted either:
+ * - Immediately when the source emits false before the skeleton is shown
+ * - After the minimum display time has passed once the skeleton is shown
+ */
+export function skeletonLoadingDelay(
+ showDelay = 1000,
+ minDisplayTime = 1000,
+): (source: Observable) => Observable {
+ return (source: Observable) => {
+ return defer(() => {
+ let skeletonShownAt: number | null = null;
+
+ return source.pipe(
+ switchMap((shouldShow): Observable => {
+ if (shouldShow) {
+ if (skeletonShownAt !== null) {
+ return of(true); // Already shown, continue showing
+ }
+
+ // Wait for delay, then mark the skeleton as shown and emit true
+ return timer(showDelay).pipe(
+ tap(() => {
+ skeletonShownAt = Date.now();
+ }),
+ map(() => true),
+ );
+ } else {
+ if (skeletonShownAt === null) {
+ // Skeleton not shown yet, can emit false immediately
+ return of(false);
+ }
+
+ // Skeleton shown, ensure minimum display time has passed
+ const elapsedTime = Date.now() - skeletonShownAt;
+ const remainingTime = Math.max(0, minDisplayTime - elapsedTime);
+
+ // Wait for remaining time to ensure minimum display time
+ return timer(remainingTime).pipe(
+ tap(() => {
+ // Reset the shown timestamp
+ skeletonShownAt = null;
+ }),
+ map(() => false),
+ );
+ }
+ }),
+ );
+ });
+ };
+}
From d7949ab2f3763be8599c848289fcb00c67f792ba Mon Sep 17 00:00:00 2001
From: Kyle Spearrin
Date: Thu, 20 Nov 2025 09:42:57 -0500
Subject: [PATCH 2/8] [PM-27766] Add policy for blocking account creation from
claimed domains (#17211)
* Added policy for blocking account creation for claimed domains.
* add feature flag
* fix desc
* learn more link
* fix localization key to learnMore
* onpush change detection
---
apps/web/src/locales/en/messages.json | 9 ++++++
...med-domain-account-creation.component.html | 15 +++++++++
...aimed-domain-account-creation.component.ts | 32 +++++++++++++++++++
.../policies/policy-edit-definitions/index.ts | 1 +
.../policies/policy-edit-register.ts | 2 ++
.../admin-console/enums/policy-type.enum.ts | 1 +
libs/common/src/enums/feature-flag.enum.ts | 2 ++
7 files changed, 62 insertions(+)
create mode 100644 bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/block-claimed-domain-account-creation.component.html
create mode 100644 bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/block-claimed-domain-account-creation.component.ts
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 1b0460e2aa6..59db19aa388 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -12122,6 +12122,15 @@
"startFreeFamiliesTrial": {
"message": "Start free Families trial"
},
+ "blockClaimedDomainAccountCreation": {
+ "message": "Block account creation for claimed domains"
+ },
+ "blockClaimedDomainAccountCreationDesc": {
+ "message": "Prevent users from creating accounts outside of your organization using email addresses from claimed domains."
+ },
+ "blockClaimedDomainAccountCreationPrerequisite": {
+ "message": "A domain must be claimed before activating this policy."
+ },
"unlockMethodNeededToChangeTimeoutActionDesc": {
"message": "Set up an unlock method to change your vault timeout action."
},
diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/block-claimed-domain-account-creation.component.html b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/block-claimed-domain-account-creation.component.html
new file mode 100644
index 00000000000..17225905995
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/block-claimed-domain-account-creation.component.html
@@ -0,0 +1,15 @@
+
+ {{ "blockClaimedDomainAccountCreationPrerequisite" | i18n }}
+ {{ "learnMore" | i18n }}
+
+
+
+
+ {{ "turnOn" | i18n }}
+
diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/block-claimed-domain-account-creation.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/block-claimed-domain-account-creation.component.ts
new file mode 100644
index 00000000000..5e2925aa0bb
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/block-claimed-domain-account-creation.component.ts
@@ -0,0 +1,32 @@
+import { ChangeDetectionStrategy, Component } from "@angular/core";
+import { map, Observable } from "rxjs";
+
+import { PolicyType } from "@bitwarden/common/admin-console/enums";
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import {
+ BasePolicyEditDefinition,
+ BasePolicyEditComponent,
+} from "@bitwarden/web-vault/app/admin-console/organizations/policies";
+import { SharedModule } from "@bitwarden/web-vault/app/shared";
+
+export class BlockClaimedDomainAccountCreationPolicy extends BasePolicyEditDefinition {
+ name = "blockClaimedDomainAccountCreation";
+ description = "blockClaimedDomainAccountCreationDesc";
+ type = PolicyType.BlockClaimedDomainAccountCreation;
+ component = BlockClaimedDomainAccountCreationPolicyComponent;
+
+ override display$(organization: Organization, configService: ConfigService): Observable {
+ return configService
+ .getFeatureFlag$(FeatureFlag.BlockClaimedDomainAccountCreation)
+ .pipe(map((enabled) => enabled && organization.useOrganizationDomains));
+ }
+}
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ templateUrl: "block-claimed-domain-account-creation.component.html",
+ imports: [SharedModule],
+})
+export class BlockClaimedDomainAccountCreationPolicyComponent extends BasePolicyEditComponent {}
diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/index.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/index.ts
index 52325eae160..b03f3680422 100644
--- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/index.ts
+++ b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-definitions/index.ts
@@ -1,3 +1,4 @@
export { ActivateAutofillPolicy } from "./activate-autofill.component";
export { AutomaticAppLoginPolicy } from "./automatic-app-login.component";
+export { BlockClaimedDomainAccountCreationPolicy } from "./block-claimed-domain-account-creation.component";
export { DisablePersonalVaultExportPolicy } from "./disable-personal-vault-export.component";
diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-register.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-register.ts
index 015b4fc17be..c2a31d936b8 100644
--- a/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-register.ts
+++ b/bitwarden_license/bit-web/src/app/admin-console/policies/policy-edit-register.ts
@@ -9,6 +9,7 @@ import { SessionTimeoutPolicy } from "../../key-management/policies/session-time
import {
ActivateAutofillPolicy,
AutomaticAppLoginPolicy,
+ BlockClaimedDomainAccountCreationPolicy,
DisablePersonalVaultExportPolicy,
} from "./policy-edit-definitions";
@@ -23,6 +24,7 @@ const policyEditRegister: BasePolicyEditDefinition[] = [
new FreeFamiliesSponsorshipPolicy(),
new ActivateAutofillPolicy(),
new AutomaticAppLoginPolicy(),
+ new BlockClaimedDomainAccountCreationPolicy(),
];
export const bitPolicyEditRegister = ossPolicyEditRegister.concat(policyEditRegister);
diff --git a/libs/common/src/admin-console/enums/policy-type.enum.ts b/libs/common/src/admin-console/enums/policy-type.enum.ts
index ae0070dda89..af8147c41e4 100644
--- a/libs/common/src/admin-console/enums/policy-type.enum.ts
+++ b/libs/common/src/admin-console/enums/policy-type.enum.ts
@@ -20,4 +20,5 @@ export enum PolicyType {
UriMatchDefaults = 16, // Sets the default URI matching strategy for all users within an organization
AutotypeDefaultSetting = 17, // Sets the default autotype setting for desktop app
AutoConfirm = 18, // Enables the auto confirmation feature for admins to enable in their client
+ BlockClaimedDomainAccountCreation = 19, // Prevents users from creating personal accounts using email addresses from verified domains
}
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index 7d2d831bfb3..d06a14d242f 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -13,6 +13,7 @@ export enum FeatureFlag {
/* Admin Console Team */
CreateDefaultLocation = "pm-19467-create-default-location",
AutoConfirm = "pm-19934-auto-confirm-organization-users",
+ BlockClaimedDomainAccountCreation = "block-claimed-domain-account-creation",
/* Auth */
PM22110_DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods",
@@ -91,6 +92,7 @@ export const DefaultFeatureFlagValue = {
/* Admin Console Team */
[FeatureFlag.CreateDefaultLocation]: FALSE,
[FeatureFlag.AutoConfirm]: FALSE,
+ [FeatureFlag.BlockClaimedDomainAccountCreation]: FALSE,
/* Autofill */
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
From a5caa194cdf010b509fb9dfa0003f5b46fbf6e1d Mon Sep 17 00:00:00 2001
From: Brandon Treston
Date: Thu, 20 Nov 2025 09:51:40 -0500
Subject: [PATCH 3/8] fix copy (#17504)
---
.../vnext-organization-data-ownership.component.html | 2 +-
apps/web/src/locales/en/messages.json | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.html
index 0abc40da683..bd2237bc2fd 100644
--- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.html
+++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-definitions/vnext-organization-data-ownership.component.html
@@ -1,5 +1,5 @@
- {{ "organizationDataOwnershipContent" | i18n }}
+ {{ "organizationDataOwnershipDescContent" | i18n }}
Date: Thu, 20 Nov 2025 16:31:05 +0100
Subject: [PATCH 4/8] Autofill/pm 25597 plex password generation (#16997)
* Correctly fill generated passwords and current password on plex.tv
* Correctly fill generated passwords and current password on plex.tv
* Leave existing forEach
* Add tests for changes
---
.../background/overlay.background.spec.ts | 6 +
.../autofill/background/overlay.background.ts | 2 +
.../services/abstractions/autofill.service.ts | 3 +
.../autofill-overlay-content.service.ts | 6 +
.../services/autofill.service.spec.ts | 209 +++++++++++++++++-
.../src/autofill/services/autofill.service.ts | 51 ++++-
6 files changed, 259 insertions(+), 18 deletions(-)
diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts
index 80e453e9e83..50fb291b121 100644
--- a/apps/browser/src/autofill/background/overlay.background.spec.ts
+++ b/apps/browser/src/autofill/background/overlay.background.spec.ts
@@ -3286,6 +3286,9 @@ describe("OverlayBackground", () => {
pageDetails: [pageDetailsForTab],
fillNewPassword: true,
allowTotpAutofill: true,
+ focusedFieldForm: undefined,
+ focusedFieldOpid: undefined,
+ inlineMenuFillType: undefined,
});
expect(overlayBackground["inlineMenuCiphers"].entries()).toStrictEqual(
new Map([
@@ -3680,6 +3683,9 @@ describe("OverlayBackground", () => {
pageDetails: [overlayBackground["pageDetailsForTab"][sender.tab.id].get(sender.frameId)],
fillNewPassword: true,
allowTotpAutofill: false,
+ focusedFieldForm: undefined,
+ focusedFieldOpid: undefined,
+ inlineMenuFillType: InlineMenuFillTypes.PasswordGeneration,
});
});
});
diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts
index f3278fa6b07..225cbbe66ca 100644
--- a/apps/browser/src/autofill/background/overlay.background.ts
+++ b/apps/browser/src/autofill/background/overlay.background.ts
@@ -1177,6 +1177,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
allowTotpAutofill: true,
focusedFieldForm: this.focusedFieldData?.focusedFieldForm,
focusedFieldOpid: this.focusedFieldData?.focusedFieldOpid,
+ inlineMenuFillType: this.focusedFieldData?.inlineMenuFillType,
});
if (totpCode) {
@@ -1863,6 +1864,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
allowTotpAutofill: false,
focusedFieldForm: this.focusedFieldData?.focusedFieldForm,
focusedFieldOpid: this.focusedFieldData?.focusedFieldOpid,
+ inlineMenuFillType: InlineMenuFillTypes.PasswordGeneration,
});
globalThis.setTimeout(async () => {
diff --git a/apps/browser/src/autofill/services/abstractions/autofill.service.ts b/apps/browser/src/autofill/services/abstractions/autofill.service.ts
index 85bf8c16610..05bfbf378a8 100644
--- a/apps/browser/src/autofill/services/abstractions/autofill.service.ts
+++ b/apps/browser/src/autofill/services/abstractions/autofill.service.ts
@@ -6,6 +6,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { AutofillMessageCommand } from "../../enums/autofill-message.enums";
+import { InlineMenuFillType } from "../../enums/autofill-overlay.enum";
import AutofillField from "../../models/autofill-field";
import AutofillForm from "../../models/autofill-form";
import AutofillPageDetails from "../../models/autofill-page-details";
@@ -30,6 +31,7 @@ export interface AutoFillOptions {
autoSubmitLogin?: boolean;
focusedFieldForm?: string;
focusedFieldOpid?: string;
+ inlineMenuFillType?: InlineMenuFillType;
}
export interface FormData {
@@ -49,6 +51,7 @@ export interface GenerateFillScriptOptions {
tabUrl: string;
defaultUriMatch: UriMatchStrategySetting;
focusedFieldOpid?: string;
+ inlineMenuFillType?: InlineMenuFillType;
}
export type CollectPageDetailsResponseMessage = {
diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
index 7854dc8e161..817a7cca43c 100644
--- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
+++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts
@@ -1118,6 +1118,12 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* @param autofillFieldData - Autofill field data captured from the form field element.
*/
private async setQualifiedLoginFillType(autofillFieldData: AutofillField) {
+ // Check if this is a current password field in a password change form
+ if (this.inlineMenuFieldQualificationService.isUpdateCurrentPasswordField(autofillFieldData)) {
+ autofillFieldData.inlineMenuFillType = InlineMenuFillTypes.CurrentPasswordUpdate;
+ return;
+ }
+
autofillFieldData.inlineMenuFillType = CipherType.Login;
autofillFieldData.showPasskeys = autofillFieldData.autoCompleteType.includes("webauthn");
diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts
index b436214f327..13e97766594 100644
--- a/apps/browser/src/autofill/services/autofill.service.spec.ts
+++ b/apps/browser/src/autofill/services/autofill.service.spec.ts
@@ -44,6 +44,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { BrowserApi } from "../../platform/browser/browser-api";
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums";
+import { InlineMenuFillTypes } from "../enums/autofill-overlay.enum";
import { AutofillPort } from "../enums/autofill-port.enum";
import AutofillField from "../models/autofill-field";
import AutofillPageDetails from "../models/autofill-page-details";
@@ -103,6 +104,15 @@ describe("AutofillService", () => {
beforeEach(() => {
configService = mock();
configService.getFeatureFlag$.mockImplementation(() => of(false));
+
+ // Initialize domainSettingsService BEFORE it's used
+ domainSettingsService = new DefaultDomainSettingsService(
+ fakeStateProvider,
+ policyService,
+ accountService,
+ );
+ domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
+
scriptInjectorService = new BrowserScriptInjectorService(
domainSettingsService,
platformUtilsService,
@@ -141,12 +151,6 @@ describe("AutofillService", () => {
userNotificationsSettings,
messageListener,
);
- domainSettingsService = new DefaultDomainSettingsService(
- fakeStateProvider,
- policyService,
- accountService,
- );
- domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
jest.spyOn(BrowserApi, "tabSendMessage");
});
@@ -2077,6 +2081,193 @@ describe("AutofillService", () => {
});
});
+ describe("given password generation with inlineMenuFillType", () => {
+ beforeEach(() => {
+ pageDetails.forms = undefined;
+ pageDetails.fields = []; // Clear fields to start fresh
+ options.inlineMenuFillType = InlineMenuFillTypes.PasswordGeneration;
+ options.cipher.login.totp = null; // Disable TOTP for these tests
+ });
+
+ it("includes all password fields from the same form when filling with password generation", async () => {
+ const newPasswordField = createAutofillFieldMock({
+ opid: "new-password",
+ type: "password",
+ form: "validFormId",
+ elementNumber: 2,
+ });
+ const confirmPasswordField = createAutofillFieldMock({
+ opid: "confirm-password",
+ type: "password",
+ form: "validFormId",
+ elementNumber: 3,
+ });
+ pageDetails.fields.push(newPasswordField, confirmPasswordField);
+ options.focusedFieldOpid = newPasswordField.opid;
+
+ await autofillService["generateLoginFillScript"](
+ fillScript,
+ pageDetails,
+ filledFields,
+ options,
+ );
+
+ expect(filledFields[newPasswordField.opid]).toBeDefined();
+ expect(filledFields[confirmPasswordField.opid]).toBeDefined();
+ });
+
+ it("finds username field for the first password field when generating passwords", async () => {
+ const newPasswordField = createAutofillFieldMock({
+ opid: "new-password",
+ type: "password",
+ form: "validFormId",
+ elementNumber: 2,
+ });
+ pageDetails.fields.push(newPasswordField);
+ options.focusedFieldOpid = newPasswordField.opid;
+ jest.spyOn(autofillService as any, "findUsernameField");
+
+ await autofillService["generateLoginFillScript"](
+ fillScript,
+ pageDetails,
+ filledFields,
+ options,
+ );
+
+ expect(autofillService["findUsernameField"]).toHaveBeenCalledWith(
+ pageDetails,
+ expect.objectContaining({ opid: newPasswordField.opid }),
+ false,
+ false,
+ true,
+ );
+ });
+
+ it("does not include password fields from different forms", async () => {
+ const formAPasswordField = createAutofillFieldMock({
+ opid: "form-a-password",
+ type: "password",
+ form: "formA",
+ elementNumber: 1,
+ });
+ const formBPasswordField = createAutofillFieldMock({
+ opid: "form-b-password",
+ type: "password",
+ form: "formB",
+ elementNumber: 2,
+ });
+ pageDetails.fields = [formAPasswordField, formBPasswordField];
+ options.focusedFieldOpid = formAPasswordField.opid;
+
+ await autofillService["generateLoginFillScript"](
+ fillScript,
+ pageDetails,
+ filledFields,
+ options,
+ );
+
+ expect(filledFields[formAPasswordField.opid]).toBeDefined();
+ expect(filledFields[formBPasswordField.opid]).toBeUndefined();
+ });
+ });
+
+ describe("given current password update with inlineMenuFillType", () => {
+ beforeEach(() => {
+ pageDetails.forms = undefined;
+ pageDetails.fields = []; // Clear fields to start fresh
+ options.inlineMenuFillType = InlineMenuFillTypes.CurrentPasswordUpdate;
+ options.cipher.login.totp = null; // Disable TOTP for these tests
+ });
+
+ it("includes all password fields from the same form when updating current password", async () => {
+ const currentPasswordField = createAutofillFieldMock({
+ opid: "current-password",
+ type: "password",
+ form: "validFormId",
+ elementNumber: 1,
+ });
+ const newPasswordField = createAutofillFieldMock({
+ opid: "new-password",
+ type: "password",
+ form: "validFormId",
+ elementNumber: 2,
+ });
+ const confirmPasswordField = createAutofillFieldMock({
+ opid: "confirm-password",
+ type: "password",
+ form: "validFormId",
+ elementNumber: 3,
+ });
+ pageDetails.fields.push(currentPasswordField, newPasswordField, confirmPasswordField);
+ options.focusedFieldOpid = currentPasswordField.opid;
+
+ await autofillService["generateLoginFillScript"](
+ fillScript,
+ pageDetails,
+ filledFields,
+ options,
+ );
+
+ expect(filledFields[currentPasswordField.opid]).toBeDefined();
+ expect(filledFields[newPasswordField.opid]).toBeDefined();
+ expect(filledFields[confirmPasswordField.opid]).toBeDefined();
+ });
+
+ it("includes all password fields from the same form without TOTP", async () => {
+ const currentPasswordField = createAutofillFieldMock({
+ opid: "current-password",
+ type: "password",
+ form: "validFormId",
+ elementNumber: 1,
+ });
+ const newPasswordField = createAutofillFieldMock({
+ opid: "new-password",
+ type: "password",
+ form: "validFormId",
+ elementNumber: 2,
+ });
+ pageDetails.fields.push(currentPasswordField, newPasswordField);
+ options.focusedFieldOpid = currentPasswordField.opid;
+
+ await autofillService["generateLoginFillScript"](
+ fillScript,
+ pageDetails,
+ filledFields,
+ options,
+ );
+
+ expect(filledFields[currentPasswordField.opid]).toBeDefined();
+ expect(filledFields[newPasswordField.opid]).toBeDefined();
+ });
+
+ it("does not include password fields from different forms during password update", async () => {
+ const formAPasswordField = createAutofillFieldMock({
+ opid: "form-a-password",
+ type: "password",
+ form: "formA",
+ elementNumber: 1,
+ });
+ const formBPasswordField = createAutofillFieldMock({
+ opid: "form-b-password",
+ type: "password",
+ form: "formB",
+ elementNumber: 2,
+ });
+ pageDetails.fields = [formAPasswordField, formBPasswordField];
+ options.focusedFieldOpid = formAPasswordField.opid;
+
+ await autofillService["generateLoginFillScript"](
+ fillScript,
+ pageDetails,
+ filledFields,
+ options,
+ );
+
+ expect(filledFields[formAPasswordField.opid]).toBeDefined();
+ expect(filledFields[formBPasswordField.opid]).toBeUndefined();
+ });
+ });
+
describe("given a set of page details that does not contain a password field", () => {
let emailField: AutofillField;
let emailFieldView: FieldView;
@@ -3140,12 +3331,16 @@ describe("AutofillService", () => {
"example.com",
"exampleapp.com",
]);
- domainSettingsService.equivalentDomains$ = of([["not-example.com"]]);
const pageUrl = "https://subdomain.example.com";
const tabUrl = "https://www.not-example.com";
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(false);
+ // Mock getUrlEquivalentDomains to return the expected domains
+ jest
+ .spyOn(domainSettingsService, "getUrlEquivalentDomains")
+ .mockReturnValue(of(equivalentDomains));
+
const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(
diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts
index fcc8861228b..010f5ea0f27 100644
--- a/apps/browser/src/autofill/services/autofill.service.ts
+++ b/apps/browser/src/autofill/services/autofill.service.ts
@@ -52,6 +52,7 @@ import { ScriptInjectorService } from "../../platform/services/abstractions/scri
// eslint-disable-next-line no-restricted-imports
import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window";
import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums";
+import { InlineMenuFillTypes } from "../enums/autofill-overlay.enum";
import { AutofillPort } from "../enums/autofill-port.enum";
import AutofillField from "../models/autofill-field";
import AutofillPageDetails from "../models/autofill-page-details";
@@ -452,6 +453,7 @@ export default class AutofillService implements AutofillServiceInterface {
tabUrl: tab.url,
defaultUriMatch: defaultUriMatch,
focusedFieldOpid: options.focusedFieldOpid,
+ inlineMenuFillType: options.inlineMenuFillType,
});
if (!fillScript || !fillScript.script || !fillScript.script.length) {
@@ -971,26 +973,53 @@ export default class AutofillService implements AutofillServiceInterface {
if (passwordFields.length && !passwords.length) {
// in the event that password fields exist but weren't processed within form elements.
- // select matching password if focused, otherwise first in prioritized list. for username, use focused field if it matches, otherwise find field before password.
- const passwordFieldToUse = focusedField
- ? prioritizedPasswordFields.find(passwordMatchesFocused) || prioritizedPasswordFields[0]
- : prioritizedPasswordFields[0];
+ const isPasswordGeneration =
+ options.inlineMenuFillType === InlineMenuFillTypes.PasswordGeneration;
+ const isCurrentPasswordUpdate =
+ options.inlineMenuFillType === InlineMenuFillTypes.CurrentPasswordUpdate;
- if (passwordFieldToUse) {
- passwords.push(passwordFieldToUse);
+ // For password generation or current password update, include all password fields from the same form
+ // This ensures we have access to all fields regardless of their login/registration classification
+ if ((isPasswordGeneration || isCurrentPasswordUpdate) && focusedField) {
+ // Add all password fields from the same form as the focused field
+ const focusedFieldForm = focusedField.form;
- if (login.username && passwordFieldToUse.elementNumber > 0) {
- username = getUsernameForPassword(passwordFieldToUse, true);
+ // Check both login and registration fields to ensure we get all password fields
+ const allPasswordFields = [...loginPasswordFields, ...registrationPasswordFields];
+ allPasswordFields.forEach((passField) => {
+ if (passField.form === focusedFieldForm) {
+ passwords.push(passField);
+ }
+ });
+ }
+
+ // If we didn't add any passwords above (either not password generation/update or no matching fields),
+ // select matching password if focused, otherwise first in prioritized list.
+ if (!passwords.length) {
+ const passwordFieldToUse = focusedField
+ ? prioritizedPasswordFields.find(passwordMatchesFocused) || prioritizedPasswordFields[0]
+ : prioritizedPasswordFields[0];
+
+ if (passwordFieldToUse) {
+ passwords.push(passwordFieldToUse);
+ }
+ }
+
+ // Handle username and TOTP for the first password field
+ const firstPasswordField = passwords[0];
+ if (firstPasswordField) {
+ if (login.username && firstPasswordField.elementNumber > 0) {
+ username = getUsernameForPassword(firstPasswordField, true);
if (username) {
usernames.set(username.opid, username);
}
}
- if (options.allowTotpAutofill && login.totp && passwordFieldToUse.elementNumber > 0) {
+ if (options.allowTotpAutofill && login.totp && firstPasswordField.elementNumber > 0) {
totp =
- isFocusedTotpField && passwordMatchesFocused(passwordFieldToUse)
+ isFocusedTotpField && passwordMatchesFocused(firstPasswordField)
? focusedField
- : this.findTotpField(pageDetails, passwordFieldToUse, false, false, true);
+ : this.findTotpField(pageDetails, firstPasswordField, false, false, true);
if (totp) {
totps.push(totp);
}
From 81453ede1bbdcf13bc0033a886995c9c3993b530 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 20 Nov 2025 11:45:21 -0500
Subject: [PATCH 5/8] [deps] Vault: Update koa to v2.16.2 [SECURITY] (#15807)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Matt Andreko
---
apps/cli/package.json | 2 +-
package-lock.json | 10 +++++-----
package.json | 2 +-
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/apps/cli/package.json b/apps/cli/package.json
index 00686959ef0..fc38440b70f 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -75,7 +75,7 @@
"inquirer": "8.2.6",
"jsdom": "26.1.0",
"jszip": "3.10.1",
- "koa": "2.16.1",
+ "koa": "2.16.2",
"koa-bodyparser": "4.4.1",
"koa-json": "2.0.2",
"lowdb": "1.0.0",
diff --git a/package-lock.json b/package-lock.json
index b017272cd77..dbb3fdb7e2d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -49,7 +49,7 @@
"inquirer": "8.2.6",
"jsdom": "26.1.0",
"jszip": "3.10.1",
- "koa": "2.16.1",
+ "koa": "2.16.2",
"koa-bodyparser": "4.4.1",
"koa-json": "2.0.2",
"lit": "3.3.0",
@@ -213,7 +213,7 @@
"inquirer": "8.2.6",
"jsdom": "26.1.0",
"jszip": "3.10.1",
- "koa": "2.16.1",
+ "koa": "2.16.2",
"koa-bodyparser": "4.4.1",
"koa-json": "2.0.2",
"lowdb": "1.0.0",
@@ -27947,9 +27947,9 @@
}
},
"node_modules/koa": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz",
- "integrity": "sha512-umfX9d3iuSxTQP4pnzLOz0HKnPg0FaUUIKcye2lOiz3KPu1Y3M3xlz76dISdFPQs37P9eJz1wUpcTS6KDPn9fA==",
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.2.tgz",
+ "integrity": "sha512-+CCssgnrWKx9aI3OeZwroa/ckG4JICxvIFnSiOUyl2Uv+UTI+xIw0FfFrWS7cQFpoePpr9o8csss7KzsTzNL8Q==",
"license": "MIT",
"dependencies": {
"accepts": "^1.3.5",
diff --git a/package.json b/package.json
index 54e2685bbec..5d23d6b9938 100644
--- a/package.json
+++ b/package.json
@@ -186,7 +186,7 @@
"inquirer": "8.2.6",
"jsdom": "26.1.0",
"jszip": "3.10.1",
- "koa": "2.16.1",
+ "koa": "2.16.2",
"koa-bodyparser": "4.4.1",
"koa-json": "2.0.2",
"lit": "3.3.0",
From 9afba33f58dd63206d5ebf24f95db1d1624cca17 Mon Sep 17 00:00:00 2001
From: Stephon Brown
Date: Thu, 20 Nov 2025 13:38:33 -0500
Subject: [PATCH 6/8] [PM-26044] Update Offboarding Survey for User and
Organization (#17472)
* feat(billing): update messages to add reasons
* feat(billing): update survey with switching reason based on param
* fix(billing): revert value of switching reasons
* fix(billing): revert removal of tooExpensive message
* fix(billing): Add plan type to params and update switching logic
* fix(billing): update to include logic
* fix(billing): PR feedback
---
...ganization-subscription-cloud.component.ts | 1 +
.../shared/offboarding-survey.component.html | 3 +-
.../shared/offboarding-survey.component.ts | 90 +++++++++++--------
apps/web/src/locales/en/messages.json | 8 ++
4 files changed, 66 insertions(+), 36 deletions(-)
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts
index 70e16ad3037..e0c1a12a80f 100644
--- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts
+++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts
@@ -344,6 +344,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
data: {
type: "Organization",
id: this.organizationId,
+ plan: this.sub.plan.type,
},
});
diff --git a/apps/web/src/app/billing/shared/offboarding-survey.component.html b/apps/web/src/app/billing/shared/offboarding-survey.component.html
index b69565d95fa..50cf71a03d5 100644
--- a/apps/web/src/app/billing/shared/offboarding-survey.component.html
+++ b/apps/web/src/app/billing/shared/offboarding-survey.component.html
@@ -21,7 +21,8 @@
{{
- "charactersCurrentAndMaximum" | i18n: formGroup.value.feedback.length : MaxFeedbackLength
+ "charactersCurrentAndMaximum"
+ | i18n: formGroup.value.feedback?.length ?? 0 : MaxFeedbackLength
}}
diff --git a/apps/web/src/app/billing/shared/offboarding-survey.component.ts b/apps/web/src/app/billing/shared/offboarding-survey.component.ts
index fe7d724a079..40e1572a3bb 100644
--- a/apps/web/src/app/billing/shared/offboarding-survey.component.ts
+++ b/apps/web/src/app/billing/shared/offboarding-survey.component.ts
@@ -1,9 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { Component, Inject } from "@angular/core";
+import { ChangeDetectionStrategy, Component, Inject } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
+import { PlanType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
@@ -21,6 +22,7 @@ type UserOffboardingParams = {
type OrganizationOffboardingParams = {
type: "Organization";
id: string;
+ plan: PlanType;
};
export type OffboardingSurveyDialogParams = UserOffboardingParams | OrganizationOffboardingParams;
@@ -46,50 +48,20 @@ export const openOffboardingSurvey = (
dialogConfig,
);
-// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
-// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-cancel-subscription-form",
templateUrl: "offboarding-survey.component.html",
+ changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class OffboardingSurveyComponent {
protected ResultType = OffboardingSurveyDialogResultType;
protected readonly MaxFeedbackLength = 400;
- protected readonly reasons: Reason[] = [
- {
- value: null,
- text: this.i18nService.t("selectPlaceholder"),
- },
- {
- value: "missing_features",
- text: this.i18nService.t("missingFeatures"),
- },
- {
- value: "switched_service",
- text: this.i18nService.t("movingToAnotherTool"),
- },
- {
- value: "too_complex",
- text: this.i18nService.t("tooDifficultToUse"),
- },
- {
- value: "unused",
- text: this.i18nService.t("notUsingEnough"),
- },
- {
- value: "too_expensive",
- text: this.i18nService.t("tooExpensive"),
- },
- {
- value: "other",
- text: this.i18nService.t("other"),
- },
- ];
+ protected readonly reasons: Reason[] = [];
protected formGroup = this.formBuilder.group({
- reason: [this.reasons[0].value, [Validators.required]],
+ reason: [null, [Validators.required]],
feedback: ["", [Validators.maxLength(this.MaxFeedbackLength)]],
});
@@ -101,7 +73,35 @@ export class OffboardingSurveyComponent {
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private toastService: ToastService,
- ) {}
+ ) {
+ this.reasons = [
+ {
+ value: null,
+ text: this.i18nService.t("selectPlaceholder"),
+ },
+ {
+ value: "missing_features",
+ text: this.i18nService.t("missingFeatures"),
+ },
+ {
+ value: "switched_service",
+ text: this.i18nService.t("movingToAnotherTool"),
+ },
+ {
+ value: "too_complex",
+ text: this.i18nService.t("tooDifficultToUse"),
+ },
+ {
+ value: "unused",
+ text: this.i18nService.t("notUsingEnough"),
+ },
+ this.getSwitchingReason(),
+ {
+ value: "other",
+ text: this.i18nService.t("other"),
+ },
+ ];
+ }
submit = async () => {
this.formGroup.markAllAsTouched();
@@ -127,4 +127,24 @@ export class OffboardingSurveyComponent {
this.dialogRef.close(this.ResultType.Submitted);
};
+
+ private getSwitchingReason(): Reason {
+ if (this.dialogParams.type === "User") {
+ return {
+ value: "too_expensive",
+ text: this.i18nService.t("switchToFreePlan"),
+ };
+ }
+
+ const isFamilyPlan = [
+ PlanType.FamiliesAnnually,
+ PlanType.FamiliesAnnually2019,
+ PlanType.FamiliesAnnually2025,
+ ].includes(this.dialogParams.plan);
+
+ return {
+ value: "too_expensive",
+ text: this.i18nService.t(isFamilyPlan ? "switchToFreeOrg" : "tooExpensive"),
+ };
+ }
}
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 0c87e00b26c..5cf1bea6fd8 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -9824,6 +9824,14 @@
"message": "Too expensive",
"description": "An option for the offboarding survey shown when a user cancels their subscription."
},
+ "switchToFreePlan": {
+ "message": "Switching to free plan",
+ "description": "An option for the offboarding survey shown when a user cancels their subscription."
+ },
+ "switchToFreeOrg": {
+ "message": "Switching to free organization",
+ "description": "An option for the offboarding survey shown when a user cancels their subscription."
+ },
"freeForOneYear": {
"message": "Free for 1 year"
},
From 43897df9ed783f5a38acdc6c14d45283a48ba968 Mon Sep 17 00:00:00 2001
From: Vijay Oommen
Date: Thu, 20 Nov 2025 12:52:23 -0600
Subject: [PATCH 7/8] [PM-27287] Items in My Items should show in Inactive 2FA
report (#17434)
---
.../reports/pages/cipher-report.component.ts | 29 +++++-----
...exposed-passwords-report.component.spec.ts | 1 +
...active-two-factor-report.component.spec.ts | 12 ++--
.../inactive-two-factor-report.component.ts | 10 ++--
.../inactive-two-factor-report.component.ts | 57 ++++++++++---------
5 files changed, 56 insertions(+), 53 deletions(-)
diff --git a/apps/web/src/app/dirt/reports/pages/cipher-report.component.ts b/apps/web/src/app/dirt/reports/pages/cipher-report.component.ts
index 69dd360ad31..d098be56663 100644
--- a/apps/web/src/app/dirt/reports/pages/cipher-report.component.ts
+++ b/apps/web/src/app/dirt/reports/pages/cipher-report.component.ts
@@ -1,5 +1,3 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { Directive, OnDestroy } from "@angular/core";
import {
BehaviorSubject,
@@ -36,7 +34,7 @@ import {
import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service";
@Directive()
-export class CipherReportComponent implements OnDestroy {
+export abstract class CipherReportComponent implements OnDestroy {
isAdminConsoleActive = false;
loading = false;
@@ -44,16 +42,16 @@ export class CipherReportComponent implements OnDestroy {
ciphers: CipherView[] = [];
allCiphers: CipherView[] = [];
dataSource = new TableDataSource();
- organization: Organization;
- organizations: Organization[];
+ organization: Organization | undefined = undefined;
+ organizations: Organization[] = [];
organizations$: Observable;
filterStatus: any = [0];
showFilterToggle: boolean = false;
vaultMsg: string = "vault";
- currentFilterStatus: number | string;
+ currentFilterStatus: number | string = 0;
protected filterOrgStatus$ = new BehaviorSubject(0);
- private destroyed$: Subject = new Subject();
+ protected destroyed$: Subject = new Subject();
private vaultItemDialogRef?: DialogRef | undefined;
constructor(
@@ -107,7 +105,7 @@ export class CipherReportComponent implements OnDestroy {
if (filterId === 0) {
cipherCount = this.allCiphers.length;
} else if (filterId === 1) {
- cipherCount = this.allCiphers.filter((c) => c.organizationId === null).length;
+ cipherCount = this.allCiphers.filter((c) => !c.organizationId).length;
} else {
this.organizations.filter((org: Organization) => {
if (org.id === filterId) {
@@ -121,9 +119,9 @@ export class CipherReportComponent implements OnDestroy {
}
async filterOrgToggle(status: any) {
- let filter = null;
+ let filter = (c: CipherView) => true;
if (typeof status === "number" && status === 1) {
- filter = (c: CipherView) => c.organizationId == null;
+ filter = (c: CipherView) => !c.organizationId;
} else if (typeof status === "string") {
const orgId = status as OrganizationId;
filter = (c: CipherView) => c.organizationId === orgId;
@@ -185,7 +183,7 @@ export class CipherReportComponent implements OnDestroy {
cipher: CipherView,
activeCollectionId?: CollectionId,
) {
- const disableForm = cipher ? !cipher.edit && !this.organization.canEditAllCiphers : false;
+ const disableForm = cipher ? !cipher.edit && !this.organization?.canEditAllCiphers : false;
this.vaultItemDialogRef = VaultItemDialogComponent.open(this.dialogService, {
mode,
@@ -230,10 +228,11 @@ export class CipherReportComponent implements OnDestroy {
let updatedCipher = await this.cipherService.get(cipher.id, activeUserId);
if (this.isAdminConsoleActive) {
- updatedCipher = await this.adminConsoleCipherFormConfigService.getCipher(
- cipher.id as CipherId,
- this.organization,
- );
+ updatedCipher =
+ (await this.adminConsoleCipherFormConfigService.getCipher(
+ cipher.id as CipherId,
+ this.organization!,
+ )) ?? updatedCipher;
}
// convert cipher to cipher view model
diff --git a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts
index 052e3bc7cfe..560245bdc34 100644
--- a/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts
+++ b/apps/web/src/app/dirt/reports/pages/exposed-passwords-report.component.spec.ts
@@ -90,6 +90,7 @@ describe("ExposedPasswordsReportComponent", () => {
});
beforeEach(() => {
+ jest.clearAllMocks();
fixture = TestBed.createComponent(ExposedPasswordsReportComponent);
component = fixture.componentInstance;
fixture.detectChanges();
diff --git a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts
index 80893737ffd..64a851e120e 100644
--- a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts
+++ b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.spec.ts
@@ -1,3 +1,4 @@
+import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { MockProxy, mock } from "jest-mock-extended";
import { of } from "rxjs";
@@ -29,14 +30,13 @@ describe("InactiveTwoFactorReportComponent", () => {
const userId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(userId);
- beforeEach(() => {
+ beforeEach(async () => {
let cipherFormConfigServiceMock: MockProxy;
organizationService = mock();
organizationService.organizations$.mockReturnValue(of([]));
syncServiceMock = mock();
- // 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
- TestBed.configureTestingModule({
+
+ await TestBed.configureTestingModule({
declarations: [InactiveTwoFactorReportComponent, I18nPipe],
providers: [
{
@@ -80,9 +80,7 @@ describe("InactiveTwoFactorReportComponent", () => {
useValue: adminConsoleCipherFormConfigServiceMock,
},
],
- schemas: [],
- // FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports
- errorOnUnknownElements: false,
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
});
diff --git a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts
index 2a8ec12ac6a..9d7de688f3e 100644
--- a/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts
+++ b/apps/web/src/app/dirt/reports/pages/inactive-two-factor-report.component.ts
@@ -1,6 +1,4 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { Component, OnInit } from "@angular/core";
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from "@angular/core";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -19,9 +17,8 @@ import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/se
import { CipherReportComponent } from "./cipher-report.component";
-// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
-// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-inactive-two-factor-report",
templateUrl: "inactive-two-factor-report.component.html",
standalone: false,
@@ -42,6 +39,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
syncService: SyncService,
cipherFormConfigService: CipherFormConfigService,
adminConsoleCipherFormConfigService: AdminConsoleCipherFormConfigService,
+ protected changeDetectorRef: ChangeDetectorRef,
) {
super(
cipherService,
@@ -86,6 +84,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
this.filterCiphersByOrg(inactive2faCiphers);
this.cipherDocs = docs;
+ this.changeDetectorRef.markForCheck();
}
}
@@ -157,6 +156,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
}
this.services.set(serviceData.domain, serviceData.documentation);
}
+ this.changeDetectorRef.markForCheck();
}
/**
diff --git a/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts
index fde9c35a6de..17555e617cb 100644
--- a/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts
+++ b/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts
@@ -1,16 +1,12 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { Component, OnInit } from "@angular/core";
+import { ChangeDetectorRef, Component, OnInit, ChangeDetectionStrategy } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
-import { firstValueFrom, map } from "rxjs";
+import { firstValueFrom, map, takeUntil } from "rxjs";
-import {
- getOrganizationById,
- OrganizationService,
-} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
+import { getById } from "@bitwarden/common/platform/misc";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
@@ -23,9 +19,8 @@ import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vau
import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service";
import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponent } from "../inactive-two-factor-report.component";
-// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
-// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-inactive-two-factor-report",
templateUrl: "../inactive-two-factor-report.component.html",
providers: [
@@ -44,7 +39,7 @@ export class InactiveTwoFactorReportComponent
implements OnInit
{
// Contains a list of ciphers, the user running the report, can manage
- private manageableCiphers: Cipher[];
+ private manageableCiphers: Cipher[] = [];
constructor(
cipherService: CipherService,
@@ -58,6 +53,7 @@ export class InactiveTwoFactorReportComponent
syncService: SyncService,
cipherFormConfigService: CipherFormConfigService,
adminConsoleCipherFormConfigService: AdminConsoleCipherFormConfigService,
+ protected changeDetectorRef: ChangeDetectorRef,
) {
super(
cipherService,
@@ -70,28 +66,37 @@ export class InactiveTwoFactorReportComponent
syncService,
cipherFormConfigService,
adminConsoleCipherFormConfigService,
+ changeDetectorRef,
);
}
async ngOnInit() {
this.isAdminConsoleActive = true;
- // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
- this.route.parent.parent.params.subscribe(async (params) => {
- const userId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
- this.organization = await firstValueFrom(
- this.organizationService
- .organizations$(userId)
- .pipe(getOrganizationById(params.organizationId)),
- );
- this.manageableCiphers = await this.cipherService.getAll(userId);
- await super.ngOnInit();
- });
+
+ this.route.parent?.parent?.params
+ ?.pipe(takeUntil(this.destroyed$))
+ // eslint-disable-next-line rxjs/no-async-subscribe
+ .subscribe(async (params) => {
+ const userId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ );
+
+ if (userId) {
+ this.organization = await firstValueFrom(
+ this.organizationService.organizations$(userId).pipe(getById(params.organizationId)),
+ );
+ this.manageableCiphers = await this.cipherService.getAll(userId);
+ await super.ngOnInit();
+ }
+ this.changeDetectorRef.markForCheck();
+ });
}
- getAllCiphers(): Promise {
- return this.cipherService.getAllFromApiForOrganization(this.organization.id);
+ async getAllCiphers(): Promise {
+ if (this.organization) {
+ return await this.cipherService.getAllFromApiForOrganization(this.organization.id, true);
+ }
+ return [];
}
protected canManageCipher(c: CipherView): boolean {
From 98401ccda151d60230911e6b8852b78306c3db5f Mon Sep 17 00:00:00 2001
From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
Date: Thu, 20 Nov 2025 15:22:48 -0500
Subject: [PATCH 8/8] PM-28506 - TwoFactorSetupYubikey - refactor yubikey form
to be rows with 1 field per row to allow remove button to be visible again.
(#17519)
---
.../two-factor-setup-yubikey.component.html | 32 +++++++++----------
1 file changed, 15 insertions(+), 17 deletions(-)
diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.html b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.html
index 172646f5d4d..8baf304969f 100644
--- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.html
+++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup-yubikey.component.html
@@ -25,23 +25,21 @@
{{ "twoFactorYubikeySaveForm" | i18n }}
-
-
-
-
{{ "yubikeyX" | i18n: (i + 1).toString() }}
-
-
-
-
- {{ keys[i].existingKey }}
-
-
+
+
+
{{ "yubikeyX" | i18n: (i + 1).toString() }}
+
+
+
+
+ {{ keys[i].existingKey }}
+