1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-22 12:24:01 +00:00

Merge branch 'auth/pm-19877/notification-processing' into auth/pm-23620/auth-request-answering-service

This commit is contained in:
Patrick Pimentel
2025-08-11 10:54:23 -04:00
86 changed files with 869 additions and 584 deletions

View File

@@ -1,4 +1,4 @@
import { SystemNotificationEvent } from "@bitwarden/common/platform/notifications/system-notifications.service";
import { SystemNotificationEvent } from "@bitwarden/common/platform/system-notifications/system-notifications.service";
import { UserId } from "@bitwarden/user-core";
export abstract class AuthRequestAnsweringServiceAbstraction {

View File

@@ -12,7 +12,7 @@ import {
ButtonLocation,
SystemNotificationEvent,
SystemNotificationsService,
} from "@bitwarden/common/platform/notifications/system-notifications.service";
} from "@bitwarden/common/platform/system-notifications/system-notifications.service";
import { UserId } from "@bitwarden/user-core";
import { AuthRequestAnsweringServiceAbstraction } from "../../abstractions/auth-request-answering/auth-request-answering.service.abstraction";

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

@@ -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<ServerConfigResponse>;
abstract get(userId: UserId | null): Promise<ServerConfigResponse>;
}

View File

@@ -95,6 +95,13 @@ export interface Environment {
*/
export abstract class EnvironmentService {
abstract environment$: Observable<Environment>;
/**
* The environment stored in global state, when a user signs in the state stored here will become
* their user environment.
*/
abstract globalEnvironment$: Observable<Environment>;
abstract cloudWebVaultUrl$: Observable<string>;
/**
@@ -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<void>;
abstract setCloudRegion(userId: UserId | null, region: Region): Promise<void>;
/**
* Get the environment from state. Useful if you need to get the environment for another user.
*/
abstract getEnvironment$(userId: UserId): Observable<Environment | undefined>;
abstract getEnvironment$(userId: UserId): Observable<Environment>;
/**
* @deprecated Use {@link getEnvironment$} instead.

View File

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

View File

@@ -137,7 +137,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);
@@ -230,7 +230,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 },
@@ -259,7 +259,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,
])(
@@ -285,7 +285,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

@@ -63,7 +63,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;
}
@@ -76,8 +76,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(
@@ -111,7 +111,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);
@@ -120,7 +120,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);
},
}),
@@ -240,7 +240,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

@@ -10,7 +10,7 @@ export class ConfigApiService implements ConfigApiServiceAbstraction {
private tokenService: TokenService,
) {}
async get(userId: UserId | undefined): Promise<ServerConfigResponse> {
async get(userId: UserId | null): Promise<ServerConfigResponse> {
// 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 =

View File

@@ -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<Environment>();
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({

View File

@@ -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<ServerConfig, A
},
);
const environmentComparer = (previous: Environment, current: Environment) => {
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<ServerConfig>();
private failedFetchFallbackSubject = new Subject<ServerConfig | null>();
serverConfig$: Observable<ServerConfig>;
serverConfig$: Observable<ServerConfig | null>;
serverSettings$: Observable<ServerSettings>;
@@ -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<void> {
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<ServerConfig> {
private globalConfigFor$(apiUrl: string): Observable<ServerConfig | null> {
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<ServerConfig> {
private userConfigFor$(userId: UserId): Observable<ServerConfig | null> {
return this.stateProvider.getUser(userId, USER_SERVER_CONFIG).state$;
}
}

View File

@@ -133,6 +133,7 @@ export class DefaultEnvironmentService implements EnvironmentService {
);
environment$: Observable<Environment>;
globalEnvironment$: Observable<Environment>;
cloudWebVaultUrl$: Observable<string>;
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<Environment | undefined> {
getEnvironment$(userId: UserId): Observable<Environment> {
return this.stateProvider.getUser(userId, USER_ENVIRONMENT_KEY).state$.pipe(
map((state) => {
return this.buildEnvironment(state?.region, state?.urls);

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

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

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

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$