1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

[PM-10611] End user notification sync (#14116)

* [PM-10611] Remove Angular dependencies from Notifications module

* [PM-10611] Move end user notification service to /libs/common/vault/notifications

* [PM-10611] Implement listenForEndUserNotifications() for EndUserNotificationService

* [PM-10611] Add missing taskId to notification models

* [PM-10611] Add switch cases for end user notification payloads

* [PM-10611] Mark task related notifications as read when visiting the at-risk password page

* [PM-10611] Revert change to default-notifications service

* [PM-10611] Fix test

* [PM-10611] Fix tests and log warning in case more notifications than the default page size are available

* [PM-10611] Use separate feature flag for end user notifications

* [PM-10611] Fix test
This commit is contained in:
Shane Melton
2025-04-21 08:57:57 -07:00
committed by GitHub
parent 43b1f55360
commit 143473927e
19 changed files with 557 additions and 344 deletions

View File

@@ -191,6 +191,10 @@ import { InternalFolderService as InternalFolderServiceAbstraction } from "@bitw
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
DefaultEndUserNotificationService,
EndUserNotificationService,
} from "@bitwarden/common/vault/notifications";
import {
CipherAuthorizationService,
DefaultCipherAuthorizationService,
@@ -402,6 +406,7 @@ export default class MainBackground {
sdkService: SdkService;
sdkLoadService: SdkLoadService;
cipherAuthorizationService: CipherAuthorizationService;
endUserNotificationService: EndUserNotificationService;
inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
taskService: TaskService;
@@ -1320,6 +1325,14 @@ export default class MainBackground {
this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService);
this.ipcService = new IpcBackgroundService(this.logService);
this.endUserNotificationService = new DefaultEndUserNotificationService(
this.stateProvider,
this.apiService,
this.notificationsService,
this.authService,
this.logService,
);
}
async bootstrap() {
@@ -1406,6 +1419,9 @@ export default class MainBackground {
this.taskService.listenForTaskNotifications();
}
if (await this.configService.getFeatureFlag(FeatureFlag.EndUserNotifications)) {
this.endUserNotificationService.listenForEndUserNotifications();
}
resolve();
}, 500);
});

View File

@@ -12,10 +12,13 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { EndUserNotificationService } from "@bitwarden/common/vault/notifications";
import { NotificationView } from "@bitwarden/common/vault/notifications/models";
import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
import { DialogService, ToastService } from "@bitwarden/components";
import {
@@ -66,6 +69,7 @@ describe("AtRiskPasswordsComponent", () => {
let mockTasks$: BehaviorSubject<SecurityTask[]>;
let mockCiphers$: BehaviorSubject<CipherView[]>;
let mockOrgs$: BehaviorSubject<Organization[]>;
let mockNotifications$: BehaviorSubject<NotificationView[]>;
let mockInlineMenuVisibility$: BehaviorSubject<InlineMenuVisibilitySetting>;
let calloutDismissed$: BehaviorSubject<boolean>;
const setInlineMenuVisibility = jest.fn();
@@ -73,6 +77,7 @@ describe("AtRiskPasswordsComponent", () => {
const mockAtRiskPasswordPageService = mock<AtRiskPasswordPageService>();
const mockChangeLoginPasswordService = mock<ChangeLoginPasswordService>();
const mockDialogService = mock<DialogService>();
const mockConfigService = mock<ConfigService>();
beforeEach(async () => {
mockTasks$ = new BehaviorSubject<SecurityTask[]>([
@@ -101,6 +106,7 @@ describe("AtRiskPasswordsComponent", () => {
name: "Org 1",
} as Organization,
]);
mockNotifications$ = new BehaviorSubject<NotificationView[]>([]);
mockInlineMenuVisibility$ = new BehaviorSubject<InlineMenuVisibilitySetting>(
AutofillOverlayVisibility.Off,
@@ -110,6 +116,7 @@ describe("AtRiskPasswordsComponent", () => {
setInlineMenuVisibility.mockClear();
mockToastService.showToast.mockClear();
mockDialogService.open.mockClear();
mockConfigService.getFeatureFlag.mockClear();
mockAtRiskPasswordPageService.isCalloutDismissed.mockReturnValue(calloutDismissed$);
await TestBed.configureTestingModule({
@@ -133,6 +140,12 @@ describe("AtRiskPasswordsComponent", () => {
cipherViews$: () => mockCiphers$,
},
},
{
provide: EndUserNotificationService,
useValue: {
unreadNotifications$: () => mockNotifications$,
},
},
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: AccountService, useValue: { activeAccount$: of({ id: "user" }) } },
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
@@ -145,6 +158,7 @@ describe("AtRiskPasswordsComponent", () => {
},
},
{ provide: ToastService, useValue: mockToastService },
{ provide: ConfigService, useValue: mockConfigService },
],
})
.overrideModule(JslibModule, {

View File

@@ -1,7 +1,19 @@
import { CommonModule } from "@angular/common";
import { Component, inject, OnInit, signal } from "@angular/core";
import { Component, DestroyRef, inject, OnInit, signal } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Router } from "@angular/router";
import { combineLatest, firstValueFrom, map, of, shareReplay, startWith, switchMap } from "rxjs";
import {
combineLatest,
concat,
concatMap,
firstValueFrom,
map,
of,
shareReplay,
startWith,
switchMap,
take,
} from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
@@ -11,10 +23,13 @@ import {
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { EndUserNotificationService } from "@bitwarden/common/vault/notifications";
import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
import {
@@ -81,6 +96,9 @@ export class AtRiskPasswordsComponent implements OnInit {
private changeLoginPasswordService = inject(ChangeLoginPasswordService);
private platformUtilsService = inject(PlatformUtilsService);
private dialogService = inject(DialogService);
private endUserNotificationService = inject(EndUserNotificationService);
private configService = inject(ConfigService);
private destroyRef = inject(DestroyRef);
/**
* The cipher that is currently being launched. Used to show a loading spinner on the badge button.
@@ -180,6 +198,36 @@ export class AtRiskPasswordsComponent implements OnInit {
await this.atRiskPasswordPageService.dismissGettingStarted(userId);
}
}
if (await this.configService.getFeatureFlag(FeatureFlag.EndUserNotifications)) {
this.markTaskNotificationsAsRead();
}
}
private markTaskNotificationsAsRead() {
this.activeUserData$
.pipe(
switchMap(({ tasks, userId }) => {
return this.endUserNotificationService.unreadNotifications$(userId).pipe(
take(1),
map((notifications) => {
return notifications.filter((notification) => {
return tasks.some((task) => task.id === notification.taskId);
});
}),
concatMap((unreadTaskNotifications) => {
// TODO: Investigate creating a bulk endpoint to mark notifications as read
return concat(
...unreadTaskNotifications.map((n) =>
this.endUserNotificationService.markAsRead(n.id, userId),
),
);
}),
);
}),
takeUntilDestroyed(this.destroyRef),
)
.subscribe();
}
async viewCipher(cipher: CipherView) {