mirror of
https://github.com/bitwarden/browser
synced 2026-02-09 13:10:17 +00:00
feat(extension-notification-demo): Demo using the notification api to display a clickable notification.
This commit is contained in:
@@ -43,6 +43,10 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||
import {
|
||||
DeviceManagementApprovalService,
|
||||
DevicesManagementApprovalAbstraction,
|
||||
} from "@bitwarden/common/auth/services/devices/device-management-approval.service";
|
||||
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
|
||||
@@ -110,6 +114,7 @@ import {
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { SystemNotificationServiceAbstraction } from "@bitwarden/common/platform/abstractions/system-notification-service.abstraction";
|
||||
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
@@ -130,6 +135,7 @@ import {
|
||||
WorkerWebPushConnectionService,
|
||||
} from "@bitwarden/common/platform/notifications/internal";
|
||||
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||
import { ChromeExtensionSystemNotificationService } from "@bitwarden/common/platform/services/chrome-extension-system-notification.service";
|
||||
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
|
||||
import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
@@ -331,6 +337,8 @@ export default class MainBackground {
|
||||
exportService: VaultExportServiceAbstraction;
|
||||
searchService: SearchServiceAbstraction;
|
||||
notificationsService: NotificationsService;
|
||||
deviceManagementApprovalService: DevicesManagementApprovalAbstraction;
|
||||
systemNotificationService: SystemNotificationServiceAbstraction;
|
||||
stateService: StateServiceAbstraction;
|
||||
userNotificationSettingsService: UserNotificationSettingsServiceAbstraction;
|
||||
autofillSettingsService: AutofillSettingsServiceAbstraction;
|
||||
@@ -1078,6 +1086,17 @@ export default class MainBackground {
|
||||
this.webPushConnectionService = new UnsupportedWebPushConnectionService();
|
||||
}
|
||||
|
||||
this.systemNotificationService = new ChromeExtensionSystemNotificationService(
|
||||
this.logService,
|
||||
this.platformUtilsService,
|
||||
);
|
||||
|
||||
this.deviceManagementApprovalService = new DeviceManagementApprovalService(
|
||||
this.platformUtilsService,
|
||||
this.logService,
|
||||
this.systemNotificationService,
|
||||
);
|
||||
|
||||
this.notificationsService = new DefaultNotificationsService(
|
||||
this.logService,
|
||||
this.syncService,
|
||||
@@ -1089,6 +1108,7 @@ export default class MainBackground {
|
||||
new SignalRConnectionService(this.apiService, this.logService),
|
||||
this.authService,
|
||||
this.webPushConnectionService,
|
||||
this.deviceManagementApprovalService,
|
||||
);
|
||||
|
||||
this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService);
|
||||
|
||||
@@ -56,7 +56,8 @@
|
||||
"unlimitedStorage",
|
||||
"webNavigation",
|
||||
"webRequest",
|
||||
"webRequestBlocking"
|
||||
"webRequestBlocking",
|
||||
"notifications"
|
||||
],
|
||||
"__safari__permissions": [
|
||||
"<all_urls>",
|
||||
|
||||
@@ -361,4 +361,8 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async openPopupToPath() {
|
||||
await chrome.action.openPopup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,4 +195,8 @@ export class WebPlatformUtilsService implements PlatformUtilsService {
|
||||
getAutofillKeyboardShortcut(): Promise<string> {
|
||||
return null;
|
||||
}
|
||||
|
||||
openPopupToPath(url: string): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,10 @@ import { AccountServiceImplementation } from "@bitwarden/common/auth/services/ac
|
||||
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||
import {
|
||||
DeviceManagementApprovalService,
|
||||
DevicesManagementApprovalAbstraction,
|
||||
} from "@bitwarden/common/auth/services/devices/device-management-approval.service";
|
||||
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
|
||||
@@ -182,6 +186,7 @@ import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sd
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { SystemNotificationServiceAbstraction } from "@bitwarden/common/platform/abstractions/system-notification-service.abstraction";
|
||||
import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
@@ -205,6 +210,7 @@ import {
|
||||
TaskSchedulerService,
|
||||
} from "@bitwarden/common/platform/scheduling";
|
||||
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||
import { ChromeExtensionSystemNotificationService } from "@bitwarden/common/platform/services/chrome-extension-system-notification.service";
|
||||
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
|
||||
import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
@@ -892,6 +898,16 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: UnsupportedWebPushConnectionService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SystemNotificationServiceAbstraction,
|
||||
useClass: ChromeExtensionSystemNotificationService,
|
||||
deps: [LogService, PlatformUtilsServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DevicesManagementApprovalAbstraction,
|
||||
useClass: DeviceManagementApprovalService,
|
||||
deps: [PlatformUtilsServiceAbstraction, LogService, SystemNotificationServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: NotificationsService,
|
||||
useClass: devFlagEnabled("noopNotifications")
|
||||
@@ -908,6 +924,7 @@ const safeProviders: SafeProvider[] = [
|
||||
SignalRConnectionService,
|
||||
AuthServiceAbstraction,
|
||||
WebPushConnectionService,
|
||||
DevicesManagementApprovalAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { filter, mergeMap } from "rxjs/operators";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
ButtonActions,
|
||||
SystemNotificationServiceAbstraction,
|
||||
SystemNotificationEvent,
|
||||
ButtonLocation,
|
||||
} from "@bitwarden/common/platform/abstractions/system-notification-service.abstraction";
|
||||
|
||||
export abstract class DevicesManagementApprovalAbstraction {
|
||||
abstract receivedPendingAuthRequest(notificationId: string): Promise<void>;
|
||||
abstract checkForPendingAuthRequestsToApprove(notificationId: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class DeviceManagementApprovalService implements DevicesManagementApprovalAbstraction {
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private systemNotificationService: SystemNotificationServiceAbstraction,
|
||||
) {
|
||||
this.systemNotificationService.systemNotificationClicked$
|
||||
.pipe(
|
||||
filter(
|
||||
(event: SystemNotificationEvent) => event.type === ButtonActions.AuthRequestNotification,
|
||||
),
|
||||
mergeMap((event: SystemNotificationEvent) =>
|
||||
this.handleAuthRequestNotificationClicked(event),
|
||||
),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async receivedPendingAuthRequest(notificationId: string): Promise<void> {
|
||||
// if popup is open, open dialog if logged in
|
||||
if (await this.platformUtilsService.isViewOpen()) {
|
||||
this.logService.info("Open dialog to user to approve request.");
|
||||
} else {
|
||||
// if not open, create a notification
|
||||
|
||||
// Short circuit because we don't support notifications on this client
|
||||
if (!this.systemNotificationService.isSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.systemNotificationService.createOSNotification({
|
||||
title: "Pending Device Request",
|
||||
body: "Please view pending auth request",
|
||||
type: ButtonActions.AuthRequestNotification,
|
||||
id: notificationId,
|
||||
buttons: [{ title: "Approve Request" }, { title: "Deny Request" }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will be used probably in a guard to route the user
|
||||
* This is a function because it could cache auth requests coming
|
||||
* in the background, or it could query the server for any outstanding
|
||||
* requests.
|
||||
*/
|
||||
async checkForPendingAuthRequestsToApprove(): Promise<void> {
|
||||
// STUB
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Requests are doubling, figure out if a subscription is duplicating or something.
|
||||
* @param event
|
||||
* @private
|
||||
*/
|
||||
private async handleAuthRequestNotificationClicked(
|
||||
event: SystemNotificationEvent,
|
||||
): Promise<void> {
|
||||
// This is the approval event. WE WILL NOT BE USING 0 or 1!
|
||||
if (event.buttonIdentifier === ButtonLocation.FirstOptionalButton) {
|
||||
this.logService.info("Approve the request");
|
||||
} else if (event.buttonIdentifier === ButtonLocation.SecondOptionalButton) {
|
||||
// This is the deny event.
|
||||
this.logService.info("Deny the request");
|
||||
} else if (event.buttonIdentifier === ButtonLocation.NotificationButton) {
|
||||
this.logService.info("Main button clicked, open popup");
|
||||
// This is unstable, to be figured out later.
|
||||
await this.platformUtilsService.openPopupToPath("/device-management");
|
||||
this.systemNotificationService.clearOSNotification({ id: event.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@ export abstract class PlatformUtilsService {
|
||||
abstract isVivaldi(): boolean;
|
||||
abstract isSafari(): boolean;
|
||||
abstract isMacAppStore(): boolean;
|
||||
|
||||
/**
|
||||
* Can only be called from the background service.
|
||||
*/
|
||||
abstract isViewOpen(): Promise<boolean>;
|
||||
abstract launchUri(uri: string, options?: any): void;
|
||||
abstract getApplicationVersion(): Promise<string>;
|
||||
@@ -45,4 +49,5 @@ export abstract class PlatformUtilsService {
|
||||
abstract readFromClipboard(): Promise<string>;
|
||||
abstract supportsSecureStorage(): boolean;
|
||||
abstract getAutofillKeyboardShortcut(): Promise<string>;
|
||||
abstract openPopupToPath(url: string): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export const ButtonActions = {
|
||||
AuthRequestNotification: "authRequestNotification",
|
||||
};
|
||||
|
||||
export type ButtonActionsKeys = (typeof ButtonActions)[keyof typeof ButtonActions];
|
||||
|
||||
// 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 = {
|
||||
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;
|
||||
type: ButtonActionsKeys;
|
||||
title: string;
|
||||
body: string;
|
||||
buttons: SystemNotificationsButton[];
|
||||
};
|
||||
|
||||
export type SystemNotificationClearInfo = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type SystemNotificationEvent = {
|
||||
id: string;
|
||||
type: string;
|
||||
buttonIdentifier: number;
|
||||
};
|
||||
|
||||
export abstract class SystemNotificationServiceAbstraction {
|
||||
abstract systemNotificationClicked$: Observable<SystemNotificationEvent>;
|
||||
abstract createOSNotification(createInfo: SystemNotificationCreateInfo): Promise<undefined>;
|
||||
abstract clearOSNotification(clearInfo: SystemNotificationClearInfo): undefined;
|
||||
|
||||
/**
|
||||
* Used to know if a given platform supports notifications.
|
||||
*/
|
||||
abstract isSupported(): boolean;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "rxjs";
|
||||
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { DevicesManagementApprovalAbstraction } from "@bitwarden/common/auth/services/devices/device-management-approval.service";
|
||||
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
@@ -41,6 +42,7 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract
|
||||
notifications$: Observable<readonly [NotificationResponse, UserId]>;
|
||||
|
||||
private activitySubject = new BehaviorSubject<"active" | "inactive">("active");
|
||||
private destroy$ = new BehaviorSubject<void>(undefined);
|
||||
|
||||
constructor(
|
||||
private readonly logService: LogService,
|
||||
@@ -53,6 +55,7 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract
|
||||
private readonly signalRConnectionService: SignalRConnectionService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly webPushConnectionService: WebPushConnectionService,
|
||||
private deviceManagementApprovalService: DevicesManagementApprovalAbstraction,
|
||||
) {
|
||||
this.notifications$ = this.accountService.activeAccount$.pipe(
|
||||
map((account) => account?.id),
|
||||
@@ -211,6 +214,9 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract
|
||||
this.messagingService.send("openLoginApproval", {
|
||||
notificationId: notification.payload.id,
|
||||
});
|
||||
await this.deviceManagementApprovalService.receivedPendingAuthRequest(
|
||||
notification.payload.id,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case NotificationType.SyncOrganizationStatusChanged:
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Observable, Subject } 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,
|
||||
SystemNotificationServiceAbstraction as SystemNotificationServiceAbstraction,
|
||||
} from "@bitwarden/common/platform/abstractions/system-notification-service.abstraction";
|
||||
|
||||
export class ChromeExtensionSystemNotificationService
|
||||
implements SystemNotificationServiceAbstraction
|
||||
{
|
||||
private systemNotificationClickedSubject = new Subject<SystemNotificationEvent>();
|
||||
systemNotificationClicked$: Observable<SystemNotificationEvent>;
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {
|
||||
this.systemNotificationClicked$ = this.systemNotificationClickedSubject.asObservable();
|
||||
}
|
||||
|
||||
async createOSNotification(createInfo: SystemNotificationCreateInfo): Promise<undefined> {
|
||||
chrome.notifications.create(createInfo.id, {
|
||||
iconUrl: "https://avatars.githubusercontent.com/u/15990069?s=200",
|
||||
message: createInfo.title,
|
||||
type: "basic",
|
||||
title: createInfo.title,
|
||||
buttons: createInfo.buttons.map((value) => {
|
||||
return { title: value.title };
|
||||
}),
|
||||
});
|
||||
|
||||
// ESLint: Using addListener in the browser popup produces a memory leak in Safari, use `BrowserApi. addListener` instead (no-restricted-syntax)
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chrome.notifications.onButtonClicked.addListener(
|
||||
(notificationId: string, buttonIndex: number) => {
|
||||
this.systemNotificationClickedSubject.next({
|
||||
id: notificationId,
|
||||
type: createInfo.type,
|
||||
buttonIdentifier: buttonIndex,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chrome.notifications.onClicked.addListener((notificationId: string) => {
|
||||
this.systemNotificationClickedSubject.next({
|
||||
id: notificationId,
|
||||
type: createInfo.type,
|
||||
buttonIdentifier: ButtonLocation.NotificationButton,
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
clearOSNotification(clearInfo: SystemNotificationClearInfo): undefined {
|
||||
chrome.notifications.clear(clearInfo.id);
|
||||
}
|
||||
|
||||
isSupported(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user