From 804ad79877fc4d1cebb2b2e987f12dca8d1130e4 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 7 Aug 2025 08:48:46 -0400 Subject: [PATCH 01/12] Fix extra signalr connection web (#15633) * Revert "fix(SignalR): Revert "[PM-23062] Fix extra signalr connections"" This reverts commit 97ec9a633988cbed7790ced508368e37384f5ca9. * Fix first login on web --- .../app/platform/web-environment.service.ts | 13 ++- .../config/config-api.service.abstraction.ts | 2 +- .../abstractions/environment.service.ts | 11 ++- .../services/config/config-api.service.ts | 2 +- .../services/config/config.service.spec.ts | 32 +++--- .../services/config/default-config.service.ts | 99 ++++++++++++------- .../services/default-environment.service.ts | 9 +- 7 files changed, 109 insertions(+), 59 deletions(-) diff --git a/apps/web/src/app/platform/web-environment.service.ts b/apps/web/src/app/platform/web-environment.service.ts index 1df842d6b31..4c4681ff715 100644 --- a/apps/web/src/app/platform/web-environment.service.ts +++ b/apps/web/src/app/platform/web-environment.service.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Router } from "@angular/router"; -import { firstValueFrom, ReplaySubject } from "rxjs"; +import { firstValueFrom, Observable, ReplaySubject } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { @@ -16,6 +16,7 @@ import { SelfHostedEnvironment, } from "@bitwarden/common/platform/services/default-environment.service"; import { StateProvider } from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/user-core"; export type WebRegionConfig = RegionConfig & { key: Region | string; // strings are used for custom environments @@ -27,6 +28,8 @@ export type WebRegionConfig = RegionConfig & { * Web specific environment service. Ensures that the urls are set from the window location. */ export class WebEnvironmentService extends DefaultEnvironmentService { + private _environmentSubject: ReplaySubject; + constructor( private win: Window, stateProvider: StateProvider, @@ -60,7 +63,9 @@ export class WebEnvironmentService extends DefaultEnvironmentService { // Override the environment observable with a replay subject const subject = new ReplaySubject(1); subject.next(environment); + this._environmentSubject = subject; this.environment$ = subject.asObservable(); + this.globalEnvironment$ = subject.asObservable(); } // Web setting env means navigating to a new location @@ -100,6 +105,12 @@ export class WebEnvironmentService extends DefaultEnvironmentService { // This return shouldn't matter as we are about to leave the current window return chosenRegionConfig.urls; } + + getEnvironment$(userId: UserId): Observable { + // Web does not support account switching, and even if it did, you'd be required to be the environment of where the application + // is running. + return this._environmentSubject.asObservable(); + } } export class WebCloudEnvironment extends CloudEnvironment { diff --git a/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts b/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts index 3c191f59ccc..0460e8c715f 100644 --- a/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts @@ -5,5 +5,5 @@ export abstract class ConfigApiServiceAbstraction { /** * Fetches the server configuration for the given user. If no user is provided, the configuration will not contain user-specific context. */ - abstract get(userId: UserId | undefined): Promise; + abstract get(userId: UserId | null): Promise; } diff --git a/libs/common/src/platform/abstractions/environment.service.ts b/libs/common/src/platform/abstractions/environment.service.ts index b8931656848..86a0fbea242 100644 --- a/libs/common/src/platform/abstractions/environment.service.ts +++ b/libs/common/src/platform/abstractions/environment.service.ts @@ -95,6 +95,13 @@ export interface Environment { */ export abstract class EnvironmentService { abstract environment$: Observable; + + /** + * The environment stored in global state, when a user signs in the state stored here will become + * their user environment. + */ + abstract globalEnvironment$: Observable; + abstract cloudWebVaultUrl$: Observable; /** @@ -125,12 +132,12 @@ export abstract class EnvironmentService { * @param userId - The user id to set the cloud web vault app URL for. If null or undefined the global environment is set. * @param region - The region of the cloud web vault app. */ - abstract setCloudRegion(userId: UserId, region: Region): Promise; + abstract setCloudRegion(userId: UserId | null, region: Region): Promise; /** * Get the environment from state. Useful if you need to get the environment for another user. */ - abstract getEnvironment$(userId: UserId): Observable; + abstract getEnvironment$(userId: UserId): Observable; /** * @deprecated Use {@link getEnvironment$} instead. diff --git a/libs/common/src/platform/services/config/config-api.service.ts b/libs/common/src/platform/services/config/config-api.service.ts index f283410acea..b7ecb9c8712 100644 --- a/libs/common/src/platform/services/config/config-api.service.ts +++ b/libs/common/src/platform/services/config/config-api.service.ts @@ -10,7 +10,7 @@ export class ConfigApiService implements ConfigApiServiceAbstraction { private tokenService: TokenService, ) {} - async get(userId: UserId | undefined): Promise { + async get(userId: UserId | null): Promise { // Authentication adds extra context to config responses, if the user has an access token, we want to use it // We don't particularly care about ensuring the token is valid and not expired, just that it exists const authed: boolean = diff --git a/libs/common/src/platform/services/config/config.service.spec.ts b/libs/common/src/platform/services/config/config.service.spec.ts index ea3b56a32f1..e8a1872c4c1 100644 --- a/libs/common/src/platform/services/config/config.service.spec.ts +++ b/libs/common/src/platform/services/config/config.service.spec.ts @@ -10,9 +10,9 @@ import { FakeGlobalState, FakeSingleUserState, FakeStateProvider, - awaitAsync, mockAccountServiceWith, } from "../../../../spec"; +import { Matrix } from "../../../../spec/matrix"; import { subscribeTo } from "../../../../spec/observable-tracker"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; @@ -74,7 +74,8 @@ describe("ConfigService", () => { }); beforeEach(() => { - environmentService.environment$ = environmentSubject; + Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject); + environmentService.globalEnvironment$ = environmentSubject; sut = new DefaultConfigService( configApiService, environmentService, @@ -98,9 +99,12 @@ describe("ConfigService", () => { : serverConfigFactory(activeApiUrl + userId, tooOld); const globalStored = configStateDescription === "missing" - ? {} + ? { + [activeApiUrl]: null, + } : { [activeApiUrl]: serverConfigFactory(activeApiUrl, tooOld), + [activeApiUrl + "0"]: serverConfigFactory(activeApiUrl + userId, tooOld), }; beforeEach(() => { @@ -108,11 +112,6 @@ describe("ConfigService", () => { userState.nextState(userStored); }); - // sanity check - test("authed and unauthorized state are different", () => { - expect(globalStored[activeApiUrl]).not.toEqual(userStored); - }); - describe("fail to fetch", () => { beforeEach(() => { configApiService.get.mockRejectedValue(new Error("Unable to fetch")); @@ -178,6 +177,7 @@ describe("ConfigService", () => { beforeEach(() => { globalState.stateSubject.next(globalStored); userState.nextState(userStored); + Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject); }); it("does not fetch from server", async () => { await firstValueFrom(sut.serverConfig$); @@ -189,21 +189,13 @@ describe("ConfigService", () => { const actual = await firstValueFrom(sut.serverConfig$); expect(actual).toEqual(activeUserId ? userStored : globalStored[activeApiUrl]); }); - - it("does not complete after emit", async () => { - const emissions = []; - const subscription = sut.serverConfig$.subscribe((v) => emissions.push(v)); - await awaitAsync(); - expect(emissions.length).toBe(1); - expect(subscription.closed).toBe(false); - }); }); }); }); it("gets global config when there is an locked active user", async () => { await accountService.switchAccount(userId); - environmentService.environment$ = of(environmentFactory(activeApiUrl)); + environmentService.globalEnvironment$ = of(environmentFactory(activeApiUrl)); globalState.stateSubject.next({ [activeApiUrl]: serverConfigFactory(activeApiUrl + "global"), @@ -236,7 +228,8 @@ describe("ConfigService", () => { beforeEach(() => { environmentSubject = new Subject(); - environmentService.environment$ = environmentSubject; + environmentService.globalEnvironment$ = environmentSubject; + Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject); sut = new DefaultConfigService( configApiService, environmentService, @@ -327,7 +320,8 @@ describe("ConfigService", () => { beforeEach(async () => { const config = serverConfigFactory("existing-data", tooOld); - environmentService.environment$ = environmentSubject; + environmentService.globalEnvironment$ = environmentSubject; + Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject); globalState.stateSubject.next({ [apiUrl(0)]: config }); userState.stateSubject.next({ diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index 33f86d30885..2dad227876e 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -1,17 +1,18 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { combineLatest, + distinctUntilChanged, firstValueFrom, map, mergeWith, NEVER, Observable, of, - shareReplay, + ReplaySubject, + share, Subject, switchMap, tap, + timer, } from "rxjs"; import { SemVer } from "semver"; @@ -50,11 +51,15 @@ export const GLOBAL_SERVER_CONFIGURATIONS = KeyDefinition.record { + return previous.getApiUrl() === current.getApiUrl(); +}; + // FIXME: currently we are limited to api requests for active users. Update to accept a UserId and APIUrl once ApiService supports it. export class DefaultConfigService implements ConfigService { - private failedFetchFallbackSubject = new Subject(); + private failedFetchFallbackSubject = new Subject(); - serverConfig$: Observable; + serverConfig$: Observable; serverSettings$: Observable; @@ -67,32 +72,61 @@ export class DefaultConfigService implements ConfigService { private stateProvider: StateProvider, private authService: AuthService, ) { - const userId$ = this.stateProvider.activeUserId$; - const authStatus$ = userId$.pipe( - switchMap((userId) => (userId == null ? of(null) : this.authService.authStatusFor$(userId))), + const globalConfig$ = this.environmentService.globalEnvironment$.pipe( + distinctUntilChanged(environmentComparer), + switchMap((environment) => + this.globalConfigFor$(environment.getApiUrl()).pipe( + map((config) => { + return [config, null as UserId | null, environment, config] as const; + }), + ), + ), ); - this.serverConfig$ = combineLatest([ - userId$, - this.environmentService.environment$, - authStatus$, - ]).pipe( - switchMap(([userId, environment, authStatus]) => { - if (userId == null || authStatus !== AuthenticationStatus.Unlocked) { - return this.globalConfigFor$(environment.getApiUrl()).pipe( - map((config) => [config, null, environment] as const), - ); + this.serverConfig$ = this.stateProvider.activeUserId$.pipe( + distinctUntilChanged(), + switchMap((userId) => { + if (userId == null) { + // Global + return globalConfig$; } - return this.userConfigFor$(userId).pipe( - map((config) => [config, userId, environment] as const), + return this.authService.authStatusFor$(userId).pipe( + map((authStatus) => authStatus === AuthenticationStatus.Unlocked), + distinctUntilChanged(), + switchMap((isUnlocked) => { + if (!isUnlocked) { + return globalConfig$; + } + + return combineLatest([ + this.environmentService + .getEnvironment$(userId) + .pipe(distinctUntilChanged(environmentComparer)), + this.userConfigFor$(userId), + ]).pipe( + switchMap(([environment, config]) => { + if (config == null) { + // If the user doesn't have any config yet, use the global config for that url as the fallback + return this.globalConfigFor$(environment.getApiUrl()).pipe( + map( + (globalConfig) => + [null as ServerConfig | null, userId, environment, globalConfig] as const, + ), + ); + } + + return of([config, userId, environment, config] as const); + }), + ); + }), ); }), tap(async (rec) => { - const [existingConfig, userId, environment] = rec; + const [existingConfig, userId, environment, fallbackConfig] = rec; // Grab new config if older retrieval interval if (!existingConfig || this.olderThanRetrievalInterval(existingConfig.utcDate)) { - await this.renewConfig(existingConfig, userId, environment); + await this.renewConfig(existingConfig, userId, environment, fallbackConfig); } }), switchMap(([existingConfig]) => { @@ -106,7 +140,7 @@ export class DefaultConfigService implements ConfigService { }), // If fetch fails, we'll emit on this subject to fallback to the existing config mergeWith(this.failedFetchFallbackSubject), - shareReplay({ refCount: true, bufferSize: 1 }), + share({ connector: () => new ReplaySubject(1), resetOnRefCountZero: () => timer(1000) }), ); this.cloudRegion$ = this.serverConfig$.pipe( @@ -155,19 +189,18 @@ export class DefaultConfigService implements ConfigService { // Updates the on-disk configuration with a newly retrieved configuration private async renewConfig( - existingConfig: ServerConfig, - userId: UserId, + existingConfig: ServerConfig | null, + userId: UserId | null, environment: Environment, + fallbackConfig: ServerConfig | null, ): Promise { try { // Feature flags often have a big impact on user experience, lets ensure we return some value // somewhat quickly even though it may not be accurate, we won't cancel the HTTP request // though so that hopefully it can have finished and hydrated a more accurate value. const handle = setTimeout(() => { - this.logService.info( - "Self-host environment did not respond in time, emitting previous config.", - ); - this.failedFetchFallbackSubject.next(existingConfig); + this.logService.info("Environment did not respond in time, emitting previous config."); + this.failedFetchFallbackSubject.next(fallbackConfig); }, SLOW_EMISSION_GUARD); const response = await this.configApiService.get(userId); clearTimeout(handle); @@ -195,17 +228,17 @@ export class DefaultConfigService implements ConfigService { // mutate error to be handled by catchError this.logService.error(`Unable to fetch ServerConfig from ${environment.getApiUrl()}`, e); // Emit the existing config - this.failedFetchFallbackSubject.next(existingConfig); + this.failedFetchFallbackSubject.next(fallbackConfig); } } - private globalConfigFor$(apiUrl: string): Observable { + private globalConfigFor$(apiUrl: string): Observable { return this.stateProvider .getGlobal(GLOBAL_SERVER_CONFIGURATIONS) - .state$.pipe(map((configs) => configs?.[apiUrl])); + .state$.pipe(map((configs) => configs?.[apiUrl] ?? null)); } - private userConfigFor$(userId: UserId): Observable { + private userConfigFor$(userId: UserId): Observable { return this.stateProvider.getUser(userId, USER_SERVER_CONFIG).state$; } } diff --git a/libs/common/src/platform/services/default-environment.service.ts b/libs/common/src/platform/services/default-environment.service.ts index df55693ba0b..4a1af68505a 100644 --- a/libs/common/src/platform/services/default-environment.service.ts +++ b/libs/common/src/platform/services/default-environment.service.ts @@ -133,6 +133,7 @@ export class DefaultEnvironmentService implements EnvironmentService { ); environment$: Observable; + globalEnvironment$: Observable; cloudWebVaultUrl$: Observable; constructor( @@ -148,6 +149,10 @@ export class DefaultEnvironmentService implements EnvironmentService { distinctUntilChanged((oldUserId: UserId, newUserId: UserId) => oldUserId == newUserId), ); + this.globalEnvironment$ = this.stateProvider + .getGlobal(GLOBAL_ENVIRONMENT_KEY) + .state$.pipe(map((state) => this.buildEnvironment(state?.region, state?.urls))); + this.environment$ = account$.pipe( switchMap((userId) => { const t = userId @@ -263,7 +268,7 @@ export class DefaultEnvironmentService implements EnvironmentService { return new SelfHostedEnvironment(urls); } - async setCloudRegion(userId: UserId, region: CloudRegion) { + async setCloudRegion(userId: UserId | null, region: CloudRegion) { if (userId == null) { await this.globalCloudRegionState.update(() => region); } else { @@ -271,7 +276,7 @@ export class DefaultEnvironmentService implements EnvironmentService { } } - getEnvironment$(userId: UserId): Observable { + getEnvironment$(userId: UserId): Observable { return this.stateProvider.getUser(userId, USER_ENVIRONMENT_KEY).state$.pipe( map((state) => { return this.buildEnvironment(state?.region, state?.urls); From c13f8241c6b59ba22c816088ebc7b1db544b6589 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 7 Aug 2025 14:50:52 +0200 Subject: [PATCH 02/12] Remove worker js on safari (#15914) --- apps/browser/src/safari/desktop.xcodeproj/project.pbxproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/browser/src/safari/desktop.xcodeproj/project.pbxproj b/apps/browser/src/safari/desktop.xcodeproj/project.pbxproj index 7642e7d1859..05e6e8be978 100644 --- a/apps/browser/src/safari/desktop.xcodeproj/project.pbxproj +++ b/apps/browser/src/safari/desktop.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 03100CAF291891F4008E14EF /* encrypt-worker.js in Resources */ = {isa = PBXBuildFile; fileRef = 03100CAE291891F4008E14EF /* encrypt-worker.js */; }; 55BC93932CB4268A008CA4C6 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 55BC93922CB4268A008CA4C6 /* assets */; }; 55E0374D2577FA6B00979016 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E0374C2577FA6B00979016 /* AppDelegate.swift */; }; 55E037502577FA6B00979016 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 55E0374E2577FA6B00979016 /* Main.storyboard */; }; @@ -53,7 +52,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 03100CAE291891F4008E14EF /* encrypt-worker.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = "encrypt-worker.js"; path = "../../../build/encrypt-worker.js"; sourceTree = ""; }; 5508DD7926051B5900A85C58 /* libswiftAppKit.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftAppKit.tbd; path = usr/lib/swift/libswiftAppKit.tbd; sourceTree = SDKROOT; }; 55BC93922CB4268A008CA4C6 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = ../../../build/assets; sourceTree = ""; }; 55E037482577FA6B00979016 /* desktop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = desktop.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -155,7 +153,6 @@ isa = PBXGroup; children = ( 55BC93922CB4268A008CA4C6 /* assets */, - 03100CAE291891F4008E14EF /* encrypt-worker.js */, 55E037702577FA6F00979016 /* popup */, 55E037712577FA6F00979016 /* background.js */, 55E037722577FA6F00979016 /* images */, @@ -272,7 +269,6 @@ 55E037802577FA6F00979016 /* background.html in Resources */, 55E0377A2577FA6F00979016 /* background.js in Resources */, 55E037792577FA6F00979016 /* popup in Resources */, - 03100CAF291891F4008E14EF /* encrypt-worker.js in Resources */, 55BC93932CB4268A008CA4C6 /* assets in Resources */, 55E0377C2577FA6F00979016 /* notification in Resources */, 55E0377E2577FA6F00979016 /* vendor.js in Resources */, From 1ffe8e433f5d1be38b4b3a8c7e94e37bb9c76074 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Thu, 7 Aug 2025 14:01:33 +0000 Subject: [PATCH 03/12] Bumped client version(s) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- package-lock.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 70dd0d7a241..fe5f3099e14 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2025.7.0", + "version": "2025.7.1", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index d6cf535b6d2..9322d9167ff 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.7.0", + "version": "2025.7.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 615ae9115b4..3a1537dc4aa 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "Bitwarden", - "version": "2025.7.0", + "version": "2025.7.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/package-lock.json b/package-lock.json index 3bf9db97ddb..524e551914d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -192,7 +192,7 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2025.7.0" + "version": "2025.7.1" }, "apps/cli": { "name": "@bitwarden/cli", From 46046ca1fa796f7d8a8a4bc2cced97c1b17b915c Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Thu, 7 Aug 2025 08:19:35 -0700 Subject: [PATCH 04/12] fix(change-password-prompt) [Auth/PM-22356] Scope org invite email to submitted email (#15783) Adds a check to make sure that the email on the Org Invite matches the email submitted in the form. If it matches, only then do we apply the org invite to get the MP policies. But if the emails do not match, it means the user attempting to login is no longer the user who originally clicked the emailed org invite link. Therefore, we clear the Org Invite + Deep Link and allow the user to login as normal. --- .../login/web-login-component.service.spec.ts | 76 +++++++++++++------ .../login/web-login-component.service.ts | 19 ++++- .../angular/login/login-component.service.ts | 2 +- .../auth/src/angular/login/login.component.ts | 37 ++++----- 4 files changed, 88 insertions(+), 46 deletions(-) diff --git a/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts b/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts index 799e10bc15c..8edf98e569e 100644 --- a/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts +++ b/apps/web/src/app/auth/core/services/login/web-login-component.service.spec.ts @@ -9,6 +9,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -86,26 +87,29 @@ describe("WebLoginComponentService", () => { }); describe("getOrgPoliciesFromOrgInvite", () => { + const mockEmail = "test@example.com"; + const orgInvite: OrganizationInvite = { + organizationId: "org-id", + token: "token", + email: mockEmail, + organizationUserId: "org-user-id", + initOrganization: false, + orgSsoIdentifier: "sso-id", + orgUserHasExistingUser: false, + organizationName: "org-name", + }; + it("returns undefined if organization invite is null", async () => { organizationInviteService.getOrganizationInvite.mockResolvedValue(null); - const result = await service.getOrgPoliciesFromOrgInvite(); + const result = await service.getOrgPoliciesFromOrgInvite(mockEmail); expect(result).toBeUndefined(); }); it("logs an error if getPoliciesByToken throws an error", async () => { const error = new Error("Test error"); - organizationInviteService.getOrganizationInvite.mockResolvedValue({ - organizationId: "org-id", - token: "token", - email: "email", - organizationUserId: "org-user-id", - initOrganization: false, - orgSsoIdentifier: "sso-id", - orgUserHasExistingUser: false, - organizationName: "org-name", - }); + organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); policyApiService.getPoliciesByToken.mockRejectedValue(error); - await service.getOrgPoliciesFromOrgInvite(); + await service.getOrgPoliciesFromOrgInvite(mockEmail); expect(logService.error).toHaveBeenCalledWith(error); }); @@ -120,16 +124,7 @@ describe("WebLoginComponentService", () => { const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions(); resetPasswordPolicyOptions.autoEnrollEnabled = autoEnrollEnabled; - organizationInviteService.getOrganizationInvite.mockResolvedValue({ - organizationId: "org-id", - token: "token", - email: "email", - organizationUserId: "org-user-id", - initOrganization: false, - orgSsoIdentifier: "sso-id", - orgUserHasExistingUser: false, - organizationName: "org-name", - }); + organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); policyApiService.getPoliciesByToken.mockResolvedValue(policies); internalPolicyService.getResetPasswordPolicyOptions.mockReturnValue([ @@ -141,7 +136,7 @@ describe("WebLoginComponentService", () => { masterPasswordPolicyOptions, ); - const result = await service.getOrgPoliciesFromOrgInvite(); + const result = await service.getOrgPoliciesFromOrgInvite(mockEmail); expect(result).toEqual({ policies: policies, @@ -151,5 +146,40 @@ describe("WebLoginComponentService", () => { }); }, ); + + describe("given the orgInvite email does not match the provided email", () => { + const mockMismatchedEmail = "mismatched@example.com"; + it("should clear the login redirect URL and organization invite", async () => { + // Arrange + organizationInviteService.getOrganizationInvite.mockResolvedValue({ + ...orgInvite, + email: mockMismatchedEmail, + }); + + // Act + await service.getOrgPoliciesFromOrgInvite(mockEmail); + + // Assert + expect(routerService.getAndClearLoginRedirectUrl).toHaveBeenCalledTimes(1); + expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalledTimes(1); + }); + + it("should log an error and return undefined", async () => { + // Arrange + organizationInviteService.getOrganizationInvite.mockResolvedValue({ + ...orgInvite, + email: mockMismatchedEmail, + }); + + // Act + const result = await service.getOrgPoliciesFromOrgInvite(mockEmail); + + // Assert + expect(logService.error).toHaveBeenCalledWith( + `WebLoginComponentService.getOrgPoliciesFromOrgInvite: Email mismatch. Expected: ${mockMismatchedEmail}, Received: ${mockEmail}`, + ); + expect(result).toBeUndefined(); + }); + }); }); }); diff --git a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts index 4ee84ecfde2..5bea0908b0a 100644 --- a/apps/web/src/app/auth/core/services/login/web-login-component.service.ts +++ b/apps/web/src/app/auth/core/services/login/web-login-component.service.ts @@ -66,10 +66,27 @@ export class WebLoginComponentService return; } - async getOrgPoliciesFromOrgInvite(): Promise { + async getOrgPoliciesFromOrgInvite(email: string): Promise { const orgInvite = await this.organizationInviteService.getOrganizationInvite(); if (orgInvite != null) { + /** + * Check if the email on the org invite matches the email submitted in the login form. This is + * important because say userA at "userA@mail.com" clicks an emailed org invite link, but then + * on the login page form they change the email to "userB@mail.com". We don't want to apply the org + * invite in state to userB. Therefore we clear the login redirect url as well as the org invite, + * allowing userB to login as normal. + */ + if (orgInvite.email !== email.toLowerCase()) { + await this.routerService.getAndClearLoginRedirectUrl(); + await this.organizationInviteService.clearOrganizationInvitation(); + + this.logService.error( + `WebLoginComponentService.getOrgPoliciesFromOrgInvite: Email mismatch. Expected: ${orgInvite.email}, Received: ${email}`, + ); + return undefined; + } + let policies: Policy[]; try { diff --git a/libs/auth/src/angular/login/login-component.service.ts b/libs/auth/src/angular/login/login-component.service.ts index 796a01c71c3..5ca83c97c5f 100644 --- a/libs/auth/src/angular/login/login-component.service.ts +++ b/libs/auth/src/angular/login/login-component.service.ts @@ -23,7 +23,7 @@ export abstract class LoginComponentService { * Gets the organization policies if there is an organization invite. * - Used by: Web */ - getOrgPoliciesFromOrgInvite?: () => Promise; + getOrgPoliciesFromOrgInvite?: (email: string) => Promise; /** * Indicates whether login with passkey is supported on the given client diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 2a2be148a86..f2954933bc6 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -80,6 +80,7 @@ export class LoginComponent implements OnInit, OnDestroy { clientType: ClientType; ClientType = ClientType; + orgPoliciesFromInvite: PasswordPolicies | null = null; LoginUiState = LoginUiState; isKnownDevice = false; loginUiState: LoginUiState = LoginUiState.EMAIL_ENTRY; @@ -232,11 +233,12 @@ export class LoginComponent implements OnInit, OnDestroy { // Try to retrieve any org policies from an org invite now so we can send it to the // login strategies. Since it is optional and we only want to be doing this on the // web we will only send in content in the right context. - const orgPoliciesFromInvite = this.loginComponentService.getOrgPoliciesFromOrgInvite - ? await this.loginComponentService.getOrgPoliciesFromOrgInvite() + this.orgPoliciesFromInvite = this.loginComponentService.getOrgPoliciesFromOrgInvite + ? await this.loginComponentService.getOrgPoliciesFromOrgInvite(email) : null; - const orgMasterPasswordPolicyOptions = orgPoliciesFromInvite?.enforcedPasswordPolicyOptions; + const orgMasterPasswordPolicyOptions = + this.orgPoliciesFromInvite?.enforcedPasswordPolicyOptions; const credentials = new PasswordLoginCredentials( email, @@ -327,25 +329,18 @@ export class LoginComponent implements OnInit, OnDestroy { // TODO: PM-18269 - evaluate if we can combine this with the // password evaluation done in the password login strategy. - // If there's an existing org invite, use it to get the org's password policies - // so we can evaluate the MP against the org policies - if (this.loginComponentService.getOrgPoliciesFromOrgInvite) { - const orgPolicies: PasswordPolicies | null = - await this.loginComponentService.getOrgPoliciesFromOrgInvite(); + if (this.orgPoliciesFromInvite) { + // Since we have retrieved the policies, we can go ahead and set them into state for future use + // e.g., the change-password page currently only references state for policy data and + // doesn't fallback to pulling them from the server like it should if they are null. + await this.setPoliciesIntoState(authResult.userId, this.orgPoliciesFromInvite.policies); - if (orgPolicies) { - // Since we have retrieved the policies, we can go ahead and set them into state for future use - // e.g., the change-password page currently only references state for policy data and - // doesn't fallback to pulling them from the server like it should if they are null. - await this.setPoliciesIntoState(authResult.userId, orgPolicies.policies); - - const isPasswordChangeRequired = await this.isPasswordChangeRequiredByOrgPolicy( - orgPolicies.enforcedPasswordPolicyOptions, - ); - if (isPasswordChangeRequired) { - await this.router.navigate(["change-password"]); - return; - } + const isPasswordChangeRequired = await this.isPasswordChangeRequiredByOrgPolicy( + this.orgPoliciesFromInvite.enforcedPasswordPolicyOptions, + ); + if (isPasswordChangeRequired) { + await this.router.navigate(["change-password"]); + return; } } From f7169e909fec6cc72af101a9845c2206356b97d8 Mon Sep 17 00:00:00 2001 From: Miles Blackwood Date: Thu, 7 Aug 2025 13:12:05 -0400 Subject: [PATCH 05/12] Generate password triggers notifications (#15874) --- .../abstractions/notification.background.ts | 3 +- .../overlay-notifications.background.ts | 11 +---- .../overlay-notifications.background.spec.ts | 12 ------ .../overlay-notifications.background.ts | 16 +++----- .../background/overlay.background.spec.ts | 41 +++++++------------ .../autofill/background/overlay.background.ts | 22 ++++++---- .../autofill-overlay-content.service.ts | 10 +---- .../autofill-overlay-content.service.spec.ts | 24 +++++++++++ .../autofill-overlay-content.service.ts | 12 +++++- .../browser/src/background/main.background.ts | 3 -- 10 files changed, 75 insertions(+), 79 deletions(-) diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index f2152b44862..571d9fbaf5f 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -1,5 +1,6 @@ import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CollectionView } from "../../content/components/common-types"; @@ -17,7 +18,7 @@ interface NotificationQueueMessage { interface AddChangePasswordQueueMessage extends NotificationQueueMessage { type: "change"; - cipherId: string; + cipherId: CipherView["id"]; newPassword: string; } diff --git a/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts b/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts index d446e18b480..71452ec975a 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts @@ -28,19 +28,12 @@ export type ModifyLoginCipherFormData = { newPassword: string; }; -export type ModifyLoginCipherFormDataForTab = Map< - chrome.tabs.Tab["id"], - { uri: string; username: string; password: string; newPassword: string } ->; +export type ModifyLoginCipherFormDataForTab = Map; export type OverlayNotificationsExtensionMessage = { command: string; - uri?: string; - username?: string; - password?: string; - newPassword?: string; details?: AutofillPageDetails; -}; +} & ModifyLoginCipherFormData; type OverlayNotificationsMessageParams = { message: OverlayNotificationsExtensionMessage }; type OverlayNotificationSenderParams = { sender: chrome.runtime.MessageSender }; diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts index 00114330bc4..cf317de4fd2 100644 --- a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts @@ -1,12 +1,9 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants"; import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { EnvironmentServerConfigData } from "@bitwarden/common/platform/models/data/server-config.data"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { TaskService } from "@bitwarden/common/vault/tasks"; import { BrowserApi } from "../../platform/browser/browser-api"; import AutofillField from "../models/autofill-field"; @@ -27,9 +24,6 @@ import { OverlayNotificationsBackground } from "./overlay-notifications.backgrou describe("OverlayNotificationsBackground", () => { let logService: MockProxy; let notificationBackground: NotificationBackground; - let taskService: TaskService; - let accountService: AccountService; - let cipherService: CipherService; let getEnableChangedPasswordPromptSpy: jest.SpyInstance; let getEnableAddedLoginPromptSpy: jest.SpyInstance; let overlayNotificationsBackground: OverlayNotificationsBackground; @@ -38,9 +32,6 @@ describe("OverlayNotificationsBackground", () => { jest.useFakeTimers(); logService = mock(); notificationBackground = mock(); - taskService = mock(); - accountService = mock(); - cipherService = mock(); getEnableChangedPasswordPromptSpy = jest .spyOn(notificationBackground, "getEnableChangedPasswordPrompt") .mockResolvedValue(true); @@ -50,9 +41,6 @@ describe("OverlayNotificationsBackground", () => { overlayNotificationsBackground = new OverlayNotificationsBackground( logService, notificationBackground, - taskService, - accountService, - cipherService, ); await overlayNotificationsBackground.init(); }); diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts index 1d7f2b1f9d8..e7126a57e9f 100644 --- a/apps/browser/src/autofill/background/overlay-notifications.background.ts +++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts @@ -2,11 +2,8 @@ // @ts-strict-ignore import { Subject, switchMap, timer } from "rxjs"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { TaskService } from "@bitwarden/common/vault/tasks"; import { BrowserApi } from "../../platform/browser/browser-api"; import { NotificationType, NotificationTypes } from "../notification/abstractions/notification-bar"; @@ -31,6 +28,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg private notificationFallbackTimeout: number | NodeJS.Timeout | null; private readonly formSubmissionRequestMethods: Set = new Set(["POST", "PUT", "PATCH"]); private readonly extensionMessageHandlers: OverlayNotificationsExtensionMessageHandlers = { + generatedPasswordFilled: ({ message, sender }) => + this.storeModifiedLoginFormData(message, sender), formFieldSubmitted: ({ message, sender }) => this.storeModifiedLoginFormData(message, sender), collectPageDetailsResponse: ({ message, sender }) => this.handleCollectPageDetailsResponse(message, sender), @@ -39,9 +38,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg constructor( private logService: LogService, private notificationBackground: NotificationBackground, - private taskService: TaskService, - private accountService: AccountService, - private cipherService: CipherService, ) {} /** @@ -442,7 +438,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg } } - this.clearCompletedWebRequest(requestId, tab); + this.clearCompletedWebRequest(requestId, tab.id); return results.join(" "); }; @@ -482,11 +478,11 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg */ private clearCompletedWebRequest = ( requestId: chrome.webRequest.ResourceRequest["requestId"], - tab: chrome.tabs.Tab, + tabId: chrome.tabs.Tab["id"], ) => { this.activeFormSubmissionRequests.delete(requestId); - this.modifyLoginCipherFormData.delete(tab.id); - this.websiteOriginsWithFields.delete(tab.id); + this.modifyLoginCipherFormData.delete(tabId); + this.websiteOriginsWithFields.delete(tabId); this.setupWebRequestsListeners(); }; diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 92b2135c973..8bee4a4675d 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -49,7 +49,6 @@ import { MAX_SUB_FRAME_DEPTH, RedirectFocusDirection, } from "../enums/autofill-overlay.enum"; -import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service"; import { AutofillService } from "../services/abstractions/autofill.service"; import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; import { @@ -71,6 +70,7 @@ import { triggerWebRequestOnCompletedEvent, } from "../spec/testing-utils"; +import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background"; import { FocusedFieldData, InlineMenuPosition, @@ -2076,7 +2076,7 @@ describe("OverlayBackground", () => { const tab = createChromeTabMock({ id: 2 }); const sender = mock({ tab, frameId: 100 }); let focusedFieldData: FocusedFieldData; - let formData: InlineMenuFormFieldData; + let formData: ModifyLoginCipherFormData; beforeEach(async () => { await initOverlayElementPorts(); @@ -3651,6 +3651,18 @@ describe("OverlayBackground", () => { }); }); + it("sends a message to the tab to store modify login change when a password is generated", async () => { + jest.useFakeTimers(); + + sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey }); + + await flushPromises(); + jest.advanceTimersByTime(400); + await flushPromises(); + + expect(tabsSendMessageSpy.mock.lastCall[1].command).toBe("generatedPasswordModifyLogin"); + }); + it("filters the page details to only include the new password fields before filling", async () => { sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey }); await flushPromises(); @@ -3663,31 +3675,6 @@ describe("OverlayBackground", () => { allowTotpAutofill: false, }); }); - - it("opens the inline menu for fields that fill a generated password", async () => { - jest.useFakeTimers(); - const formData = { - uri: "https://example.com", - username: "username", - password: "password", - newPassword: "newPassword", - }; - tabsSendMessageSpy.mockImplementation((_tab, message) => { - if (message.command === "getInlineMenuFormFieldData") { - return Promise.resolve(formData); - } - - return Promise.resolve(); - }); - const openInlineMenuSpy = jest.spyOn(overlayBackground as any, "openInlineMenu"); - - sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey }); - await flushPromises(); - jest.advanceTimersByTime(400); - await flushPromises(); - - expect(openInlineMenuSpy).toHaveBeenCalled(); - }); }); }); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index f55b5c8cc3d..4027689f014 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -69,7 +69,6 @@ import { MAX_SUB_FRAME_DEPTH, } from "../enums/autofill-overlay.enum"; import AutofillField from "../models/autofill-field"; -import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service"; import { AutofillService, PageDetail } from "../services/abstractions/autofill.service"; import { InlineMenuFieldQualificationService } from "../services/abstractions/inline-menu-field-qualifications.service"; import { @@ -82,6 +81,7 @@ import { } from "../utils"; import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background"; +import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background"; import { BuildCipherDataParams, CloseInlineMenuMessage, @@ -1813,7 +1813,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { /** * Triggers a fill of the generated password into the current tab. Will trigger - * a focus of the last focused field after filling the password. + * a focus of the last focused field after filling the password. * * @param port - The port of the sender */ @@ -1857,10 +1857,16 @@ export class OverlayBackground implements OverlayBackgroundInterface { }); globalThis.setTimeout(async () => { - if (await this.shouldShowSaveLoginInlineMenuList(port.sender.tab)) { - await this.openInlineMenu(port.sender, true); - } - }, 300); + await BrowserApi.tabSendMessage( + port.sender.tab, + { + command: "generatedPasswordModifyLogin", + }, + { + frameId: this.focusedFieldData.frameId || 0, + }, + ); + }, 150); } /** @@ -1891,7 +1897,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { * * @param tab - The tab to get the form field data from */ - private async getInlineMenuFormFieldData(tab: chrome.tabs.Tab): Promise { + private async getInlineMenuFormFieldData( + tab: chrome.tabs.Tab, + ): Promise { return await BrowserApi.tabSendMessage( tab, { diff --git a/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts index fac6221790f..ddacb547908 100644 --- a/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts @@ -1,3 +1,4 @@ +import { ModifyLoginCipherFormData } from "../../background/abstractions/overlay-notifications.background"; import { SubFrameOffsetData } from "../../background/abstractions/overlay.background"; import { AutofillExtensionMessageParam } from "../../content/abstractions/autofill-init"; import AutofillField from "../../models/autofill-field"; @@ -8,13 +9,6 @@ export type SubFrameDataFromWindowMessage = SubFrameOffsetData & { subFrameDepth: number; }; -export type InlineMenuFormFieldData = { - uri: string; - username: string; - password: string; - newPassword: string; -}; - export type AutofillOverlayContentExtensionMessageHandlers = { [key: string]: CallableFunction; addNewVaultItemFromOverlay: ({ message }: AutofillExtensionMessageParam) => void; @@ -32,7 +26,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = { destroyAutofillInlineMenuListeners: () => void; getInlineMenuFormFieldData: ({ message, - }: AutofillExtensionMessageParam) => Promise; + }: AutofillExtensionMessageParam) => Promise; }; export interface AutofillOverlayContentService { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts index 730b002953b..96b05b81c96 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts @@ -3,6 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { EVENTS } from "@bitwarden/common/autofill/constants"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { ModifyLoginCipherFormData } from "../background/abstractions/overlay-notifications.background"; import AutofillInit from "../content/autofill-init"; import { AutofillOverlayElement, @@ -1750,6 +1751,29 @@ describe("AutofillOverlayContentService", () => { }); describe("extension onMessage handlers", () => { + describe("generatedPasswordModifyLogin", () => { + it("relays a message regarding password generation to store modified login data", async () => { + const formFieldData: ModifyLoginCipherFormData = { + newPassword: "newPassword", + password: "password", + uri: "http://localhost/", + username: "username", + }; + + jest + .spyOn(autofillOverlayContentService as any, "getFormFieldData") + .mockResolvedValue(formFieldData); + + sendMockExtensionMessage({ + command: "generatedPasswordModifyLogin", + }); + await flushPromises(); + + const resolvedValue = await sendExtensionMessageSpy.mock.calls[0][1]; + expect(resolvedValue).toEqual(formFieldData); + }); + }); + describe("addNewVaultItemFromOverlay message handler", () => { it("skips sending the message if the overlay list is not visible", async () => { jest 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 1a972e0eaa0..4db00901759 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -12,6 +12,7 @@ import { } from "@bitwarden/common/autofill/constants"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { ModifyLoginCipherFormData } from "../background/abstractions/overlay-notifications.background"; import { FocusedFieldData, NewCardCipherData, @@ -48,7 +49,6 @@ import { import { AutofillOverlayContentExtensionMessageHandlers, AutofillOverlayContentService as AutofillOverlayContentServiceInterface, - InlineMenuFormFieldData, SubFrameDataFromWindowMessage, } from "./abstractions/autofill-overlay-content.service"; import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service"; @@ -95,6 +95,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ destroyAutofillInlineMenuListeners: () => this.destroy(), getInlineMenuFormFieldData: ({ message }) => this.handleGetInlineMenuFormFieldDataMessage(message), + generatedPasswordModifyLogin: () => this.sendGeneratedPasswordModifyLogin(), }; private readonly loginFieldQualifiers: Record = { [AutofillFieldQualifier.username]: this.inlineMenuFieldQualificationService.isUsernameField, @@ -235,6 +236,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ }); } + /** + * On password generation, send form field data i.e. modified login data + */ + sendGeneratedPasswordModifyLogin = async () => { + await this.sendExtensionMessage("generatedPasswordFilled", this.getFormFieldData()); + }; + /** * Formats any found user filled fields for a login cipher and sends a message * to the background script to add a new cipher. @@ -637,7 +645,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ /** * Returns the form field data used for add login and change password notifications. */ - private getFormFieldData = (): InlineMenuFormFieldData => { + private getFormFieldData = (): ModifyLoginCipherFormData => { return { uri: globalThis.document.URL, username: this.userFilledFields["username"]?.value || "", diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index da5f0fbfc69..9df8ce4d4db 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1258,9 +1258,6 @@ export default class MainBackground { this.overlayNotificationsBackground = new OverlayNotificationsBackground( this.logService, this.notificationBackground, - this.taskService, - this.accountService, - this.cipherService, ); this.autoSubmitLoginBackground = new AutoSubmitLoginBackground( From c3f6892f9ee74fd1200b659878ed76cfde0ce6b9 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 7 Aug 2025 19:19:14 +0200 Subject: [PATCH 06/12] [PM-24451] firefox extension crash due to huge memory usage when unlocking vault (#15938) * feat: only set badge state for the active tab * fix: tests * feat: avoid calculating states unecessarily * Revert BrowserApi.removeListener change * Add fatal log on observable failure * Use fromChromeEvent * Remove required-using tests * Only disable some * One of each --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- .../browser/src/background/main.background.ts | 1 + .../src/platform/badge/badge-browser-api.ts | 9 ++ .../src/platform/badge/badge.service.spec.ts | 62 ++++--------- .../src/platform/badge/badge.service.ts | 66 +++++++------ .../badge/test/mock-badge-browser-api.ts | 12 +++ libs/eslint/platform/required-using.spec.mjs | 92 +++++++++---------- 6 files changed, 115 insertions(+), 127 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 9df8ce4d4db..7208c1bb008 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1372,6 +1372,7 @@ export default class MainBackground { this.badgeService = new BadgeService( this.stateProvider, new DefaultBadgeBrowserApi(this.platformUtilsService), + this.logService, ); this.authStatusBadgeUpdaterService = new AuthStatusBadgeUpdaterService( this.badgeService, diff --git a/apps/browser/src/platform/badge/badge-browser-api.ts b/apps/browser/src/platform/badge/badge-browser-api.ts index 9febaf8d39c..097c6109743 100644 --- a/apps/browser/src/platform/badge/badge-browser-api.ts +++ b/apps/browser/src/platform/badge/badge-browser-api.ts @@ -1,7 +1,10 @@ +import { map, Observable } from "rxjs"; + import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { BrowserApi } from "../browser/browser-api"; +import { fromChromeEvent } from "../browser/from-chrome-event"; import { BadgeIcon, IconPaths } from "./icon"; @@ -13,6 +16,8 @@ export interface RawBadgeState { } export interface BadgeBrowserApi { + activeTab$: Observable; + setState(state: RawBadgeState, tabId?: number): Promise; getTabs(): Promise; } @@ -21,6 +26,10 @@ export class DefaultBadgeBrowserApi implements BadgeBrowserApi { private badgeAction = BrowserApi.getBrowserAction(); private sidebarAction = BrowserApi.getSidebarAction(self); + activeTab$ = fromChromeEvent(chrome.tabs.onActivated).pipe( + map(([tabActiveInfo]) => tabActiveInfo), + ); + constructor(private platformUtilsService: PlatformUtilsService) {} async setState(state: RawBadgeState, tabId?: number): Promise { diff --git a/apps/browser/src/platform/badge/badge.service.spec.ts b/apps/browser/src/platform/badge/badge.service.spec.ts index 2a7ba2ce392..52be2afa71b 100644 --- a/apps/browser/src/platform/badge/badge.service.spec.ts +++ b/apps/browser/src/platform/badge/badge.service.spec.ts @@ -1,5 +1,7 @@ +import { mock, MockProxy } from "jest-mock-extended"; import { Subscription } from "rxjs"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { FakeAccountService, FakeStateProvider } from "@bitwarden/common/spec"; import { RawBadgeState } from "./badge-browser-api"; @@ -13,6 +15,7 @@ import { MockBadgeBrowserApi } from "./test/mock-badge-browser-api"; describe("BadgeService", () => { let badgeApi: MockBadgeBrowserApi; let stateProvider: FakeStateProvider; + let logService!: MockProxy; let badgeService!: BadgeService; let badgeServiceSubscription: Subscription; @@ -20,8 +23,9 @@ describe("BadgeService", () => { beforeEach(() => { badgeApi = new MockBadgeBrowserApi(); stateProvider = new FakeStateProvider(new FakeAccountService({})); + logService = mock(); - badgeService = new BadgeService(stateProvider, badgeApi); + badgeService = new BadgeService(stateProvider, badgeApi, logService); }); afterEach(() => { @@ -34,14 +38,10 @@ describe("BadgeService", () => { describe("given a single tab is open", () => { beforeEach(() => { badgeApi.tabs = [1]; + badgeApi.setActiveTab(tabId); badgeServiceSubscription = badgeService.startListening(); }); - // This relies on the state provider to auto-emit - it("sets default values on startup", async () => { - expect(badgeApi.generalState).toEqual(DefaultBadgeState); - }); - it("sets provided state when no other state has been set", async () => { const state: BadgeState = { text: "text", @@ -52,7 +52,6 @@ describe("BadgeService", () => { await badgeService.setState("state-name", BadgeStatePriority.Default, state); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.generalState).toEqual(state); expect(badgeApi.specificStates[tabId]).toEqual(state); }); @@ -63,7 +62,6 @@ describe("BadgeService", () => { await badgeService.setState("state-name", BadgeStatePriority.Default, state); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.generalState).toEqual(DefaultBadgeState); expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); }); @@ -82,7 +80,6 @@ describe("BadgeService", () => { backgroundColor: "#fff", icon: BadgeIcon.Locked, }; - expect(badgeApi.generalState).toEqual(expectedState); expect(badgeApi.specificStates[tabId]).toEqual(expectedState); }); @@ -105,7 +102,6 @@ describe("BadgeService", () => { backgroundColor: "#aaa", icon: BadgeIcon.Locked, }; - expect(badgeApi.generalState).toEqual(expectedState); expect(badgeApi.specificStates[tabId]).toEqual(expectedState); }); @@ -126,7 +122,6 @@ describe("BadgeService", () => { backgroundColor: "#fff", icon: BadgeIcon.Locked, }; - expect(badgeApi.generalState).toEqual(expectedState); expect(badgeApi.specificStates[tabId]).toEqual(expectedState); }); @@ -147,7 +142,6 @@ describe("BadgeService", () => { await badgeService.clearState("state-3"); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.generalState).toEqual(DefaultBadgeState); expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); }); @@ -167,7 +161,6 @@ describe("BadgeService", () => { backgroundColor: "#fff", icon: DefaultBadgeState.icon, }; - expect(badgeApi.generalState).toEqual(expectedState); expect(badgeApi.specificStates[tabId]).toEqual(expectedState); }); @@ -190,26 +183,20 @@ describe("BadgeService", () => { backgroundColor: "#fff", icon: BadgeIcon.Unlocked, }; - expect(badgeApi.generalState).toEqual(expectedState); expect(badgeApi.specificStates[tabId]).toEqual(expectedState); }); }); describe("given multiple tabs are open", () => { + const tabId = 1; const tabIds = [1, 2, 3]; beforeEach(() => { badgeApi.tabs = tabIds; + badgeApi.setActiveTab(tabId); badgeServiceSubscription = badgeService.startListening(); }); - it("sets default values for each tab on startup", async () => { - expect(badgeApi.generalState).toEqual(DefaultBadgeState); - for (const tabId of tabIds) { - expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); - } - }); - it("sets state for each tab when no other state has been set", async () => { const state: BadgeState = { text: "text", @@ -220,11 +207,10 @@ describe("BadgeService", () => { await badgeService.setState("state-name", BadgeStatePriority.Default, state); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.generalState).toEqual(state); expect(badgeApi.specificStates).toEqual({ 1: state, - 2: state, - 3: state, + 2: undefined, + 3: undefined, }); }); }); @@ -236,6 +222,7 @@ describe("BadgeService", () => { beforeEach(() => { badgeApi.tabs = [tabId]; + badgeApi.setActiveTab(tabId); badgeServiceSubscription = badgeService.startListening(); }); @@ -249,7 +236,6 @@ describe("BadgeService", () => { await badgeService.setState("state-name", BadgeStatePriority.Default, state, tabId); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.generalState).toEqual(DefaultBadgeState); expect(badgeApi.specificStates[tabId]).toEqual(state); }); @@ -260,7 +246,6 @@ describe("BadgeService", () => { await badgeService.setState("state-name", BadgeStatePriority.Default, state, tabId); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.generalState).toEqual(DefaultBadgeState); expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); }); @@ -279,11 +264,6 @@ describe("BadgeService", () => { }); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.generalState).toEqual({ - ...DefaultBadgeState, - text: "text", - icon: BadgeIcon.Locked, - }); expect(badgeApi.specificStates[tabId]).toEqual({ text: "text", backgroundColor: "#fff", @@ -316,7 +296,6 @@ describe("BadgeService", () => { backgroundColor: "#fff", icon: BadgeIcon.Locked, }; - expect(badgeApi.generalState).toEqual(DefaultBadgeState); expect(badgeApi.specificStates[tabId]).toEqual(expectedState); }); @@ -354,7 +333,6 @@ describe("BadgeService", () => { backgroundColor: "#aaa", icon: BadgeIcon.Locked, }; - expect(badgeApi.generalState).toEqual(DefaultBadgeState); expect(badgeApi.specificStates[tabId]).toEqual(expectedState); }); @@ -377,11 +355,6 @@ describe("BadgeService", () => { }); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.generalState).toEqual({ - text: "override", - backgroundColor: "#aaa", - icon: DefaultBadgeState.icon, - }); expect(badgeApi.specificStates[tabId]).toEqual({ text: "override", backgroundColor: "#aaa", @@ -411,7 +384,6 @@ describe("BadgeService", () => { await badgeService.clearState("state-2"); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.generalState).toEqual(DefaultBadgeState); expect(badgeApi.specificStates[tabId]).toEqual({ text: "text", backgroundColor: "#fff", @@ -451,7 +423,6 @@ describe("BadgeService", () => { await badgeService.clearState("state-3"); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.generalState).toEqual(DefaultBadgeState); expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState); }); @@ -476,7 +447,6 @@ describe("BadgeService", () => { ); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.generalState).toEqual(DefaultBadgeState); expect(badgeApi.specificStates[tabId]).toEqual({ text: "text", backgroundColor: "#fff", @@ -513,7 +483,6 @@ describe("BadgeService", () => { ); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.generalState).toEqual(DefaultBadgeState); expect(badgeApi.specificStates[tabId]).toEqual({ text: "text", backgroundColor: "#fff", @@ -523,14 +492,16 @@ describe("BadgeService", () => { }); describe("given multiple tabs are open", () => { + const tabId = 1; const tabIds = [1, 2, 3]; beforeEach(() => { badgeApi.tabs = tabIds; + badgeApi.setActiveTab(tabId); badgeServiceSubscription = badgeService.startListening(); }); - it("sets tab-specific state for provided tab and general state for the others", async () => { + it("sets tab-specific state for provided tab", async () => { const generalState: BadgeState = { text: "general-text", backgroundColor: "general-color", @@ -550,11 +521,10 @@ describe("BadgeService", () => { ); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(badgeApi.generalState).toEqual(generalState); expect(badgeApi.specificStates).toEqual({ [tabIds[0]]: { ...specificState, backgroundColor: "general-color" }, - [tabIds[1]]: generalState, - [tabIds[2]]: generalState, + [tabIds[1]]: undefined, + [tabIds[2]]: undefined, }); }); }); diff --git a/apps/browser/src/platform/badge/badge.service.ts b/apps/browser/src/platform/badge/badge.service.ts index d48150ac516..b3831530e8d 100644 --- a/apps/browser/src/platform/badge/badge.service.ts +++ b/apps/browser/src/platform/badge/badge.service.ts @@ -1,15 +1,15 @@ import { - defer, + combineLatest, + concatMap, distinctUntilChanged, filter, map, - mergeMap, pairwise, startWith, Subscription, - switchMap, } from "rxjs"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { BADGE_MEMORY, GlobalState, @@ -39,6 +39,7 @@ export class BadgeService { constructor( private stateProvider: StateProvider, private badgeApi: BadgeBrowserApi, + private logService: LogService, ) { this.states = this.stateProvider.getGlobal(BADGE_STATES); } @@ -48,52 +49,47 @@ export class BadgeService { * Without this the service will not be able to update the badge state. */ startListening(): Subscription { - const initialSetup$ = defer(async () => { - const openTabs = await this.badgeApi.getTabs(); - await this.badgeApi.setState(DefaultBadgeState); - for (const tabId of openTabs) { - await this.badgeApi.setState(DefaultBadgeState, tabId); - } - }); - - return initialSetup$ - .pipe( - switchMap(() => this.states.state$), + return combineLatest({ + states: this.states.state$.pipe( startWith({}), distinctUntilChanged(), map((states) => new Set(states ? Object.values(states) : [])), pairwise(), map(([previous, current]) => { const [removed, added] = difference(previous, current); - return { states: current, removed, added }; + return { all: current, removed, added }; }), filter(({ removed, added }) => removed.size > 0 || added.size > 0), - mergeMap(async ({ states, removed, added }) => { - const changed = [...removed, ...added]; - const changedTabIds = new Set( - changed.map((s) => s.tabId).filter((tabId) => tabId !== undefined), - ); - const onlyTabSpecificStatesChanged = changed.every((s) => s.tabId != undefined); - if (onlyTabSpecificStatesChanged) { - // If only tab-specific states changed then we only need to update those specific tabs. - for (const tabId of changedTabIds) { - const newState = this.calculateState(states, tabId); - await this.badgeApi.setState(newState, tabId); - } + ), + activeTab: this.badgeApi.activeTab$.pipe(startWith(undefined)), + }) + .pipe( + concatMap(async ({ states, activeTab }) => { + const changed = [...states.removed, ...states.added]; + + // If the active tab wasn't changed, we don't need to update the badge. + if (!changed.some((s) => s.tabId === activeTab?.tabId || s.tabId === undefined)) { return; } - // If there are any general states that changed then we need to update all tabs. - const openTabs = await this.badgeApi.getTabs(); - const generalState = this.calculateState(states); - await this.badgeApi.setState(generalState); - for (const tabId of openTabs) { - const newState = this.calculateState(states, tabId); - await this.badgeApi.setState(newState, tabId); + try { + const state = this.calculateState(states.all, activeTab?.tabId); + await this.badgeApi.setState(state, activeTab?.tabId); + } catch (error) { + // This usually happens when the user opens a popout because of how the browser treats it + // as a tab in the same window but then won't let you set the badge state for it. + this.logService.warning("Failed to set badge state", error); } }), ) - .subscribe(); + .subscribe({ + error: (err: unknown) => { + this.logService.error( + "Fatal error in badge service observable, badge will fail to update", + err, + ); + }, + }); } /** diff --git a/apps/browser/src/platform/badge/test/mock-badge-browser-api.ts b/apps/browser/src/platform/badge/test/mock-badge-browser-api.ts index 19bde1e1fd8..4f91420b273 100644 --- a/apps/browser/src/platform/badge/test/mock-badge-browser-api.ts +++ b/apps/browser/src/platform/badge/test/mock-badge-browser-api.ts @@ -1,10 +1,22 @@ +import { BehaviorSubject } from "rxjs"; + import { BadgeBrowserApi, RawBadgeState } from "../badge-browser-api"; export class MockBadgeBrowserApi implements BadgeBrowserApi { + private _activeTab$ = new BehaviorSubject(undefined); + activeTab$ = this._activeTab$.asObservable(); + specificStates: Record = {}; generalState?: RawBadgeState; tabs: number[] = []; + setActiveTab(tabId: number) { + this._activeTab$.next({ + tabId, + windowId: 1, + }); + } + setState(state: RawBadgeState, tabId?: number): Promise { if (tabId !== undefined) { this.specificStates[tabId] = state; diff --git a/libs/eslint/platform/required-using.spec.mjs b/libs/eslint/platform/required-using.spec.mjs index d91a0e615e1..7b60060cf51 100644 --- a/libs/eslint/platform/required-using.spec.mjs +++ b/libs/eslint/platform/required-using.spec.mjs @@ -34,14 +34,14 @@ ruleTester.run("required-using", rule.default, { using client = rc.take(); `, }, - { - name: "Function reference with `using`", - code: ` - ${setup} - const t = rc.take; - using client = t(); - `, - }, + // { + // name: "Function reference with `using`", + // code: ` + // ${setup} + // const t = rc.take; + // using client = t(); + // `, + // }, ], invalid: [ { @@ -56,43 +56,43 @@ ruleTester.run("required-using", rule.default, { }, ], }, - { - name: "Assignment without `using`", - code: ` - ${setup} - let client; - client = rc.take(); - `, - errors: [ - { - message: errorMessage, - }, - ], - }, - { - name: "Function reference without `using`", - code: ` - ${setup} - const t = rc.take; - const client = t(); - `, - errors: [ - { - message: errorMessage, - }, - ], - }, - { - name: "Destructuring without `using`", - code: ` - ${setup} - const { value } = rc.take(); - `, - errors: [ - { - message: errorMessage, - }, - ], - }, + // { + // name: "Assignment without `using`", + // code: ` + // ${setup} + // let client; + // client = rc.take(); + // `, + // errors: [ + // { + // message: errorMessage, + // }, + // ], + // }, + // { + // name: "Function reference without `using`", + // code: ` + // ${setup} + // const t = rc.take; + // const client = t(); + // `, + // errors: [ + // { + // message: errorMessage, + // }, + // ], + // }, + // { + // name: "Destructuring without `using`", + // code: ` + // ${setup} + // const { value } = rc.take(); + // `, + // errors: [ + // { + // message: errorMessage, + // }, + // ], + // }, ], }); From 2ef8b1a6bfe2a6676b4f87781d532707a7024afd Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Thu, 7 Aug 2025 13:21:24 -0400 Subject: [PATCH 07/12] [PM-18066] new item details view (#15311) * update item details v2 in libs for new view design. targets web, browser, and desktop --- apps/browser/src/_locales/en/messages.json | 6 + apps/desktop/src/locales/en/messages.json | 8 +- .../emergency-view-dialog.component.spec.ts | 8 + apps/web/src/locales/en/messages.json | 8 +- .../src/vault/components/icon.component.html | 57 +++++-- .../src/vault/components/icon.component.ts | 5 + .../item-details-v2.component.html | 151 +++++++++--------- .../item-details-v2.component.spec.ts | 48 ++++-- .../item-details/item-details-v2.component.ts | 101 ++++++++++-- 9 files changed, 273 insertions(+), 119 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index ad933c24875..f8dde376b35 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5573,5 +5573,11 @@ "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", "description": "'WebAssembly' is a technical term and should not be translated." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 8b30bd85ec9..72d40ed750f 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3620,7 +3620,7 @@ }, "uriMatchDefaultStrategyHint": { "message": "URI match detection is how Bitwarden identifies autofill suggestions.", - "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", @@ -4066,6 +4066,12 @@ } } }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" + }, "enableAutotype": { "message": "Enable Autotype" }, diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts index b341fc4f8e4..f0ecca1686d 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts @@ -2,11 +2,14 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -60,6 +63,11 @@ describe("EmergencyViewDialogComponent", () => { { provide: AccountService, useValue: accountService }, { provide: TaskService, useValue: mock() }, { provide: LogService, useValue: mock() }, + { + provide: EnvironmentService, + useValue: { environment$: of({ getIconsUrl: () => "https://icons.example.com" }) }, + }, + { provide: DomainSettingsService, useValue: { showFavicons$: of(true) } }, ], }) .overrideComponent(EmergencyViewDialogComponent, { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 587dcd84e0c..4eaf141abc2 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -11002,5 +11002,11 @@ }, "providersubCanceledmessage": { "message" : "To resubscribe, contact Bitwarden Customer Support." + }, + "showMore": { + "message": "Show more" + }, + "showLess": { + "message": "Show less" } -} \ No newline at end of file +} diff --git a/libs/angular/src/vault/components/icon.component.html b/libs/angular/src/vault/components/icon.component.html index 2dae3b26cc5..0f14de64e21 100644 --- a/libs/angular/src/vault/components/icon.component.html +++ b/libs/angular/src/vault/components/icon.component.html @@ -1,19 +1,44 @@ - + +
+ @for (item of showItems(); track item.id; let last = $last) { + - + @if (isOrgIcon(item)) { + + } @else { + + } - - - -
  • - - -
  • - +
    + } + @if (allItems().length === 0) { + + + + + } + @if (hasSmallScreen() && allItems().length > 2 && cipher().collectionIds.length > 1) { + + + } +
    +
    diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts index f093cd020b5..ead2979fac7 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts @@ -1,11 +1,17 @@ +import { ComponentRef } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; +import { of } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { ClientType } from "@bitwarden/common/enums"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -14,6 +20,7 @@ import { ItemDetailsV2Component } from "./item-details-v2.component"; describe("ItemDetailsV2Component", () => { let component: ItemDetailsV2Component; let fixture: ComponentFixture; + let componentRef: ComponentRef; const cipher = { id: "cipher1", @@ -46,37 +53,46 @@ describe("ItemDetailsV2Component", () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ItemDetailsV2Component], - providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: PlatformUtilsService, useValue: { getClientType: () => ClientType.Web } }, + { + provide: EnvironmentService, + useValue: { environment$: of({ getIconsUrl: () => "https://icons.example.com" }) }, + }, + { provide: DomainSettingsService, useValue: { showFavicons$: of(true) } }, + ], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(ItemDetailsV2Component); component = fixture.componentInstance; - component.cipher = cipher; - component.organization = organization; - component.collections = [collection, collection2]; - component.folder = folder; + componentRef = fixture.componentRef; + componentRef.setInput("cipher", cipher); + componentRef.setInput("organization", organization); + componentRef.setInput("collections", [collection, collection2]); + componentRef.setInput("folder", folder); + jest.spyOn(component, "hasSmallScreen").mockReturnValue(false); // Mocking small screen check fixture.detectChanges(); }); it("displays all available fields", () => { const itemName = fixture.debugElement.query(By.css('[data-testid="item-name"]')); - const owner = fixture.debugElement.query(By.css('[data-testid="owner"]')); - const collections = fixture.debugElement.queryAll(By.css('[data-testid="collections"] li')); - const folderElement = fixture.debugElement.query(By.css('[data-testid="folder"]')); + const itemDetailsList = fixture.debugElement.queryAll( + By.css('[data-testid="item-details-list"]'), + ); - expect(itemName.nativeElement.value).toBe(cipher.name); - expect(owner.nativeElement.textContent.trim()).toBe(organization.name); - expect(collections.map((c) => c.nativeElement.textContent.trim())).toEqual([ - collection.name, - collection2.name, - ]); - expect(folderElement.nativeElement.textContent.trim()).toBe(folder.name); + expect(itemName.nativeElement.textContent.trim()).toEqual(cipher.name); + expect(itemDetailsList.length).toBe(4); // Organization, Collection, Collection2, Folder + expect(itemDetailsList[0].nativeElement.textContent.trim()).toContain(organization.name); + expect(itemDetailsList[1].nativeElement.textContent.trim()).toContain(collection.name); + expect(itemDetailsList[2].nativeElement.textContent.trim()).toContain(collection2.name); + expect(itemDetailsList[3].nativeElement.textContent.trim()).toContain(folder.name); }); it("does not render owner when `hideOwner` is true", () => { - component.hideOwner = true; + componentRef.setInput("hideOwner", true); fixture.detectChanges(); const owner = fixture.debugElement.query(By.css('[data-testid="owner"]')); diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts index 8f0fedbe599..6ccd0b7ee61 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts @@ -1,19 +1,22 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { CommonModule } from "@angular/common"; -import { Component, Input } from "@angular/core"; - +import { Component, computed, input, signal } from "@angular/core"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +import { toSignal } from "@angular/core/rxjs-interop"; +import { fromEvent, map, startWith } from "rxjs"; + // eslint-disable-next-line no-restricted-imports import { CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { + ButtonLinkDirective, CardComponent, FormFieldModule, - SectionHeaderComponent, TypographyModule, } from "@bitwarden/components"; @@ -26,20 +29,96 @@ import { OrgIconDirective } from "../../components/org-icon.directive"; CommonModule, JslibModule, CardComponent, - SectionHeaderComponent, TypographyModule, OrgIconDirective, FormFieldModule, + ButtonLinkDirective, ], }) export class ItemDetailsV2Component { - @Input() cipher: CipherView; - @Input() organization?: Organization; - @Input() collections?: CollectionView[]; - @Input() folder?: FolderView; - @Input() hideOwner?: boolean = false; + hideOwner = input(false); + cipher = input.required(); + organization = input(); + folder = input(); + collections = input(); + showAllDetails = signal(false); - get showOwnership() { - return this.cipher.organizationId && this.organization && !this.hideOwner; + showOwnership = computed(() => { + return this.cipher().organizationId && this.organization() && !this.hideOwner(); + }); + + hasSmallScreen = toSignal( + fromEvent(window, "resize").pipe( + map(() => window.innerWidth), + startWith(window.innerWidth), + map((width) => width < 681), + ), + ); + + // Array to hold all details of item. Organization, Collections, and Folder + allItems = computed(() => { + let items: any[] = []; + if (this.showOwnership() && this.organization()) { + items.push(this.organization()); + } + if (this.cipher().collectionIds?.length > 0 && this.collections()) { + items = [...items, ...this.collections()]; + } + if (this.cipher().folderId && this.folder()) { + items.push(this.folder()); + } + return items; + }); + + showItems = computed(() => { + if ( + this.hasSmallScreen() && + this.allItems().length > 2 && + !this.showAllDetails() && + this.cipher().collectionIds?.length > 1 + ) { + return this.allItems().slice(0, 2); + } else { + return this.allItems(); + } + }); + + constructor(private i18nService: I18nService) {} + + toggleShowMore() { + this.showAllDetails.update((value) => !value); + } + + getAriaLabel(item: Organization | CollectionView | FolderView): string { + if (item instanceof Organization) { + return this.i18nService.t("owner") + item.name; + } else if (item instanceof CollectionView) { + return this.i18nService.t("collection") + item.name; + } else if (item instanceof FolderView) { + return this.i18nService.t("folder") + item.name; + } + return ""; + } + + getIconClass(item: Organization | CollectionView | FolderView): string { + if (item instanceof CollectionView) { + return "bwi-collection-shared"; + } else if (item instanceof FolderView) { + return "bwi-folder"; + } + return ""; + } + + getItemTitle(item: Organization | CollectionView | FolderView): string { + if (item instanceof CollectionView) { + return this.i18nService.t("collection"); + } else if (item instanceof FolderView) { + return this.i18nService.t("folder"); + } + return ""; + } + + isOrgIcon(item: Organization | CollectionView | FolderView): boolean { + return item instanceof Organization; } } From b57238ca991b0629d7ec7d3d41bc3328394c2bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Thu, 7 Aug 2025 20:34:03 +0200 Subject: [PATCH 08/12] [PM-23663] Update yao-pkg and migrate CLI to Node 22 (#15622) --- .github/workflows/build-cli.yml | 5 +- apps/cli/.nvmrc | 1 - apps/cli/package.json | 4 - package-lock.json | 127 ++++++++++++++++++++++++++++---- package.json | 2 +- 5 files changed, 117 insertions(+), 22 deletions(-) delete mode 100644 apps/cli/.nvmrc diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index b31b22b926e..73b765f207a 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -71,6 +71,7 @@ jobs: - name: Get Node Version id: retrieve-node-version + working-directory: ./ run: | NODE_NVMRC=$(cat .nvmrc) NODE_VERSION=${NODE_NVMRC/v/''} @@ -104,7 +105,7 @@ jobs: env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} - _WIN_PKG_FETCH_VERSION: 20.11.1 + _WIN_PKG_FETCH_VERSION: 22.15.1 _WIN_PKG_VERSION: 3.5 permissions: contents: read @@ -283,7 +284,7 @@ jobs: env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} - _WIN_PKG_FETCH_VERSION: 20.11.1 + _WIN_PKG_FETCH_VERSION: 22.15.1 _WIN_PKG_VERSION: 3.5 steps: - name: Check out repo diff --git a/apps/cli/.nvmrc b/apps/cli/.nvmrc deleted file mode 100644 index 9a2a0e219c9..00000000000 --- a/apps/cli/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v20 diff --git a/apps/cli/package.json b/apps/cli/package.json index 0d3c151f012..a4ff56206e4 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -90,9 +90,5 @@ "semver": "7.7.2", "tldts": "7.0.1", "zxcvbn": "4.4.2" - }, - "engines": { - "node": "~20", - "npm": "~10" } } diff --git a/package-lock.json b/package-lock.json index 524e551914d..715ccd8bff1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -120,7 +120,7 @@ "@typescript-eslint/rule-tester": "8.31.0", "@typescript-eslint/utils": "8.31.0", "@webcomponents/custom-elements": "1.6.0", - "@yao-pkg/pkg": "5.16.1", + "@yao-pkg/pkg": "6.5.1", "angular-eslint": "19.6.0", "autoprefixer": "10.4.21", "axe-playwright": "2.1.0", @@ -229,10 +229,6 @@ }, "bin": { "bw": "build/bw.js" - }, - "engines": { - "node": "~20", - "npm": "~10" } }, "apps/cli/node_modules/define-lazy-prop": { @@ -14132,34 +14128,39 @@ "license": "Apache-2.0" }, "node_modules/@yao-pkg/pkg": { - "version": "5.16.1", - "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-5.16.1.tgz", - "integrity": "sha512-crUlnNFSReFNFuXDc4f3X2ignkFlc9kmEG7Bp/mJMA1jYyqR0lqjZGLgrSDYTYiNsYud8AzgA3RY1DrMdcUZWg==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-6.5.1.tgz", + "integrity": "sha512-z6XlySYfnqfm1AfVlBN8A3yeAQniIwL7TKQfDCGsswYSVYLt2snbRefQYsfQQ3pw5lVXrZdLqgTjzaqID9IkWA==", "dev": true, "license": "MIT", "dependencies": { "@babel/generator": "^7.23.0", "@babel/parser": "^7.23.0", "@babel/types": "^7.23.0", - "@yao-pkg/pkg-fetch": "3.5.16", + "@yao-pkg/pkg-fetch": "3.5.23", "into-stream": "^6.0.0", "minimist": "^1.2.6", "multistream": "^4.1.0", "picocolors": "^1.1.0", "picomatch": "^4.0.2", "prebuild-install": "^7.1.1", - "resolve": "^1.22.0", + "resolve": "^1.22.10", "stream-meter": "^1.0.4", - "tinyglobby": "^0.2.9" + "tar": "^7.4.3", + "tinyglobby": "^0.2.11", + "unzipper": "^0.12.3" }, "bin": { "pkg": "lib-es5/bin.js" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@yao-pkg/pkg-fetch": { - "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@yao-pkg/pkg-fetch/-/pkg-fetch-3.5.16.tgz", - "integrity": "sha512-mCnZvZz0/Ylpk4TGyt34pqWJyBGYJM8c3dPoMRV8Knodv2QhcYS4iXb5kB/JNWkrRtCKukGZIKkMLXZ3TQlzPg==", + "version": "3.5.23", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg-fetch/-/pkg-fetch-3.5.23.tgz", + "integrity": "sha512-rn45sqVQSkcJNSBdTnYze3n+kyub4CN8aiWYlPgA9yp9FZeEF+BlpL68kSIm3HaVuANniF+7RBMH5DkC4zlHZA==", "dev": true, "license": "MIT", "dependencies": { @@ -14261,6 +14262,73 @@ "node": ">=10" } }, + "node_modules/@yao-pkg/pkg/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -15769,6 +15837,13 @@ "node": ">= 6" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -18881,6 +18956,16 @@ "dev": true, "license": "MIT" }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -37611,6 +37696,20 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.9.1" } }, + "node_modules/unzipper": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", + "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "^11.2.0", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", diff --git a/package.json b/package.json index 6f79985c9df..0d8ba9989b4 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@typescript-eslint/rule-tester": "8.31.0", "@typescript-eslint/utils": "8.31.0", "@webcomponents/custom-elements": "1.6.0", - "@yao-pkg/pkg": "5.16.1", + "@yao-pkg/pkg": "6.5.1", "angular-eslint": "19.6.0", "autoprefixer": "10.4.21", "axe-playwright": "2.1.0", From fe2f68d5d8c7b209cdbca1a7492a69ae262ee0f7 Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Thu, 7 Aug 2025 15:03:47 -0400 Subject: [PATCH 09/12] Update README.md --- libs/common/src/platform/actions/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/platform/actions/README.md b/libs/common/src/platform/actions/README.md index 179fc203360..19165d88b56 100644 --- a/libs/common/src/platform/actions/README.md +++ b/libs/common/src/platform/actions/README.md @@ -28,7 +28,7 @@ The `openPopup()` method has limitations in some environments due to browser-spe - **Safari**: Only works when `openPopup()` is triggered from a window context. Attempts from background service workers fail. - **Firefox**: Does not appear to support `openPopup()` in either context. -- **Chrome**: Fully functional in both contexts. +- **Chrome**: Fully functional in both contexts, but only on Mac. Windows it does not work in. - **Edge**: Behavior has not been tested. - **Vivaldi**: `openPopup()` results in an error that _might_ be related to running in a background context, but the cause is currently unclear. - **Opera**: Works from window context. Background calls fail silently with no error message. From 49bef8ee3c5e53ea6b5b22d15e554648242a17ea Mon Sep 17 00:00:00 2001 From: Patrick Pimentel Date: Fri, 8 Aug 2025 13:10:28 -0400 Subject: [PATCH 10/12] fix(notification-processing): [PM-19877] System Notification Implementation - Addressed more feedback. --- .../overlay-notifications.background.spec.ts | 2 +- .../overlay-notifications.background.ts | 6 +-- .../src/autofill/content/autofill-init.ts | 2 +- .../autofill-overlay-content.service.ts | 2 +- .../browser/src/background/idle.background.ts | 2 +- .../browser/src/background/main.background.ts | 8 +-- ...foreground-server-notifications.service.ts | 4 +- .../browser-system-notification.service.ts | 54 +++++-------------- .../src/popup/services/services.module.ts | 4 +- .../at-risk-passwords.component.ts | 2 +- apps/desktop/src/app/app.component.ts | 2 +- apps/desktop/src/app/services/init.service.ts | 2 +- apps/web/src/app/app.component.ts | 2 +- apps/web/src/app/core/core.module.ts | 2 +- apps/web/src/app/core/init.service.ts | 2 +- .../permissions-webpush-connection.service.ts | 2 +- .../vault/individual-vault/vault.component.ts | 2 +- .../src/services/jslib-services.module.ts | 24 ++++----- .../auth-request.service.abstraction.ts | 4 +- .../auth-request/auth-request-api.service.ts | 2 +- .../services/domain-settings.service.ts | 4 +- libs/common/src/enums/push-technology.enum.ts | 4 +- .../index.ts | 0 .../default-notifications.service.spec.ts | 8 +-- .../default-server-notifications.service.ts | 13 ++--- .../internal/index.ts | 2 +- .../noop-server-notifications.service.ts} | 4 +- .../internal/signalr-connection.service.ts | 0 .../unsupported-webpush-connection.service.ts | 0 .../web-push-notifications-api.service.ts | 2 +- .../internal/web-push.request.ts | 0 .../internal/webpush-connection.service.ts | 0 .../websocket-webpush-connection.service.ts | 0 .../worker-webpush-connection.service.spec.ts | 0 .../worker-webpush-connection.service.ts | 4 +- .../server-notifications.service.ts | 4 +- .../src/platform/sync/core-sync.service.ts | 2 +- .../platform/system-notifications/index.ts | 1 + .../system-notifications.service.ts | 6 +-- ...nsupported-system-notifications.service.ts | 2 +- .../end-user-notification.service.ts | 10 ++-- ...ault-end-user-notification.service.spec.ts | 14 ++--- .../default-end-user-notification.service.ts | 18 +++---- .../vault/tasks/abstractions/task.service.ts | 2 +- .../services/default-task.service.spec.ts | 12 ++--- .../tasks/services/default-task.service.ts | 4 +- libs/components/src/toast/toast.service.ts | 2 +- libs/components/src/toast/toastr.component.ts | 2 +- .../src/lock/components/lock.component.ts | 2 +- 49 files changed, 112 insertions(+), 140 deletions(-) rename libs/common/src/platform/{notifications => server-notifications}/index.ts (100%) rename libs/common/src/platform/{notifications => server-notifications}/internal/default-notifications.service.spec.ts (97%) rename libs/common/src/platform/{notifications => server-notifications}/internal/default-server-notifications.service.ts (94%) rename libs/common/src/platform/{notifications => server-notifications}/internal/index.ts (86%) rename libs/common/src/platform/{notifications/internal/unsupported-server-notifications.service.ts => server-notifications/internal/noop-server-notifications.service.ts} (82%) rename libs/common/src/platform/{notifications => server-notifications}/internal/signalr-connection.service.ts (100%) rename libs/common/src/platform/{notifications => server-notifications}/internal/unsupported-webpush-connection.service.ts (100%) rename libs/common/src/platform/{notifications => server-notifications}/internal/web-push-notifications-api.service.ts (94%) rename libs/common/src/platform/{notifications => server-notifications}/internal/web-push.request.ts (100%) rename libs/common/src/platform/{notifications => server-notifications}/internal/webpush-connection.service.ts (100%) rename libs/common/src/platform/{notifications => server-notifications}/internal/websocket-webpush-connection.service.ts (100%) rename libs/common/src/platform/{notifications => server-notifications}/internal/worker-webpush-connection.service.spec.ts (100%) rename libs/common/src/platform/{notifications => server-notifications}/internal/worker-webpush-connection.service.ts (98%) rename libs/common/src/platform/{notifications => server-notifications}/server-notifications.service.ts (84%) create mode 100644 libs/common/src/platform/system-notifications/index.ts rename libs/common/src/platform/{notifications => system-notifications}/system-notifications.service.ts (90%) rename libs/common/src/platform/{notifications => system-notifications}/unsupported-system-notifications.service.ts (97%) diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts index cf317de4fd2..c59ca28719a 100644 --- a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts @@ -451,7 +451,7 @@ describe("OverlayNotificationsBackground", () => { }); }); - describe("web requests that trigger notifications", () => { + describe("web requests that trigger server notifications", () => { const requestId = "123345"; const pageDetails = mock({ fields: [mock()] }); diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts index e7126a57e9f..7a7eb0028f3 100644 --- a/apps/browser/src/autofill/background/overlay-notifications.background.ts +++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts @@ -41,7 +41,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg ) {} /** - * Initialize the overlay notifications background service. + * Initialize the overlay server notifications background service. */ async init() { this.setupExtensionListeners(); @@ -395,7 +395,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg }; /** - * This method attempts to trigger the add login, change password, or at-risk password notifications + * This method attempts to trigger the add login, change password, or at-risk password server notifications * based on the modified login data and the tab details. * * @param requestId - The details of the web response @@ -462,7 +462,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg case NotificationTypes.AtRiskPassword: return !modifyLoginData.newPassword; case NotificationTypes.Unlock: - // Unlock notifications are handled separately and do not require form data + // Unlock server notifications are handled separately and do not require form data return false; default: this.logService.error(`Unknown notification type: ${notificationType}`); diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index b6fc6c3392e..5a71e3bd8da 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -35,7 +35,7 @@ class AutofillInit implements AutofillInitInterface { * @param domElementVisibilityService - Used to check if an element is viewable. * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. * @param autofillInlineMenuContentService - The inline menu content service, potentially undefined. - * @param overlayNotificationsContentService - The overlay notifications content service, potentially undefined. + * @param overlayNotificationsContentService - The overlay server notifications content service, potentially undefined. */ constructor( domQueryService: DomQueryService, 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 4db00901759..fc8ce376f12 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -643,7 +643,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ }; /** - * Returns the form field data used for add login and change password notifications. + * Returns the form field data used for add login and change password server notifications. */ private getFormFieldData = (): ModifyLoginCipherFormData => { return { diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index aaa23a140db..81a869917a6 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -7,7 +7,7 @@ import { VaultTimeoutSettingsService, VaultTimeoutStringType, } from "@bitwarden/common/key-management/vault-timeout"; -import { ServerNotificationsService } from "@bitwarden/common/platform/notifications"; +import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; const IdleInterval = 60 * 5; // 5 minutes diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index b2bb40ef700..530b67c34a3 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -119,7 +119,7 @@ import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { ServerNotificationsService } from "@bitwarden/common/platform/notifications"; +import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; // eslint-disable-next-line no-restricted-imports -- Needed for service creation import { DefaultServerNotificationsService, @@ -127,9 +127,7 @@ import { UnsupportedWebPushConnectionService, WebPushNotificationsApiService, WorkerWebPushConnectionService, -} from "@bitwarden/common/platform/notifications/internal"; -import { SystemNotificationsService } from "@bitwarden/common/platform/notifications/system-notifications.service"; -import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/notifications/unsupported-system-notifications.service"; +} from "@bitwarden/common/platform/server-notifications/internal"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; @@ -169,6 +167,8 @@ import { WindowStorageService } from "@bitwarden/common/platform/storage/window- import { SyncService } from "@bitwarden/common/platform/sync"; // eslint-disable-next-line no-restricted-imports -- Needed for service creation import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal"; +import { SystemNotificationsService } from "@bitwarden/common/platform/system-notifications/"; +import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/system-notifications/unsupported-system-notifications.service"; import { DefaultThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ApiService } from "@bitwarden/common/services/api.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; diff --git a/apps/browser/src/platform/notifications/foreground-server-notifications.service.ts b/apps/browser/src/platform/notifications/foreground-server-notifications.service.ts index 02fd092339f..6ab164d9c63 100644 --- a/apps/browser/src/platform/notifications/foreground-server-notifications.service.ts +++ b/apps/browser/src/platform/notifications/foreground-server-notifications.service.ts @@ -2,10 +2,10 @@ import { Observable, Subscription } from "rxjs"; import { NotificationResponse } from "@bitwarden/common/models/response/notification.response"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { ServerNotificationsService } from "@bitwarden/common/platform/notifications"; +import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; import { UserId } from "@bitwarden/common/types/guid"; -// Eventually if we want to support listening to notifications from browser foreground we +// Eventually if we want to support listening to server notifications from browser foreground we // will only ever create a single SignalR connection, likely messaging to the background to reuse its connection. export class ForegroundServerNotificationsService implements ServerNotificationsService { notifications$: Observable; diff --git a/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts b/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts index e41609a50be..26b49515b82 100644 --- a/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts +++ b/apps/browser/src/platform/system-notifications/browser-system-notification.service.ts @@ -1,5 +1,4 @@ import { map, merge, Observable } from "rxjs"; -import { v4 as uuidv4 } from "uuid"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -9,7 +8,7 @@ import { SystemNotificationCreateInfo, SystemNotificationEvent, SystemNotificationsService, -} from "@bitwarden/common/platform/notifications/system-notifications.service"; +} from "@bitwarden/common/platform/system-notifications/system-notifications.service"; import { fromChromeEvent } from "../browser/from-chrome-event"; @@ -36,48 +35,19 @@ export class BrowserSystemNotificationService implements SystemNotificationsServ ); } - async create(createInfo: SystemNotificationCreateInfo): Promise { - try { - const notificationId = createInfo.id || uuidv4(); - - chrome.notifications.create(notificationId, { - iconUrl: "https://avatars.githubusercontent.com/u/15990069?s=200", - message: createInfo.body, - type: "basic", - title: createInfo.title, - buttons: createInfo.buttons.map((value) => { - return { title: value.title }; - }), - }); - - // eslint-disable-next-line no-restricted-syntax - chrome.notifications.onButtonClicked.addListener( - (notificationId: string, buttonIndex: number) => { - this.notificationClicked$.subscribe({ - next: () => ({ - id: notificationId, - buttonIdentifier: buttonIndex, - }), - }); + async create(createInfo: SystemNotificationCreateInfo): Promise { + return new Promise((resolve) => { + chrome.notifications.create( + { + iconUrl: chrome.runtime.getURL("images/icon128.png"), + message: createInfo.body, + type: "basic", + title: createInfo.title, + buttons: createInfo.buttons.map((value) => ({ title: value.title })), }, + (notificationId) => resolve(notificationId), ); - - // eslint-disable-next-line no-restricted-syntax - chrome.notifications.onClicked.addListener((notificationId: string) => { - this.notificationClicked$.subscribe({ - next: () => ({ - id: notificationId, - buttonIdentifier: ButtonLocation.NotificationButton, - }), - }); - }); - - return notificationId; - } catch (e) { - this.logService.error( - `Failed to create notification on ${this.platformUtilsService.getDevice()} with error: ${e}`, - ); - } + }); } async clear(clearInfo: SystemNotificationClearInfo): Promise { diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 7e9017e930a..100e472e9a4 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -96,9 +96,8 @@ import { Message, MessageListener, MessageSender } from "@bitwarden/common/platf // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { flagEnabled } from "@bitwarden/common/platform/misc/flags"; -import { ServerNotificationsService } from "@bitwarden/common/platform/notifications"; -import { SystemNotificationsService } from "@bitwarden/common/platform/notifications/system-notifications.service"; import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; +import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; @@ -114,6 +113,7 @@ import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/imp import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service"; import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { SystemNotificationsService } from "@bitwarden/common/platform/system-notifications/system-notifications.service"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts index 1bfb65a15cc..d144169a715 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts @@ -218,7 +218,7 @@ export class AtRiskPasswordsComponent implements OnInit { }); }), concatMap((unreadTaskNotifications) => { - // TODO: Investigate creating a bulk endpoint to mark notifications as read + // TODO: Investigate creating a bulk endpoint to mark server notifications as read return concat( ...unreadTaskNotifications.map((n) => this.endUserNotificationService.markAsRead(n.id, userId), diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 6afda4e9ddf..8c74624414b 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -61,7 +61,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SystemService } from "@bitwarden/common/platform/abstractions/system.service"; -import { ServerNotificationsService } from "@bitwarden/common/platform/notifications"; +import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; import { StateEventRunnerService } from "@bitwarden/common/platform/state"; import { SyncService } from "@bitwarden/common/platform/sync"; import { UserId } from "@bitwarden/common/types/guid"; diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index dad7739bc5a..0738aaba295 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -13,7 +13,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; -import { ServerNotificationsService } from "@bitwarden/common/platform/notifications"; +import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/platform/sync"; diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 0729d42f053..6240fd4eec4 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -21,7 +21,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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { ServerNotificationsService } from "@bitwarden/common/platform/notifications"; +import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; import { StateEventRunnerService } from "@bitwarden/common/platform/state"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index a222b668043..965a9d5c99d 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -84,7 +84,7 @@ import { IpcService } from "@bitwarden/common/platform/ipc"; import { UnsupportedWebPushConnectionService, WebPushConnectionService, -} from "@bitwarden/common/platform/notifications/internal"; +} from "@bitwarden/common/platform/server-notifications/internal"; import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index ecf10bfa723..f75d1268053 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -13,7 +13,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { IpcService } from "@bitwarden/common/platform/ipc"; -import { ServerNotificationsService } from "@bitwarden/common/platform/notifications"; +import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; diff --git a/apps/web/src/app/platform/notifications/permissions-webpush-connection.service.ts b/apps/web/src/app/platform/notifications/permissions-webpush-connection.service.ts index 44866285251..c0bc9ed3e77 100644 --- a/apps/web/src/app/platform/notifications/permissions-webpush-connection.service.ts +++ b/apps/web/src/app/platform/notifications/permissions-webpush-connection.service.ts @@ -5,7 +5,7 @@ import { SupportStatus } from "@bitwarden/common/platform/misc/support-status"; import { WebPushConnector, WorkerWebPushConnectionService, -} from "@bitwarden/common/platform/notifications/internal"; +} from "@bitwarden/common/platform/server-notifications/internal"; import { UserId } from "@bitwarden/common/types/guid"; export class PermissionsWebPushConnectionService extends WorkerWebPushConnectionService { diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 658f829128d..ca8b05c2701 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -565,7 +565,7 @@ export class VaultComponent implements OnInit, OnDestr this.refreshing = false; // Explicitly mark for check to ensure the view is updated - // Some sources are not always emitted within the Angular zone (e.g. ciphers updated via WS notifications) + // Some sources are not always emitted within the Angular zone (e.g. ciphers updated via WS server notifications) this.changeDetectorRef.markForCheck(); }, ); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 0a64c3a90cf..f88088f4812 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -202,20 +202,20 @@ import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/inter import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; -import { ServerNotificationsService } from "@bitwarden/common/platform/notifications"; -// eslint-disable-next-line no-restricted-imports -- Needed for service creation -import { - DefaultServerNotificationsService, - UnsupportedServerNotificationsService, - SignalRConnectionService, - UnsupportedWebPushConnectionService, - WebPushConnectionService, - WebPushNotificationsApiService, -} from "@bitwarden/common/platform/notifications/internal"; import { DefaultTaskSchedulerService, TaskSchedulerService, } from "@bitwarden/common/platform/scheduling"; +import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; +// eslint-disable-next-line no-restricted-imports -- Needed for service creation +import { + DefaultServerNotificationsService, + NoopServerNotificationsService, + SignalRConnectionService, + UnsupportedWebPushConnectionService, + WebPushConnectionService, + WebPushNotificationsApiService, +} from "@bitwarden/common/platform/server-notifications/internal"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; @@ -952,7 +952,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: ServerNotificationsService, useClass: devFlagEnabled("noopNotifications") - ? UnsupportedServerNotificationsService + ? NoopServerNotificationsService : DefaultServerNotificationsService, deps: [ LogService, @@ -1115,7 +1115,7 @@ const safeProviders: SafeProvider[] = [ // This is a slightly odd dependency tree for a specialized api service // it depends on SyncService so that new data can be retrieved through the sync // rather than updating the OrganizationService directly. Instead OrganizationService - // subscribes to sync notifications and will update itself based on that. + // subscribes to sync server notifications and will update itself based on that. deps: [ApiServiceAbstraction, SyncService], }), safeProvider({ diff --git a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts index 9eea3fe7bb0..10cc643fd45 100644 --- a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts @@ -109,9 +109,9 @@ export abstract class AuthRequestServiceAbstraction { ): Promise<{ masterKey: MasterKey; masterKeyHash: string }>; /** - * Handles incoming auth request push notifications. + * Handles incoming auth request push server notifications. * @param notification push notification. - * @remark We should only be receiving approved push notifications to prevent enumeration. + * @remark We should only be receiving approved push server notifications to prevent enumeration. */ abstract sendAuthRequestPushNotification(notification: AuthRequestPushNotification): void; diff --git a/libs/auth/src/common/services/auth-request/auth-request-api.service.ts b/libs/auth/src/common/services/auth-request/auth-request-api.service.ts index 15517a9a0e5..52c8cff1b7f 100644 --- a/libs/auth/src/common/services/auth-request/auth-request-api.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request-api.service.ts @@ -63,7 +63,7 @@ export class DefaultAuthRequestApiService implements AuthRequestApiServiceAbstra try { // Submit the current device identifier in the header as well as in the POST body. // The value in the header will be used to build the request context and ensure that the resulting - // notifications have the current device as a source. + // server notifications have the current device as a source. const response = await this.apiService.send( "POST", "/auth-requests/", diff --git a/libs/common/src/autofill/services/domain-settings.service.ts b/libs/common/src/autofill/services/domain-settings.service.ts index b2833b9ee25..d08fe07c50d 100644 --- a/libs/common/src/autofill/services/domain-settings.service.ts +++ b/libs/common/src/autofill/services/domain-settings.service.ts @@ -25,7 +25,7 @@ const SHOW_FAVICONS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "showFavicons", { deserializer: (value: boolean) => value ?? true, }); -// Domain exclusion list for notifications +// Domain exclusion list for server notifications const NEVER_DOMAINS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "neverDomains", { deserializer: (value: NeverDomains) => value ?? null, }); @@ -64,7 +64,7 @@ export abstract class DomainSettingsService { setShowFavicons: (newValue: boolean) => Promise; /** - * User-specified URIs for which the client notifications should not appear + * User-specified URIs for which the client server notifications should not appear */ neverDomains$: Observable; setNeverDomains: (newValue: NeverDomains) => Promise; diff --git a/libs/common/src/enums/push-technology.enum.ts b/libs/common/src/enums/push-technology.enum.ts index 1bc4e62cc9d..83de1f5849a 100644 --- a/libs/common/src/enums/push-technology.enum.ts +++ b/libs/common/src/enums/push-technology.enum.ts @@ -5,11 +5,11 @@ // eslint-disable-next-line @bitwarden/platform/no-enums export enum PushTechnology { /** - * Indicates that we should use SignalR over web sockets to receive push notifications from the server. + * Indicates that we should use SignalR over web sockets to receive push server notifications from the server. */ SignalR = 0, /** - * Indicatates that we should use WebPush to receive push notifications from the server. + * Indicates that we should use WebPush to receive push server notifications from the server. */ WebPush = 1, } diff --git a/libs/common/src/platform/notifications/index.ts b/libs/common/src/platform/server-notifications/index.ts similarity index 100% rename from libs/common/src/platform/notifications/index.ts rename to libs/common/src/platform/server-notifications/index.ts diff --git a/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts b/libs/common/src/platform/server-notifications/internal/default-notifications.service.spec.ts similarity index 97% rename from libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts rename to libs/common/src/platform/server-notifications/internal/default-notifications.service.spec.ts index 3842622d328..567e0fbfc3d 100644 --- a/libs/common/src/platform/notifications/internal/default-notifications.service.spec.ts +++ b/libs/common/src/platform/server-notifications/internal/default-notifications.service.spec.ts @@ -134,7 +134,7 @@ describe("NotificationsService", () => { expect(actualNotification.type).toBe(expectedType); }; - it("emits notifications through WebPush when supported", async () => { + it("emits server notifications through WebPush when supported", async () => { const notificationsPromise = firstValueFrom(sut.notifications$.pipe(bufferCount(2))); emitActiveUser(mockUser1); @@ -227,7 +227,7 @@ describe("NotificationsService", () => { }); it.each([ - // Temporarily rolling back notifications being connected while locked + // Temporarily rolling back server notifications being connected while locked // { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked }, // { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked }, // { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked }, @@ -256,7 +256,7 @@ describe("NotificationsService", () => { ); it.each([ - // Temporarily disabling notifications connecting while in a locked state + // Temporarily disabling server notifications connecting while in a locked state // AuthenticationStatus.Locked, AuthenticationStatus.Unlocked, ])( @@ -282,7 +282,7 @@ describe("NotificationsService", () => { }, ); - it("does not connect to any notification stream when notifications are disabled through special url", () => { + it("does not connect to any notification stream when server notifications are disabled through special url", () => { const subscription = sut.notifications$.subscribe(); emitActiveUser(mockUser1); emitNotificationUrl(DISABLED_NOTIFICATIONS_URL); diff --git a/libs/common/src/platform/notifications/internal/default-server-notifications.service.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts similarity index 94% rename from libs/common/src/platform/notifications/internal/default-server-notifications.service.ts rename to libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts index 09657d3908c..4502d9663a3 100644 --- a/libs/common/src/platform/notifications/internal/default-server-notifications.service.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts @@ -61,7 +61,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer distinctUntilChanged(), switchMap((activeAccountId) => { if (activeAccountId == null) { - // We don't emit notifications for inactive accounts currently + // We don't emit server-notifications for inactive accounts currently return EMPTY; } @@ -74,8 +74,8 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer } /** - * Retrieves a stream of push notifications for the given user. - * @param userId The user id of the user to get the push notifications for. + * Retrieves a stream of push server notifications for the given user. + * @param userId The user id of the user to get the push server notifications for. */ private userNotifications$(userId: UserId) { return this.environmentService.environment$.pipe( @@ -109,7 +109,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer }), supportSwitch({ supported: (service) => { - this.logService.info("Using WebPush for notifications"); + this.logService.info("Using WebPush for server notifications"); return service.notifications$.pipe( catchError((err: unknown) => { this.logService.warning("Issue with web push, falling back to SignalR", err); @@ -118,7 +118,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer ); }, notSupported: () => { - this.logService.info("Using SignalR for notifications"); + this.logService.info("Using SignalR for server notifications"); return this.connectSignalR$(userId, notificationsUrl); }, }), @@ -236,7 +236,8 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer mergeMap(async ([notification, userId]) => this.processNotification(notification, userId)), ) .subscribe({ - error: (e: unknown) => this.logService.warning("Error in notifications$ observable", e), + error: (e: unknown) => + this.logService.warning("Error in server notifications$ observable", e), }); } diff --git a/libs/common/src/platform/notifications/internal/index.ts b/libs/common/src/platform/server-notifications/internal/index.ts similarity index 86% rename from libs/common/src/platform/notifications/internal/index.ts rename to libs/common/src/platform/server-notifications/internal/index.ts index c6ffd8162b8..daa89a79468 100644 --- a/libs/common/src/platform/notifications/internal/index.ts +++ b/libs/common/src/platform/server-notifications/internal/index.ts @@ -1,7 +1,7 @@ export * from "./worker-webpush-connection.service"; export * from "./signalr-connection.service"; export * from "./default-server-notifications.service"; -export * from "./unsupported-server-notifications.service"; +export * from "./noop-server-notifications.service"; export * from "./unsupported-webpush-connection.service"; export * from "./webpush-connection.service"; export * from "./websocket-webpush-connection.service"; diff --git a/libs/common/src/platform/notifications/internal/unsupported-server-notifications.service.ts b/libs/common/src/platform/server-notifications/internal/noop-server-notifications.service.ts similarity index 82% rename from libs/common/src/platform/notifications/internal/unsupported-server-notifications.service.ts rename to libs/common/src/platform/server-notifications/internal/noop-server-notifications.service.ts index 6422e17247c..6a6bb9e19ed 100644 --- a/libs/common/src/platform/notifications/internal/unsupported-server-notifications.service.ts +++ b/libs/common/src/platform/server-notifications/internal/noop-server-notifications.service.ts @@ -6,14 +6,14 @@ import { UserId } from "@bitwarden/common/types/guid"; import { LogService } from "../../abstractions/log.service"; import { ServerNotificationsService } from "../server-notifications.service"; -export class UnsupportedServerNotificationsService implements ServerNotificationsService { +export class NoopServerNotificationsService implements ServerNotificationsService { notifications$: Observable = new Subject(); constructor(private logService: LogService) {} startListening(): Subscription { this.logService.info( - "Initializing no-op notification service, no push notifications will be received", + "Initializing no-op notification service, no push server notifications will be received", ); return Subscription.EMPTY; } diff --git a/libs/common/src/platform/notifications/internal/signalr-connection.service.ts b/libs/common/src/platform/server-notifications/internal/signalr-connection.service.ts similarity index 100% rename from libs/common/src/platform/notifications/internal/signalr-connection.service.ts rename to libs/common/src/platform/server-notifications/internal/signalr-connection.service.ts diff --git a/libs/common/src/platform/notifications/internal/unsupported-webpush-connection.service.ts b/libs/common/src/platform/server-notifications/internal/unsupported-webpush-connection.service.ts similarity index 100% rename from libs/common/src/platform/notifications/internal/unsupported-webpush-connection.service.ts rename to libs/common/src/platform/server-notifications/internal/unsupported-webpush-connection.service.ts diff --git a/libs/common/src/platform/notifications/internal/web-push-notifications-api.service.ts b/libs/common/src/platform/server-notifications/internal/web-push-notifications-api.service.ts similarity index 94% rename from libs/common/src/platform/notifications/internal/web-push-notifications-api.service.ts rename to libs/common/src/platform/server-notifications/internal/web-push-notifications-api.service.ts index b824b8c7d65..891dab2c069 100644 --- a/libs/common/src/platform/notifications/internal/web-push-notifications-api.service.ts +++ b/libs/common/src/platform/server-notifications/internal/web-push-notifications-api.service.ts @@ -10,7 +10,7 @@ export class WebPushNotificationsApiService { ) {} /** - * Posts a device-user association to the server and ensures it's installed for push notifications + * Posts a device-user association to the server and ensures it's installed for push server notifications */ async putSubscription(pushSubscription: PushSubscriptionJSON): Promise { const request = WebPushRequest.from(pushSubscription); diff --git a/libs/common/src/platform/notifications/internal/web-push.request.ts b/libs/common/src/platform/server-notifications/internal/web-push.request.ts similarity index 100% rename from libs/common/src/platform/notifications/internal/web-push.request.ts rename to libs/common/src/platform/server-notifications/internal/web-push.request.ts diff --git a/libs/common/src/platform/notifications/internal/webpush-connection.service.ts b/libs/common/src/platform/server-notifications/internal/webpush-connection.service.ts similarity index 100% rename from libs/common/src/platform/notifications/internal/webpush-connection.service.ts rename to libs/common/src/platform/server-notifications/internal/webpush-connection.service.ts diff --git a/libs/common/src/platform/notifications/internal/websocket-webpush-connection.service.ts b/libs/common/src/platform/server-notifications/internal/websocket-webpush-connection.service.ts similarity index 100% rename from libs/common/src/platform/notifications/internal/websocket-webpush-connection.service.ts rename to libs/common/src/platform/server-notifications/internal/websocket-webpush-connection.service.ts diff --git a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.spec.ts b/libs/common/src/platform/server-notifications/internal/worker-webpush-connection.service.spec.ts similarity index 100% rename from libs/common/src/platform/notifications/internal/worker-webpush-connection.service.spec.ts rename to libs/common/src/platform/server-notifications/internal/worker-webpush-connection.service.spec.ts diff --git a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts b/libs/common/src/platform/server-notifications/internal/worker-webpush-connection.service.ts similarity index 98% rename from libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts rename to libs/common/src/platform/server-notifications/internal/worker-webpush-connection.service.ts index 528ad90ed61..d8a2c33568e 100644 --- a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts +++ b/libs/common/src/platform/server-notifications/internal/worker-webpush-connection.service.ts @@ -40,7 +40,7 @@ interface PushEvent { } /** - * An implementation for connecting to web push based notifications running in a Worker. + * An implementation for connecting to web push based server notifications running in a Worker. */ export class WorkerWebPushConnectionService implements WebPushConnectionService { private pushEvent = new Subject(); @@ -75,7 +75,7 @@ export class WorkerWebPushConnectionService implements WebPushConnectionService } supportStatus$(userId: UserId): Observable> { - // Check the server config to see if it supports sending WebPush notifications + // Check the server config to see if it supports sending WebPush server notifications // FIXME: get config of server for the specified userId, once ConfigService supports it return this.configService.serverConfig$.pipe( map((config) => diff --git a/libs/common/src/platform/notifications/server-notifications.service.ts b/libs/common/src/platform/server-notifications/server-notifications.service.ts similarity index 84% rename from libs/common/src/platform/notifications/server-notifications.service.ts rename to libs/common/src/platform/server-notifications/server-notifications.service.ts index 02d261aca35..97431290d7a 100644 --- a/libs/common/src/platform/notifications/server-notifications.service.ts +++ b/libs/common/src/platform/server-notifications/server-notifications.service.ts @@ -13,11 +13,11 @@ export abstract class ServerNotificationsService { /** * @deprecated This method should not be consumed, an observable to listen to server * notifications will be available one day but it is not ready to be consumed generally. - * Please add code reacting to notifications in {@link DefaultServerNotificationsService.processNotification} + * Please add code reacting to server notifications in {@link DefaultServerNotificationsService.processNotification} */ abstract notifications$: Observable; /** - * Starts automatic listening and processing of notifications, should only be called once per application, + * Starts automatic listening and processing of server notifications, should only be called once per application, * or you will risk notifications being processed multiple times. */ abstract startListening(): Subscription; diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts index 40419a343da..ebd28e98d95 100644 --- a/libs/common/src/platform/sync/core-sync.service.ts +++ b/libs/common/src/platform/sync/core-sync.service.ts @@ -230,7 +230,7 @@ export abstract class CoreSyncService implements SyncService { }), ), ); - // Process only notifications for currently active user when user is not logged out + // Process only server notifications for currently active user when user is not logged out // TODO: once send service allows data manipulation of non-active users, this should process any received notification if (activeUserId === notification.userId && status !== AuthenticationStatus.LoggedOut) { try { diff --git a/libs/common/src/platform/system-notifications/index.ts b/libs/common/src/platform/system-notifications/index.ts new file mode 100644 index 00000000000..1209f1b42eb --- /dev/null +++ b/libs/common/src/platform/system-notifications/index.ts @@ -0,0 +1 @@ +export { SystemNotificationsService } from "./system-notifications.service"; diff --git a/libs/common/src/platform/notifications/system-notifications.service.ts b/libs/common/src/platform/system-notifications/system-notifications.service.ts similarity index 90% rename from libs/common/src/platform/notifications/system-notifications.service.ts rename to libs/common/src/platform/system-notifications/system-notifications.service.ts index 2ebf086ed0a..54369231967 100644 --- a/libs/common/src/platform/notifications/system-notifications.service.ts +++ b/libs/common/src/platform/system-notifications/system-notifications.service.ts @@ -32,7 +32,7 @@ export type SystemNotificationEvent = { }; /** - * A service responsible for displaying operating system level notifications. + * A service responsible for displaying operating system level server notifications. */ export abstract class SystemNotificationsService { abstract notificationClicked$: Observable; @@ -43,7 +43,7 @@ export abstract class SystemNotificationsService { * @returns If a notification is successfully created it will respond back with an * id that refers to a notification. */ - abstract create(createInfo: SystemNotificationCreateInfo): Promise; + abstract create(createInfo: SystemNotificationCreateInfo): Promise; /** * Clears a notification. @@ -52,7 +52,7 @@ export abstract class SystemNotificationsService { abstract clear(clearInfo: SystemNotificationClearInfo): Promise; /** - * Used to know if a given platform supports notifications. + * Used to know if a given platform supports server notifications. */ abstract isSupported(): boolean; } diff --git a/libs/common/src/platform/notifications/unsupported-system-notifications.service.ts b/libs/common/src/platform/system-notifications/unsupported-system-notifications.service.ts similarity index 97% rename from libs/common/src/platform/notifications/unsupported-system-notifications.service.ts rename to libs/common/src/platform/system-notifications/unsupported-system-notifications.service.ts index bfc9371a92f..b3627b2b07b 100644 --- a/libs/common/src/platform/notifications/unsupported-system-notifications.service.ts +++ b/libs/common/src/platform/system-notifications/unsupported-system-notifications.service.ts @@ -9,7 +9,7 @@ import { export class UnsupportedSystemNotificationsService implements SystemNotificationsService { notificationClicked$ = throwError(() => new Error("Notification clicked is not supported.")); - async create(createInfo: SystemNotificationCreateInfo): Promise { + async create(createInfo: SystemNotificationCreateInfo): Promise { throw new Error("Create OS Notification unsupported."); } diff --git a/libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts b/libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts index bc5dd4d97a4..c1345a3d993 100644 --- a/libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts +++ b/libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts @@ -5,17 +5,17 @@ import { NotificationId, UserId } from "@bitwarden/common/types/guid"; import { NotificationView } from "../models"; /** - * A service for retrieving and managing notifications for end users. + * A service for retrieving and managing server notifications for end users. */ export abstract class EndUserNotificationService { /** - * Observable of all notifications for the given user. + * Observable of all server notifications for the given user. * @param userId */ abstract notifications$(userId: UserId): Observable; /** - * Observable of all unread notifications for the given user. + * Observable of all unread server notifications for the given user. * @param userId */ abstract unreadNotifications$(userId: UserId): Observable; @@ -35,13 +35,13 @@ export abstract class EndUserNotificationService { abstract markAsDeleted(notificationId: NotificationId, userId: UserId): Promise; /** - * Clear all notifications from state for the given user. + * Clear all server notifications from state for the given user. * @param userId */ abstract clearState(userId: UserId): Promise; /** - * Creates a subscription to listen for end user push notifications and notification status updates. + * Creates a subscription to listen for end user push server notifications and notification status updates. */ abstract listenForEndUserNotifications(): Subscription; } diff --git a/libs/common/src/vault/notifications/services/default-end-user-notification.service.spec.ts b/libs/common/src/vault/notifications/services/default-end-user-notification.service.spec.ts index 3a5ba06508e..9eecf5eb496 100644 --- a/libs/common/src/vault/notifications/services/default-end-user-notification.service.spec.ts +++ b/libs/common/src/vault/notifications/services/default-end-user-notification.service.spec.ts @@ -4,7 +4,7 @@ import { firstValueFrom, of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { ServerNotificationsService } from "@bitwarden/common/platform/notifications"; +import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; import { StateProvider } from "@bitwarden/common/platform/state"; import { NotificationId, UserId } from "@bitwarden/common/types/guid"; @@ -48,7 +48,7 @@ describe("End User Notification Center Service", () => { }); describe("notifications$", () => { - it("should return notifications from state when not null", async () => { + it("should return server notifications from state when not null", async () => { fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ { id: "notification-id" as NotificationId, @@ -62,7 +62,7 @@ describe("End User Notification Center Service", () => { expect(mockLogService.warning).not.toHaveBeenCalled(); }); - it("should return notifications API when state is null", async () => { + it("should return server notifications API when state is null", async () => { mockApiService.send.mockResolvedValue({ data: [ { @@ -86,7 +86,7 @@ describe("End User Notification Center Service", () => { expect(mockLogService.warning).not.toHaveBeenCalled(); }); - it("should log a warning if there are more notifications available", async () => { + it("should log a warning if there are more server notifications available", async () => { mockApiService.send.mockResolvedValue({ data: [ ...new Array(DEFAULT_NOTIFICATION_PAGE_SIZE + 1).fill({ id: "notification-id" }), @@ -120,7 +120,7 @@ describe("End User Notification Center Service", () => { }); describe("unreadNotifications$", () => { - it("should return unread notifications from state when read value is null", async () => { + it("should return unread server notifications from state when read value is null", async () => { fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ { id: "notification-id" as NotificationId, @@ -136,7 +136,7 @@ describe("End User Notification Center Service", () => { }); describe("getNotifications", () => { - it("should call getNotifications returning notifications from API", async () => { + it("should call getNotifications returning server notifications from API", async () => { mockApiService.send.mockResolvedValue({ data: [ { @@ -156,7 +156,7 @@ describe("End User Notification Center Service", () => { ); }); - it("should update local state when notifications are updated", async () => { + it("should update local state when server notifications are updated", async () => { mockApiService.send.mockResolvedValue({ data: [ { diff --git a/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts b/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts index 621a5951ed5..688f90b555f 100644 --- a/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts +++ b/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts @@ -6,7 +6,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { NotificationType } from "@bitwarden/common/enums"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { ServerNotificationsService } from "@bitwarden/common/platform/notifications"; +import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; import { StateProvider } from "@bitwarden/common/platform/state"; import { NotificationId, UserId } from "@bitwarden/common/types/guid"; import { @@ -19,7 +19,7 @@ import { NotificationView, NotificationViewData, NotificationViewResponse } from import { NOTIFICATIONS } from "../state/end-user-notification.state"; /** - * The default number of notifications to fetch from the API. + * The default number of server notifications to fetch from the API. */ export const DEFAULT_NOTIFICATION_PAGE_SIZE = 50; @@ -30,7 +30,7 @@ const getLoggedInUserIds = map, UserId[]>(( ); /** - * A service for retrieving and managing notifications for end users. + * A service for retrieving and managing server notifications for end users. */ export class DefaultEndUserNotificationService implements EndUserNotificationService { constructor( @@ -100,7 +100,7 @@ export class DefaultEndUserNotificationService implements EndUserNotificationSer } /** - * Helper observable to filter notifications by the notification type and user ids + * Helper observable to filter server notifications by the notification type and user ids * Returns EMPTY if no user ids are provided * @param userIds * @private @@ -121,7 +121,7 @@ export class DefaultEndUserNotificationService implements EndUserNotificationSer } /** - * Creates a subscription to listen for end user push notifications and notification status updates. + * Creates a subscription to listen for end user push server notifications and notification status updates. */ listenForEndUserNotifications(): Subscription { return this.authService.authStatuses$ @@ -139,7 +139,7 @@ export class DefaultEndUserNotificationService implements EndUserNotificationSer } /** - * Fetches the notifications from the API and updates the local state + * Fetches the server notifications from the API and updates the local state * @param userId * @private */ @@ -164,7 +164,7 @@ export class DefaultEndUserNotificationService implements EndUserNotificationSer } /** - * Replaces the local state with notifications and returns the updated state + * Replaces the local state with server notifications and returns the updated state * @param userId * @param notifications * @private @@ -178,7 +178,7 @@ export class DefaultEndUserNotificationService implements EndUserNotificationSer /** * Updates the local state adding the new notification or updates an existing one with the same id - * Returns the entire updated notifications state + * Returns the entire updated server notifications state * @param userId * @param notification * @private @@ -203,7 +203,7 @@ export class DefaultEndUserNotificationService implements EndUserNotificationSer } /** - * Returns the local state for notifications + * Returns the local state for server notifications * @param userId * @private */ diff --git a/libs/common/src/vault/tasks/abstractions/task.service.ts b/libs/common/src/vault/tasks/abstractions/task.service.ts index 79cefff0b71..6908476251c 100644 --- a/libs/common/src/vault/tasks/abstractions/task.service.ts +++ b/libs/common/src/vault/tasks/abstractions/task.service.ts @@ -45,7 +45,7 @@ export abstract class TaskService { abstract markAsComplete(taskId: SecurityTaskId, userId: UserId): Promise; /** - * Creates a subscription for pending security task notifications or completed syncs for unlocked users. + * Creates a subscription for pending security task server notifications or completed syncs for unlocked users. */ abstract listenForTaskNotifications(): Subscription; } diff --git a/libs/common/src/vault/tasks/services/default-task.service.spec.ts b/libs/common/src/vault/tasks/services/default-task.service.spec.ts index 2322ab8f423..9d8559cd859 100644 --- a/libs/common/src/vault/tasks/services/default-task.service.spec.ts +++ b/libs/common/src/vault/tasks/services/default-task.service.spec.ts @@ -8,7 +8,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { NotificationType } from "@bitwarden/common/enums"; import { NotificationResponse } from "@bitwarden/common/models/response/notification.response"; import { Message, MessageListener } from "@bitwarden/common/platform/messaging"; -import { ServerNotificationsService } from "@bitwarden/common/platform/notifications"; +import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid"; import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; @@ -304,7 +304,7 @@ describe("Default task service", () => { }); describe("listenForTaskNotifications()", () => { - it("should not subscribe to notifications when there are no unlocked users", () => { + it("should not subscribe to server notifications when there are no unlocked users", () => { mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Locked, }); @@ -320,7 +320,7 @@ describe("Default task service", () => { subscription.unsubscribe(); }); - it("should not subscribe to notifications when no users have tasks enabled", () => { + it("should not subscribe to server notifications when no users have tasks enabled", () => { mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Unlocked, }); @@ -336,7 +336,7 @@ describe("Default task service", () => { subscription.unsubscribe(); }); - it("should subscribe to notifications when there are unlocked users with tasks enabled", () => { + it("should subscribe to server notifications when there are unlocked users with tasks enabled", () => { mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Unlocked, }); @@ -378,7 +378,7 @@ describe("Default task service", () => { subscription.unsubscribe(); }); - it("should ignore notifications for other users", async () => { + it("should ignore server notifications for other users", async () => { mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Unlocked, }); @@ -403,7 +403,7 @@ describe("Default task service", () => { subscription.unsubscribe(); }); - it("should ignore other notifications types", async () => { + it("should ignore other server notifications types", async () => { mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Unlocked, }); diff --git a/libs/common/src/vault/tasks/services/default-task.service.ts b/libs/common/src/vault/tasks/services/default-task.service.ts index 487ab16b2a2..6238076ccf5 100644 --- a/libs/common/src/vault/tasks/services/default-task.service.ts +++ b/libs/common/src/vault/tasks/services/default-task.service.ts @@ -17,7 +17,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { NotificationType } from "@bitwarden/common/enums"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { MessageListener } from "@bitwarden/common/platform/messaging"; -import { ServerNotificationsService } from "@bitwarden/common/platform/notifications"; +import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; import { StateProvider } from "@bitwarden/common/platform/state"; import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid"; import { @@ -171,7 +171,7 @@ export class DefaultTaskService implements TaskService { } /** - * Creates a subscription for pending security task notifications or completed syncs for unlocked users. + * Creates a subscription for pending security task server notifications or completed syncs for unlocked users. */ listenForTaskNotifications(): Subscription { return this.authService.authStatuses$ diff --git a/libs/components/src/toast/toast.service.ts b/libs/components/src/toast/toast.service.ts index f55fb3ada83..14b737040f9 100644 --- a/libs/components/src/toast/toast.service.ts +++ b/libs/components/src/toast/toast.service.ts @@ -17,7 +17,7 @@ export type ToastOptions = { }; /** - * Presents toast notifications + * Presents toast server notifications **/ @Injectable({ providedIn: "root" }) export class ToastService { diff --git a/libs/components/src/toast/toastr.component.ts b/libs/components/src/toast/toastr.component.ts index 3b7665f1d64..b8f8af6266c 100644 --- a/libs/components/src/toast/toastr.component.ts +++ b/libs/components/src/toast/toastr.component.ts @@ -5,7 +5,7 @@ import { Toast as BaseToastrComponent, ToastPackage, ToastrService } from "ngx-t import { ToastComponent } from "./toast.component"; /** - * Toasts are ephemeral notifications. They most often communicate the result of a user action. Due to their ephemeral nature, long messages and critical alerts should not utilize toasts. + * Toasts are ephemeral server notifications. They most often communicate the result of a user action. Due to their ephemeral nature, long messages and critical alerts should not utilize toasts. */ @Component({ template: ` diff --git a/libs/key-management-ui/src/lock/components/lock.component.ts b/libs/key-management-ui/src/lock/components/lock.component.ts index 92c8004e4c9..c7aa8969660 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.ts @@ -625,7 +625,7 @@ export class LockComponent implements OnInit, OnDestroy { } } - // Vault can be de-synced since notifications get ignored while locked. Need to check whether sync is required using the sync service. + // Vault can be de-synced since server notifications get ignored while locked. Need to check whether sync is required using the sync service. const startSync = new Date().getTime(); // TODO: This should probably not be blocking await this.syncService.fullSync(false); From 6a181f274c7b13a7174373fafdd6ca9fc669a465 Mon Sep 17 00:00:00 2001 From: Patrick Pimentel Date: Fri, 8 Aug 2025 16:30:05 -0400 Subject: [PATCH 11/12] docs(notification-processing): [PM-19877] System Notification Implementation - Removed incorrect comments for server notifications. --- .../overlay-notifications.background.spec.ts | 2 +- .../overlay-notifications.background.ts | 6 +++--- .../services/autofill-overlay-content.service.ts | 2 +- .../at-risk-passwords.component.ts | 2 +- .../src/services/jslib-services.module.ts | 2 +- .../autofill/services/domain-settings.service.ts | 4 ++-- .../end-user-notification.service.ts | 10 +++++----- .../default-end-user-notification.service.ts | 16 ++++++++-------- .../src/vault/tasks/abstractions/task.service.ts | 2 +- libs/components/src/toast/toast.service.ts | 2 +- libs/components/src/toast/toastr.component.ts | 2 +- 11 files changed, 25 insertions(+), 25 deletions(-) diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts index c59ca28719a..cf317de4fd2 100644 --- a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts @@ -451,7 +451,7 @@ describe("OverlayNotificationsBackground", () => { }); }); - describe("web requests that trigger server notifications", () => { + describe("web requests that trigger notifications", () => { const requestId = "123345"; const pageDetails = mock({ fields: [mock()] }); diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts index 7a7eb0028f3..e7126a57e9f 100644 --- a/apps/browser/src/autofill/background/overlay-notifications.background.ts +++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts @@ -41,7 +41,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg ) {} /** - * Initialize the overlay server notifications background service. + * Initialize the overlay notifications background service. */ async init() { this.setupExtensionListeners(); @@ -395,7 +395,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg }; /** - * This method attempts to trigger the add login, change password, or at-risk password server notifications + * This method attempts to trigger the add login, change password, or at-risk password notifications * based on the modified login data and the tab details. * * @param requestId - The details of the web response @@ -462,7 +462,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg case NotificationTypes.AtRiskPassword: return !modifyLoginData.newPassword; case NotificationTypes.Unlock: - // Unlock server notifications are handled separately and do not require form data + // Unlock notifications are handled separately and do not require form data return false; default: this.logService.error(`Unknown notification type: ${notificationType}`); 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 fc8ce376f12..4db00901759 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -643,7 +643,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ }; /** - * Returns the form field data used for add login and change password server notifications. + * Returns the form field data used for add login and change password notifications. */ private getFormFieldData = (): ModifyLoginCipherFormData => { return { diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts index d144169a715..1bfb65a15cc 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts @@ -218,7 +218,7 @@ export class AtRiskPasswordsComponent implements OnInit { }); }), concatMap((unreadTaskNotifications) => { - // TODO: Investigate creating a bulk endpoint to mark server notifications as read + // TODO: Investigate creating a bulk endpoint to mark notifications as read return concat( ...unreadTaskNotifications.map((n) => this.endUserNotificationService.markAsRead(n.id, userId), diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index f88088f4812..dcd2940913e 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1115,7 +1115,7 @@ const safeProviders: SafeProvider[] = [ // This is a slightly odd dependency tree for a specialized api service // it depends on SyncService so that new data can be retrieved through the sync // rather than updating the OrganizationService directly. Instead OrganizationService - // subscribes to sync server notifications and will update itself based on that. + // subscribes to sync notifications and will update itself based on that. deps: [ApiServiceAbstraction, SyncService], }), safeProvider({ diff --git a/libs/common/src/autofill/services/domain-settings.service.ts b/libs/common/src/autofill/services/domain-settings.service.ts index d08fe07c50d..b2833b9ee25 100644 --- a/libs/common/src/autofill/services/domain-settings.service.ts +++ b/libs/common/src/autofill/services/domain-settings.service.ts @@ -25,7 +25,7 @@ const SHOW_FAVICONS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "showFavicons", { deserializer: (value: boolean) => value ?? true, }); -// Domain exclusion list for server notifications +// Domain exclusion list for notifications const NEVER_DOMAINS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "neverDomains", { deserializer: (value: NeverDomains) => value ?? null, }); @@ -64,7 +64,7 @@ export abstract class DomainSettingsService { setShowFavicons: (newValue: boolean) => Promise; /** - * User-specified URIs for which the client server notifications should not appear + * User-specified URIs for which the client notifications should not appear */ neverDomains$: Observable; setNeverDomains: (newValue: NeverDomains) => Promise; diff --git a/libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts b/libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts index c1345a3d993..bc5dd4d97a4 100644 --- a/libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts +++ b/libs/common/src/vault/notifications/abstractions/end-user-notification.service.ts @@ -5,17 +5,17 @@ import { NotificationId, UserId } from "@bitwarden/common/types/guid"; import { NotificationView } from "../models"; /** - * A service for retrieving and managing server notifications for end users. + * A service for retrieving and managing notifications for end users. */ export abstract class EndUserNotificationService { /** - * Observable of all server notifications for the given user. + * Observable of all notifications for the given user. * @param userId */ abstract notifications$(userId: UserId): Observable; /** - * Observable of all unread server notifications for the given user. + * Observable of all unread notifications for the given user. * @param userId */ abstract unreadNotifications$(userId: UserId): Observable; @@ -35,13 +35,13 @@ export abstract class EndUserNotificationService { abstract markAsDeleted(notificationId: NotificationId, userId: UserId): Promise; /** - * Clear all server notifications from state for the given user. + * Clear all notifications from state for the given user. * @param userId */ abstract clearState(userId: UserId): Promise; /** - * Creates a subscription to listen for end user push server notifications and notification status updates. + * Creates a subscription to listen for end user push notifications and notification status updates. */ abstract listenForEndUserNotifications(): Subscription; } diff --git a/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts b/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts index 688f90b555f..e3be4c19b0a 100644 --- a/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts +++ b/libs/common/src/vault/notifications/services/default-end-user-notification.service.ts @@ -19,7 +19,7 @@ import { NotificationView, NotificationViewData, NotificationViewResponse } from import { NOTIFICATIONS } from "../state/end-user-notification.state"; /** - * The default number of server notifications to fetch from the API. + * The default number of notifications to fetch from the API. */ export const DEFAULT_NOTIFICATION_PAGE_SIZE = 50; @@ -30,7 +30,7 @@ const getLoggedInUserIds = map, UserId[]>(( ); /** - * A service for retrieving and managing server notifications for end users. + * A service for retrieving and managing notifications for end users. */ export class DefaultEndUserNotificationService implements EndUserNotificationService { constructor( @@ -100,7 +100,7 @@ export class DefaultEndUserNotificationService implements EndUserNotificationSer } /** - * Helper observable to filter server notifications by the notification type and user ids + * Helper observable to filter notifications by the notification type and user ids * Returns EMPTY if no user ids are provided * @param userIds * @private @@ -121,7 +121,7 @@ export class DefaultEndUserNotificationService implements EndUserNotificationSer } /** - * Creates a subscription to listen for end user push server notifications and notification status updates. + * Creates a subscription to listen for end user push notifications and notification status updates. */ listenForEndUserNotifications(): Subscription { return this.authService.authStatuses$ @@ -139,7 +139,7 @@ export class DefaultEndUserNotificationService implements EndUserNotificationSer } /** - * Fetches the server notifications from the API and updates the local state + * Fetches the notifications from the API and updates the local state * @param userId * @private */ @@ -164,7 +164,7 @@ export class DefaultEndUserNotificationService implements EndUserNotificationSer } /** - * Replaces the local state with server notifications and returns the updated state + * Replaces the local state with notifications and returns the updated state * @param userId * @param notifications * @private @@ -178,7 +178,7 @@ export class DefaultEndUserNotificationService implements EndUserNotificationSer /** * Updates the local state adding the new notification or updates an existing one with the same id - * Returns the entire updated server notifications state + * Returns the entire updated notifications state * @param userId * @param notification * @private @@ -203,7 +203,7 @@ export class DefaultEndUserNotificationService implements EndUserNotificationSer } /** - * Returns the local state for server notifications + * Returns the local state for notifications * @param userId * @private */ diff --git a/libs/common/src/vault/tasks/abstractions/task.service.ts b/libs/common/src/vault/tasks/abstractions/task.service.ts index 6908476251c..79cefff0b71 100644 --- a/libs/common/src/vault/tasks/abstractions/task.service.ts +++ b/libs/common/src/vault/tasks/abstractions/task.service.ts @@ -45,7 +45,7 @@ export abstract class TaskService { abstract markAsComplete(taskId: SecurityTaskId, userId: UserId): Promise; /** - * Creates a subscription for pending security task server notifications or completed syncs for unlocked users. + * Creates a subscription for pending security task notifications or completed syncs for unlocked users. */ abstract listenForTaskNotifications(): Subscription; } diff --git a/libs/components/src/toast/toast.service.ts b/libs/components/src/toast/toast.service.ts index 14b737040f9..f55fb3ada83 100644 --- a/libs/components/src/toast/toast.service.ts +++ b/libs/components/src/toast/toast.service.ts @@ -17,7 +17,7 @@ export type ToastOptions = { }; /** - * Presents toast server notifications + * Presents toast notifications **/ @Injectable({ providedIn: "root" }) export class ToastService { diff --git a/libs/components/src/toast/toastr.component.ts b/libs/components/src/toast/toastr.component.ts index b8f8af6266c..3b7665f1d64 100644 --- a/libs/components/src/toast/toastr.component.ts +++ b/libs/components/src/toast/toastr.component.ts @@ -5,7 +5,7 @@ import { Toast as BaseToastrComponent, ToastPackage, ToastrService } from "ngx-t import { ToastComponent } from "./toast.component"; /** - * Toasts are ephemeral server notifications. They most often communicate the result of a user action. Due to their ephemeral nature, long messages and critical alerts should not utilize toasts. + * Toasts are ephemeral notifications. They most often communicate the result of a user action. Due to their ephemeral nature, long messages and critical alerts should not utilize toasts. */ @Component({ template: ` From a3bc3e7975eca85eb784572a35e36ae77b490433 Mon Sep 17 00:00:00 2001 From: Patrick Pimentel Date: Fri, 8 Aug 2025 16:34:53 -0400 Subject: [PATCH 12/12] docs(notification-processing): [PM-19877] System Notification Implementation - Removed incorrect comments for server notifications. --- .../vault/tasks/services/default-task.service.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/common/src/vault/tasks/services/default-task.service.spec.ts b/libs/common/src/vault/tasks/services/default-task.service.spec.ts index 9d8559cd859..df755dd9f90 100644 --- a/libs/common/src/vault/tasks/services/default-task.service.spec.ts +++ b/libs/common/src/vault/tasks/services/default-task.service.spec.ts @@ -304,7 +304,7 @@ describe("Default task service", () => { }); describe("listenForTaskNotifications()", () => { - it("should not subscribe to server notifications when there are no unlocked users", () => { + it("should not subscribe to notifications when there are no unlocked users", () => { mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Locked, }); @@ -320,7 +320,7 @@ describe("Default task service", () => { subscription.unsubscribe(); }); - it("should not subscribe to server notifications when no users have tasks enabled", () => { + it("should not subscribe to notifications when no users have tasks enabled", () => { mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Unlocked, }); @@ -336,7 +336,7 @@ describe("Default task service", () => { subscription.unsubscribe(); }); - it("should subscribe to server notifications when there are unlocked users with tasks enabled", () => { + it("should subscribe to notifications when there are unlocked users with tasks enabled", () => { mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Unlocked, }); @@ -378,7 +378,7 @@ describe("Default task service", () => { subscription.unsubscribe(); }); - it("should ignore server notifications for other users", async () => { + it("should ignore notifications for other users", async () => { mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Unlocked, }); @@ -403,7 +403,7 @@ describe("Default task service", () => { subscription.unsubscribe(); }); - it("should ignore other server notifications types", async () => { + it("should ignore other notifications types", async () => { mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Unlocked, });