1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-19 02:44:01 +00:00

fix(notification-processing): [PM-19877] System Notification Implementation - Addressed more feedback.

This commit is contained in:
Patrick Pimentel
2025-08-08 13:10:28 -04:00
parent 81281a2b6a
commit 49bef8ee3c
49 changed files with 112 additions and 140 deletions

View File

@@ -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<void>;
/**
* 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<NeverDomains>;
setNeverDomains: (newValue: NeverDomains) => Promise<void>;

View File

@@ -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,
}

View File

@@ -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);

View File

@@ -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),
});
}

View File

@@ -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";

View File

@@ -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<readonly [NotificationResponse, UserId]> = 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;
}

View File

@@ -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<void> {
const request = WebPushRequest.from(pushSubscription);

View File

@@ -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<PushEvent>();
@@ -75,7 +75,7 @@ export class WorkerWebPushConnectionService implements WebPushConnectionService
}
supportStatus$(userId: UserId): Observable<SupportStatus<WebPushConnector>> {
// 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) =>

View File

@@ -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<readonly [NotificationResponse, UserId]>;
/**
* 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;

View File

@@ -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 {

View File

@@ -0,0 +1 @@
export { SystemNotificationsService } from "./system-notifications.service";

View File

@@ -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<SystemNotificationEvent>;
@@ -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<string | undefined>;
abstract create(createInfo: SystemNotificationCreateInfo): Promise<string>;
/**
* Clears a notification.
@@ -52,7 +52,7 @@ export abstract class SystemNotificationsService {
abstract clear(clearInfo: SystemNotificationClearInfo): Promise<void>;
/**
* Used to know if a given platform supports notifications.
* Used to know if a given platform supports server notifications.
*/
abstract isSupported(): boolean;
}

View File

@@ -9,7 +9,7 @@ import {
export class UnsupportedSystemNotificationsService implements SystemNotificationsService {
notificationClicked$ = throwError(() => new Error("Notification clicked is not supported."));
async create(createInfo: SystemNotificationCreateInfo): Promise<undefined> {
async create(createInfo: SystemNotificationCreateInfo): Promise<string> {
throw new Error("Create OS Notification unsupported.");
}

View File

@@ -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<NotificationView[]>;
/**
* 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<NotificationView[]>;
@@ -35,13 +35,13 @@ export abstract class EndUserNotificationService {
abstract markAsDeleted(notificationId: NotificationId, userId: UserId): Promise<void>;
/**
* 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<void>;
/**
* 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;
}

View File

@@ -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: [
{

View File

@@ -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<Record<UserId, AuthenticationStatus>, 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
*/

View File

@@ -45,7 +45,7 @@ export abstract class TaskService {
abstract markAsComplete(taskId: SecurityTaskId, userId: UserId): Promise<void>;
/**
* 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;
}

View File

@@ -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,
});

View File

@@ -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$