1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

Feat PM-19877 System Notification Processing (#15611)

* feat(notification-processing): [PM-19877] System Notification Implementation - Minor changes to popup logic and removed content in login component.

* docs(notification-processing): [PM-19877] System Notification Implementation - Added more docs.

* docs(notification-processing): [PM-19877] System Notification Implementation - Added markdown document.

* fix(notification-processing): [PM-19877] System Notification Implementation - Updated condition for if notification is supported.

* fix(notification-processing): [PM-19877] System Notification Implementation - Updated services module with correct platform utils service.
This commit is contained in:
Patrick-Pimentel-Bitwarden
2025-08-20 12:42:16 -04:00
committed by GitHub
parent bcd73a9c00
commit 719a43d050
55 changed files with 420 additions and 132 deletions

View File

@@ -7,7 +7,7 @@ import {
VaultTimeoutSettingsService,
VaultTimeoutStringType,
} from "@bitwarden/common/key-management/vault-timeout";
import { NotificationsService } from "@bitwarden/common/platform/notifications";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
const IdleInterval = 60 * 5; // 5 minutes
@@ -18,7 +18,7 @@ export default class IdleBackground {
constructor(
private vaultTimeoutService: VaultTimeoutService,
private notificationsService: NotificationsService,
private serverNotificationsService: ServerNotificationsService,
private accountService: AccountService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
) {
@@ -32,9 +32,9 @@ export default class IdleBackground {
const idleHandler = (newState: string) => {
if (newState === "active") {
this.notificationsService.reconnectFromActivity();
this.serverNotificationsService.reconnectFromActivity();
} else {
this.notificationsService.disconnectFromInactivity();
this.serverNotificationsService.disconnectFromInactivity();
}
};
if (this.idle.onStateChanged && this.idle.setDetectionInterval) {

View File

@@ -111,21 +111,22 @@ import {
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
import { ActionsService } from "@bitwarden/common/platform/actions/actions-service";
import { IpcService } from "@bitwarden/common/platform/ipc";
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- Used for dependency creation
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { NotificationsService } 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 {
DefaultNotificationsService,
DefaultServerNotificationsService,
SignalRConnectionService,
UnsupportedWebPushConnectionService,
WebPushNotificationsApiService,
WorkerWebPushConnectionService,
} from "@bitwarden/common/platform/notifications/internal";
} 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";
@@ -164,6 +165,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";
@@ -264,6 +267,7 @@ import { InlineMenuFieldQualificationService } from "../autofill/services/inline
import { SafariApp } from "../browser/safariApp";
import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service";
import VaultTimeoutService from "../key-management/vault-timeout/vault-timeout.service";
import { BrowserActionsService } from "../platform/actions/browser-actions.service";
import { DefaultBadgeBrowserApi } from "../platform/badge/badge-browser-api";
import { BadgeService } from "../platform/badge/badge.service";
import { BrowserApi } from "../platform/browser/browser-api";
@@ -292,6 +296,7 @@ import { BackgroundMemoryStorageService } from "../platform/storage/background-m
import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider";
import { OffscreenStorageService } from "../platform/storage/offscreen-storage.service";
import { SyncServiceListener } from "../platform/sync/sync-service.listener";
import { BrowserSystemNotificationService } from "../platform/system-notifications/browser-system-notification.service";
import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging";
import { VaultFilterService } from "../vault/services/vault-filter.service";
@@ -337,7 +342,9 @@ export default class MainBackground {
importService: ImportServiceAbstraction;
exportService: VaultExportServiceAbstraction;
searchService: SearchServiceAbstraction;
notificationsService: NotificationsService;
serverNotificationsService: ServerNotificationsService;
systemNotificationService: SystemNotificationsService;
actionsService: ActionsService;
stateService: StateServiceAbstraction;
userNotificationSettingsService: UserNotificationSettingsServiceAbstraction;
autofillSettingsService: AutofillSettingsServiceAbstraction;
@@ -439,7 +446,6 @@ export default class MainBackground {
private webRequestBackground: WebRequestBackground;
private syncTimeout: any;
private isSafari: boolean;
private nativeMessagingBackground: NativeMessagingBackground;
private popupViewCacheBackgroundService: PopupViewCacheBackgroundService;
@@ -1109,7 +1115,18 @@ export default class MainBackground {
this.webPushConnectionService = new UnsupportedWebPushConnectionService();
}
this.notificationsService = new DefaultNotificationsService(
this.actionsService = new BrowserActionsService(this.logService, this.platformUtilsService);
if ("notifications" in chrome) {
this.systemNotificationService = new BrowserSystemNotificationService(
this.logService,
this.platformUtilsService,
);
} else {
this.systemNotificationService = new UnsupportedSystemNotificationsService();
}
this.serverNotificationsService = new DefaultServerNotificationsService(
this.logService,
this.syncService,
this.appIdService,
@@ -1163,9 +1180,6 @@ export default class MainBackground {
this.logService,
);
// Other fields
this.isSafari = this.platformUtilsService.isSafari();
// Background
this.fido2Background = new Fido2Background(
@@ -1183,7 +1197,6 @@ export default class MainBackground {
this,
this.autofillService,
this.platformUtilsService as BrowserPlatformUtilsService,
this.notificationsService,
this.autofillSettingsService,
this.processReloadService,
this.environmentService,
@@ -1222,7 +1235,7 @@ export default class MainBackground {
this.apiService,
this.organizationService,
this.authService,
this.notificationsService,
this.serverNotificationsService,
messageListener,
);
@@ -1296,7 +1309,7 @@ export default class MainBackground {
this.idleBackground = new IdleBackground(
this.vaultTimeoutService,
this.notificationsService,
this.serverNotificationsService,
this.accountService,
this.vaultTimeoutSettingsService,
);
@@ -1354,7 +1367,7 @@ export default class MainBackground {
this.endUserNotificationService = new DefaultEndUserNotificationService(
this.stateProvider,
this.apiService,
this.notificationsService,
this.serverNotificationsService,
this.authService,
this.logService,
);
@@ -1433,7 +1446,7 @@ export default class MainBackground {
setTimeout(async () => {
await this.fullSync(false);
this.backgroundSyncService.init();
this.notificationsService.startListening();
this.serverNotificationsService.startListening();
this.taskService.listenForTaskNotifications();
this.endUserNotificationService.listenForEndUserNotifications();
@@ -1656,6 +1669,11 @@ export default class MainBackground {
);
}
/**
* Opens the popup.
*
* @deprecated Migrating to the browser actions service.
*/
async openPopup() {
const browserAction = BrowserApi.getBrowserAction();
@@ -1664,7 +1682,7 @@ export default class MainBackground {
return;
}
if (this.isSafari) {
if (this.platformUtilsService.isSafari()) {
await SafariApp.sendMessageToApp("showPopover", null, true);
}
}
@@ -1691,7 +1709,9 @@ export default class MainBackground {
/**
* Opens the popup to the given page
*
* @default ExtensionPageUrls.Index
* @deprecated Migrating to the browser actions service.
*/
async openTheExtensionToPage(url: ExtensionPageUrls = ExtensionPageUrls.Index) {
const isValidUrl = Object.values(ExtensionPageUrls).includes(url);

View File

@@ -15,7 +15,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/messaging";
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { NotificationsService } from "@bitwarden/common/platform/notifications";
import { CipherType } from "@bitwarden/common/vault/enums";
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
import { BiometricsCommands } from "@bitwarden/key-management";
@@ -46,7 +45,6 @@ export default class RuntimeBackground {
private main: MainBackground,
private autofillService: AutofillService,
private platformUtilsService: BrowserPlatformUtilsService,
private notificationsService: NotificationsService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private processReloadService: ProcessReloadServiceAbstraction,
private environmentService: BrowserEnvironmentService,
@@ -424,6 +422,11 @@ export default class RuntimeBackground {
return await BrowserApi.tabsQuery({ url: `${urlObj.href}*` });
}
/**
* Opens the popup.
*
* @deprecated Migrating to the browser actions service.
*/
private async openPopup() {
await this.main.openPopup();
}
@@ -450,7 +453,7 @@ export default class RuntimeBackground {
/** Sends a message to each tab that the popup was opened */
private announcePopupOpen() {
const announceToAllTabs = async () => {
const isOpen = await this.platformUtilsService.isViewOpen();
const isOpen = await this.platformUtilsService.isPopupOpen();
const tabs = await this.getBwTabs();
if (isOpen && tabs.length > 0) {

View File

@@ -0,0 +1,48 @@
import { DeviceType } from "@bitwarden/common/enums";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ActionsService } from "@bitwarden/common/platform/actions/actions-service";
import { LogService } from "@bitwarden/logging";
import { SafariApp } from "../../browser/safariApp";
import { BrowserApi } from "../browser/browser-api";
export class BrowserActionsService implements ActionsService {
constructor(
private logService: LogService,
private platformUtilsService: PlatformUtilsService,
) {}
async openPopup(): Promise<void> {
const deviceType = this.platformUtilsService.getDevice();
try {
switch (deviceType) {
case DeviceType.FirefoxExtension:
case DeviceType.ChromeExtension: {
const browserAction = BrowserApi.getBrowserAction();
if ("openPopup" in browserAction && typeof browserAction.openPopup === "function") {
await browserAction.openPopup();
return;
} else {
this.logService.warning(
`No openPopup function found on browser actions. On browser: ${deviceType} and manifest version: ${BrowserApi.manifestVersion}`,
);
}
break;
}
case DeviceType.SafariExtension:
await SafariApp.sendMessageToApp("showPopover", null, true);
return;
default:
this.logService.warning(
`Tried to open the popup from an unsupported device type: ${deviceType}`,
);
}
} catch (e) {
this.logService.error(
`Failed to open the popup on ${DeviceType[deviceType]} with manifest ${BrowserApi.manifestVersion} and error: ${e}`,
);
}
}
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { BrowserApi } from "./browser-api";
@@ -26,13 +24,13 @@ export function fromChromeEvent<T extends unknown[]>(
event: chrome.events.Event<(...args: T) => void>,
): Observable<T> {
return new Observable<T>((subscriber) => {
const handler = (...args: T) => {
const handler = (...args: readonly unknown[]) => {
if (chrome.runtime.lastError) {
subscriber.error(chrome.runtime.lastError);
return;
}
subscriber.next(args);
subscriber.next(args as T);
};
BrowserApi.addListener(event, handler);

View File

@@ -2,12 +2,12 @@ import { Observable, Subscription } from "rxjs";
import { NotificationResponse } from "@bitwarden/common/models/response/notification.response";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { NotificationsService } 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 ForegroundNotificationsService implements NotificationsService {
export class ForegroundServerNotificationsService implements ServerNotificationsService {
notifications$: Observable<readonly [NotificationResponse, UserId]>;
constructor(private readonly logService: LogService) {

View File

@@ -150,7 +150,7 @@ describe("Browser Utils Service", () => {
callback(undefined);
});
const isViewOpen = await browserPlatformUtilsService.isViewOpen();
const isViewOpen = await browserPlatformUtilsService.isPopupOpen();
expect(isViewOpen).toBe(false);
});
@@ -160,7 +160,7 @@ describe("Browser Utils Service", () => {
callback(message.command === "checkVaultPopupHeartbeat");
});
const isViewOpen = await browserPlatformUtilsService.isViewOpen();
const isViewOpen = await browserPlatformUtilsService.isPopupOpen();
expect(isViewOpen).toBe(true);
});
@@ -173,7 +173,7 @@ describe("Browser Utils Service", () => {
callback(undefined);
});
const isViewOpen = await browserPlatformUtilsService.isViewOpen();
const isViewOpen = await browserPlatformUtilsService.isPopupOpen();
expect(isViewOpen).toBe(false);

View File

@@ -150,7 +150,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
* message to the popup and waiting for a response. If a response is received,
* the view is open.
*/
async isViewOpen(): Promise<boolean> {
async isPopupOpen(): Promise<boolean> {
if (this.isSafari()) {
// Query views on safari since chrome.runtime.sendMessage does not timeout and will hang.
return BrowserApi.isPopupOpen();

View File

@@ -0,0 +1,60 @@
import { map, merge, Observable } from "rxjs";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
ButtonLocation,
SystemNotificationClearInfo,
SystemNotificationCreateInfo,
SystemNotificationEvent,
SystemNotificationsService,
} from "@bitwarden/common/platform/system-notifications/system-notifications.service";
import { fromChromeEvent } from "../browser/from-chrome-event";
export class BrowserSystemNotificationService implements SystemNotificationsService {
notificationClicked$: Observable<SystemNotificationEvent>;
constructor(
private logService: LogService,
private platformUtilsService: PlatformUtilsService,
) {
this.notificationClicked$ = merge(
fromChromeEvent(chrome.notifications.onButtonClicked).pipe(
map(([notificationId, buttonIndex]) => ({
id: notificationId,
buttonIdentifier: buttonIndex,
})),
),
fromChromeEvent(chrome.notifications.onClicked).pipe(
map(([notificationId]: [string]) => ({
id: notificationId,
buttonIdentifier: ButtonLocation.NotificationButton,
})),
),
);
}
async create(createInfo: SystemNotificationCreateInfo): Promise<string> {
return new Promise<string>((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),
);
});
}
async clear(clearInfo: SystemNotificationClearInfo): Promise<undefined> {
chrome.notifications.clear(clearInfo.id);
}
isSupported(): boolean {
return "notifications" in chrome;
}
}

View File

@@ -92,12 +92,13 @@ import {
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { ActionsService } from "@bitwarden/common/platform/actions";
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
// 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 { NotificationsService } from "@bitwarden/common/platform/notifications";
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";
@@ -113,6 +114,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";
@@ -160,13 +162,14 @@ import { InlineMenuFieldQualificationService } from "../../autofill/services/inl
import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics";
import { ExtensionLockComponentService } from "../../key-management/lock/services/extension-lock-component.service";
import { ForegroundVaultTimeoutService } from "../../key-management/vault-timeout/foreground-vault-timeout.service";
import { BrowserActionsService } from "../../platform/actions/browser-actions.service";
import { BrowserApi } from "../../platform/browser/browser-api";
import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator";
/* eslint-disable no-restricted-imports */
import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service";
import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sender";
/* eslint-enable no-restricted-imports */
import { ForegroundNotificationsService } from "../../platform/notifications/foreground-notifications.service";
import { ForegroundServerNotificationsService } from "../../platform/notifications/foreground-server-notifications.service";
import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document";
import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service";
import { PopupCompactModeService } from "../../platform/popup/layout/popup-compact-mode.service";
@@ -184,6 +187,7 @@ import { ForegroundTaskSchedulerService } from "../../platform/services/task-sch
import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider";
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
import { ForegroundSyncService } from "../../platform/sync/foreground-sync.service";
import { BrowserSystemNotificationService } from "../../platform/system-notifications/browser-system-notification.service";
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
import { Fido2UserVerificationService } from "../../vault/services/fido2-user-verification.service";
@@ -253,6 +257,11 @@ const safeProviders: SafeProvider[] = [
},
deps: [GlobalStateProvider],
}),
safeProvider({
provide: ActionsService,
useClass: BrowserActionsService,
deps: [LogService, PlatformUtilsService],
}),
safeProvider({
provide: KeyService,
useFactory: (
@@ -609,6 +618,11 @@ const safeProviders: SafeProvider[] = [
useClass: SsoUrlService,
deps: [],
}),
safeProvider({
provide: SystemNotificationsService,
useClass: BrowserSystemNotificationService,
deps: [LogService, PlatformUtilsService],
}),
safeProvider({
provide: LoginComponentService,
useClass: ExtensionLoginComponentService,
@@ -674,8 +688,8 @@ const safeProviders: SafeProvider[] = [
deps: [KeyService, MasterPasswordApiService, InternalMasterPasswordServiceAbstraction, WINDOW],
}),
safeProvider({
provide: NotificationsService,
useClass: ForegroundNotificationsService,
provide: ServerNotificationsService,
useClass: ForegroundServerNotificationsService,
deps: [LogService],
}),
safeProvider({

View File

@@ -75,7 +75,7 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
return false;
}
isViewOpen() {
isPopupOpen() {
return Promise.resolve(false);
}

View File

@@ -62,7 +62,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 { NotificationsService } 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";
@@ -155,7 +155,7 @@ export class AppComponent implements OnInit, OnDestroy {
private messagingService: MessagingService,
private collectionService: CollectionService,
private searchService: SearchService,
private notificationsService: NotificationsService,
private notificationsService: ServerNotificationsService,
private platformUtilsService: PlatformUtilsService,
private systemService: SystemService,
private processReloadService: ProcessReloadServiceAbstraction,

View File

@@ -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 { NotificationsService } from "@bitwarden/common/platform/notifications";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
@@ -38,7 +38,7 @@ export class InitService {
private i18nService: I18nServiceAbstraction,
private eventUploadService: EventUploadServiceAbstraction,
private twoFactorService: TwoFactorServiceAbstraction,
private notificationsService: NotificationsService,
private notificationsService: ServerNotificationsService,
private platformUtilsService: PlatformUtilsServiceAbstraction,
private stateService: StateServiceAbstraction,
private keyService: KeyServiceAbstraction,

View File

@@ -59,7 +59,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
return ipc.platform.isMacAppStore;
}
isViewOpen(): Promise<boolean> {
isPopupOpen(): Promise<boolean> {
return Promise.resolve(false);
}

View File

@@ -22,7 +22,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 { NotificationsService } 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";
@@ -76,7 +76,7 @@ export class AppComponent implements OnDestroy, OnInit {
private keyService: KeyService,
private collectionService: CollectionService,
private searchService: SearchService,
private notificationsService: NotificationsService,
private serverNotificationsService: ServerNotificationsService,
private stateService: StateService,
private eventUploadService: EventUploadService,
protected policyListService: PolicyListService,
@@ -88,14 +88,14 @@ export class AppComponent implements OnDestroy, OnInit {
private accountService: AccountService,
private processReloadService: ProcessReloadServiceAbstraction,
private deviceTrustToastService: DeviceTrustToastService,
private readonly destoryRef: DestroyRef,
private readonly destroy: DestroyRef,
private readonly documentLangSetter: DocumentLangSetter,
private readonly tokenService: TokenService,
) {
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
const langSubscription = this.documentLangSetter.start();
this.destoryRef.onDestroy(() => langSubscription.unsubscribe());
this.destroy.onDestroy(() => langSubscription.unsubscribe());
}
ngOnInit() {
@@ -347,9 +347,9 @@ export class AppComponent implements OnDestroy, OnInit {
private idleStateChanged() {
if (this.isIdle) {
this.notificationsService.disconnectFromInactivity();
this.serverNotificationsService.disconnectFromInactivity();
} else {
this.notificationsService.reconnectFromActivity();
this.serverNotificationsService.reconnectFromActivity();
}
}
}

View File

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

View File

@@ -12,7 +12,7 @@ import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vau
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { IpcService } from "@bitwarden/common/platform/ipc";
import { NotificationsService } from "@bitwarden/common/platform/notifications";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
@@ -26,7 +26,7 @@ import { VersionService } from "../platform/version.service";
export class InitService {
constructor(
@Inject(WINDOW) private win: Window,
private notificationsService: NotificationsService,
private serverNotificationsService: ServerNotificationsService,
private vaultTimeoutService: DefaultVaultTimeoutService,
private i18nService: I18nServiceAbstraction,
private eventUploadService: EventUploadServiceAbstraction,
@@ -56,7 +56,7 @@ export class InitService {
await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(activeAccount.id);
}
this.notificationsService.startListening();
this.serverNotificationsService.startListening();
await this.vaultTimeoutService.init(true);
await this.i18nService.init();
(this.eventUploadService as EventUploadService).init(true);

View File

@@ -98,7 +98,7 @@ export class WebPlatformUtilsService implements PlatformUtilsService {
return false;
}
isViewOpen(): Promise<boolean> {
isPopupOpen(): Promise<boolean> {
return Promise.resolve(false);
}

View File

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

View File

@@ -565,7 +565,7 @@ export class VaultComponent<C extends CipherViewLike> 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();
},
);

View File

@@ -196,24 +196,26 @@ import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.serv
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service";
import { ActionsService } from "@bitwarden/common/platform/actions";
import { UnsupportedActionsService } from "@bitwarden/common/platform/actions/unsupported-actions.service";
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
import { NotificationsService } from "@bitwarden/common/platform/notifications";
// eslint-disable-next-line no-restricted-imports -- Needed for service creation
import {
DefaultNotificationsService,
NoopNotificationsService,
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";
@@ -919,10 +921,15 @@ const safeProviders: SafeProvider[] = [
deps: [],
}),
safeProvider({
provide: NotificationsService,
provide: ActionsService,
useClass: UnsupportedActionsService,
deps: [],
}),
safeProvider({
provide: ServerNotificationsService,
useClass: devFlagEnabled("noopNotifications")
? NoopNotificationsService
: DefaultNotificationsService,
? NoopServerNotificationsService
: DefaultServerNotificationsService,
deps: [
LogService,
SyncService,
@@ -1513,7 +1520,7 @@ const safeProviders: SafeProvider[] = [
ApiServiceAbstraction,
OrganizationServiceAbstraction,
AuthServiceAbstraction,
NotificationsService,
ServerNotificationsService,
MessageListener,
],
}),
@@ -1523,7 +1530,7 @@ const safeProviders: SafeProvider[] = [
deps: [
StateProvider,
ApiServiceAbstraction,
NotificationsService,
ServerNotificationsService,
AuthServiceAbstraction,
LogService,
],

View File

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

View File

@@ -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/",

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

@@ -185,7 +185,7 @@ describe("VaultTimeoutService", () => {
),
);
platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false);
platformUtilsService.isPopupOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false);
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockImplementation((userId) => {
return new BehaviorSubject<VaultTimeoutAction>(accounts[userId]?.timeoutAction);
@@ -225,7 +225,7 @@ describe("VaultTimeoutService", () => {
it.each([AuthenticationStatus.Locked, AuthenticationStatus.LoggedOut])(
"should not try to log out or lock any user that has authStatus === %s.",
async (authStatus) => {
platformUtilsService.isViewOpen.mockResolvedValue(false);
platformUtilsService.isPopupOpen.mockResolvedValue(false);
setupAccounts({
1: {
authStatus: authStatus,

View File

@@ -84,7 +84,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
async checkVaultTimeout(): Promise<void> {
// Get whether or not the view is open a single time so it can be compared for each user
const isViewOpen = await this.platformUtilsService.isViewOpen();
const isViewOpen = await this.platformUtilsService.isPopupOpen();
await firstValueFrom(
combineLatest([

View File

@@ -22,7 +22,7 @@ export abstract class PlatformUtilsService {
abstract isVivaldi(): boolean;
abstract isSafari(): boolean;
abstract isMacAppStore(): boolean;
abstract isViewOpen(): Promise<boolean>;
abstract isPopupOpen(): Promise<boolean>;
abstract launchUri(uri: string, options?: any): void;
abstract getApplicationVersion(): Promise<string>;
abstract getApplicationVersionNumber(): Promise<string>;

View File

@@ -0,0 +1,40 @@
# Platform Actions API
## ActionsService.openPopup()
This document outlines the current behavior of `ActionsService.openPopup()` across different browsers, specifically in two contexts:
- **Window Context**: When the call is triggered from an active browser window (e.g., from a tab's script).
- **Background Service Worker Context**: When the call is made from a background context, such as a service worker.
The `openPopup()` method has limitations in some environments due to browser-specific restrictions or bugs. Below is a compatibility chart detailing the observed behavior.
---
## Compatibility Table
| Browser | Window Context | Background Service Worker Context |
| ------- | ------------------- | --------------------------------- |
| Safari | ✅ Works | ❌ Fails |
| Firefox | ❌ Fails | ❌ Fails |
| Chrome | ✅ Works | ✅ Works |
| Edge | 🟡 Untested | 🟡 Untested |
| Vivaldi | ⚠️ Ambiguous (Bug?) | ⚠️ Ambiguous (Bug?) |
| Opera | ✅ Works | ❌ Fails silently |
---
## Notes
- **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, 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.
---
## Summary
When implementing `ActionsService.openPopup()`, prefer triggering it from a window context whenever possible to maximize cross-browser compatibility. Full background service worker support is only reliable in **Chrome**.

View File

@@ -0,0 +1,6 @@
export abstract class ActionsService {
/**
* Opens the popup if it is supported.
*/
abstract openPopup(): Promise<void>;
}

View File

@@ -0,0 +1 @@
export { ActionsService } from "./actions-service";

View File

@@ -0,0 +1,7 @@
import { ActionsService } from "./actions-service";
export class UnsupportedActionsService implements ActionsService {
openPopup(): Promise<void> {
throw new Error("Open Popup unsupported.");
}
}

View File

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

View File

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

View File

@@ -21,9 +21,9 @@ import { SupportStatus } from "../../misc/support-status";
import { SyncService } from "../../sync";
import {
DefaultNotificationsService,
DefaultServerNotificationsService,
DISABLED_NOTIFICATIONS_URL,
} from "./default-notifications.service";
} from "./default-server-notifications.service";
import { SignalRConnectionService, SignalRNotification } from "./signalr-connection.service";
import { WebPushConnectionService, WebPushConnector } from "./webpush-connection.service";
import { WorkerWebPushConnectionService } from "./worker-webpush-connection.service";
@@ -52,7 +52,7 @@ describe("NotificationsService", () => {
notificationsUrl: string,
) => Subject<SignalRNotification>;
let sut: DefaultNotificationsService;
let sut: DefaultServerNotificationsService;
beforeEach(() => {
syncService = mock<SyncService>();
@@ -93,7 +93,7 @@ describe("NotificationsService", () => {
() => new Subject<SignalRNotification>(),
);
sut = new DefaultNotificationsService(
sut = new DefaultServerNotificationsService(
mock<LogService>(),
syncService,
appIdService,
@@ -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

@@ -32,14 +32,14 @@ import { EnvironmentService } from "../../abstractions/environment.service";
import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { supportSwitch } from "../../misc/support-status";
import { NotificationsService as NotificationsServiceAbstraction } from "../notifications.service";
import { ServerNotificationsService } from "../server-notifications.service";
import { ReceiveMessage, SignalRConnectionService } from "./signalr-connection.service";
import { WebPushConnectionService } from "./webpush-connection.service";
export const DISABLED_NOTIFICATIONS_URL = "http://-";
export class DefaultNotificationsService implements NotificationsServiceAbstraction {
export class DefaultServerNotificationsService implements ServerNotificationsService {
notifications$: Observable<readonly [NotificationResponse, UserId]>;
private activitySubject = new BehaviorSubject<"active" | "inactive">("active");
@@ -61,7 +61,7 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract
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 DefaultNotificationsService implements NotificationsServiceAbstract
}
/**
* 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 DefaultNotificationsService implements NotificationsServiceAbstract
}),
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 DefaultNotificationsService implements NotificationsServiceAbstract
);
},
notSupported: () => {
this.logService.info("Using SignalR for notifications");
this.logService.info("Using SignalR for server notifications");
return this.connectSignalR$(userId, notificationsUrl);
},
}),
@@ -188,7 +188,6 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract
case NotificationType.SyncCiphers:
case NotificationType.SyncSettings:
await this.syncService.fullSync(false);
break;
case NotificationType.SyncOrganizations:
// An organization update may not have bumped the user's account revision date, so force a sync
@@ -214,11 +213,11 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract
await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification);
break;
case NotificationType.AuthRequest:
{
this.messagingService.send("openLoginApproval", {
notificationId: notification.payload.id,
});
}
// create notification
this.messagingService.send("openLoginApproval", {
notificationId: notification.payload.id,
});
break;
case NotificationType.SyncOrganizationStatusChanged:
await this.syncService.fullSync(true);
@@ -237,7 +236,8 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract
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-notifications.service";
export * from "./noop-notifications.service";
export * from "./default-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

@@ -4,16 +4,16 @@ import { NotificationResponse } from "@bitwarden/common/models/response/notifica
import { UserId } from "@bitwarden/common/types/guid";
import { LogService } from "../../abstractions/log.service";
import { NotificationsService } from "../notifications.service";
import { ServerNotificationsService } from "../server-notifications.service";
export class NoopNotificationsService implements NotificationsService {
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

@@ -4,20 +4,20 @@ import { NotificationResponse } from "@bitwarden/common/models/response/notifica
import { UserId } from "@bitwarden/common/types/guid";
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Needed to link to API
import type { DefaultNotificationsService } from "./internal";
import type { DefaultServerNotificationsService } from "./internal";
/**
* A service offering abilities to interact with push notifications from the server.
*/
export abstract class NotificationsService {
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 DefaultNotificationsService.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

@@ -0,0 +1,58 @@
import { Observable } from "rxjs";
// This is currently tailored for chrome extension's api, if safari works
// differently where clicking a notification button produces a different
// identifier we need to reconcile that here.
export const ButtonLocation = Object.freeze({
FirstOptionalButton: 0, // this is the first optional button we can set
SecondOptionalButton: 1, // this is the second optional button we can set
NotificationButton: 2, // this is when you click the notification as a whole
});
export type ButtonLocationKeys = (typeof ButtonLocation)[keyof typeof ButtonLocation];
export type SystemNotificationsButton = {
title: string;
};
export type SystemNotificationCreateInfo = {
id?: string;
title: string;
body: string;
buttons: SystemNotificationsButton[];
};
export type SystemNotificationClearInfo = {
id: string;
};
export type SystemNotificationEvent = {
id: string;
buttonIdentifier: number;
};
/**
* A service responsible for displaying operating system level server notifications.
*/
export abstract class SystemNotificationsService {
abstract notificationClicked$: Observable<SystemNotificationEvent>;
/**
* Creates a notification.
* @param createInfo
* @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>;
/**
* Clears a notification.
* @param clearInfo Any info needed required to clear a notification.
*/
abstract clear(clearInfo: SystemNotificationClearInfo): Promise<void>;
/**
* Used to know if a given platform supports server notifications.
*/
abstract isSupported(): boolean;
}

View File

@@ -0,0 +1,23 @@
import { throwError } from "rxjs";
import {
SystemNotificationClearInfo,
SystemNotificationCreateInfo,
SystemNotificationsService,
} from "./system-notifications.service";
export class UnsupportedSystemNotificationsService implements SystemNotificationsService {
notificationClicked$ = throwError(() => new Error("Notification clicked is not supported."));
async create(createInfo: SystemNotificationCreateInfo): Promise<string> {
throw new Error("Create OS Notification unsupported.");
}
clear(clearInfo: SystemNotificationClearInfo): Promise<undefined> {
throw new Error("Clear OS Notification unsupported.");
}
isSupported(): boolean {
return false;
}
}

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 { NotificationsService } 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";
@@ -20,7 +20,7 @@ import {
describe("End User Notification Center Service", () => {
let fakeStateProvider: FakeStateProvider;
let mockApiService: jest.Mocked<ApiService>;
let mockNotificationsService: jest.Mocked<NotificationsService>;
let mockNotificationsService: jest.Mocked<ServerNotificationsService>;
let mockAuthService: jest.Mocked<AuthService>;
let mockLogService: jest.Mocked<LogService>;
let service: DefaultEndUserNotificationService;
@@ -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 { NotificationsService } 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 {
@@ -36,7 +36,7 @@ export class DefaultEndUserNotificationService implements EndUserNotificationSer
constructor(
private stateProvider: StateProvider,
private apiService: ApiService,
private notificationService: NotificationsService,
private notificationService: ServerNotificationsService,
private authService: AuthService,
private logService: LogService,
) {}

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 { NotificationsService } 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";
@@ -39,7 +39,9 @@ describe("Default task service", () => {
{ send: mockApiSend } as unknown as ApiService,
{ organizations$: mockGetAllOrgs$ } as unknown as OrganizationService,
{ authStatuses$: mockAuthStatuses$.asObservable() } as unknown as AuthService,
{ notifications$: mockNotifications$.asObservable() } as unknown as NotificationsService,
{
notifications$: mockNotifications$.asObservable(),
} as unknown as ServerNotificationsService,
{ allMessages$: mockMessages$.asObservable() } as unknown as MessageListener,
);
});

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 { NotificationsService } 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 {
@@ -42,7 +42,7 @@ export class DefaultTaskService implements TaskService {
private apiService: ApiService,
private organizationService: OrganizationService,
private authService: AuthService,
private notificationService: NotificationsService,
private notificationService: ServerNotificationsService,
private messageListener: MessageListener,
) {}
@@ -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$

View File

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