mirror of
https://github.com/bitwarden/browser
synced 2026-02-05 11:13:44 +00:00
feat(inactive-user-server-notification): [PM-25130] Inactive User Server Notify - Added feature flag.
This commit is contained in:
@@ -1139,6 +1139,7 @@ export default class MainBackground {
|
||||
new SignalRConnectionService(this.apiService, this.logService),
|
||||
this.authService,
|
||||
this.webPushConnectionService,
|
||||
this.configService,
|
||||
);
|
||||
|
||||
this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService);
|
||||
|
||||
@@ -53,6 +53,7 @@ export enum FeatureFlag {
|
||||
|
||||
/* Platform */
|
||||
IpcChannelFramework = "ipc-channel-framework",
|
||||
InactiveUserServerNotification = "pm-25130-receive-push-notifications-for-inactive-users",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
@@ -112,6 +113,7 @@ export const DefaultFeatureFlagValue = {
|
||||
|
||||
/* Platform */
|
||||
[FeatureFlag.IpcChannelFramework]: FALSE,
|
||||
[FeatureFlag.InactiveUserServerNotification]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BehaviorSubject, bufferCount, firstValueFrom, Subject, ObservedValueOf
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
// TODO: When PM-14943 goes in, uncomment
|
||||
// import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||
|
||||
@@ -34,7 +35,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
||||
let webPushNotificationConnectionService: MockProxy<WebPushConnectionService>;
|
||||
// TODO: When PM-14943 goes in, uncomment
|
||||
// let authRequestAnsweringService: MockProxy<AuthRequestAnsweringServiceAbstraction>;
|
||||
let configurationService: MockProxy<ConfigService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
let activeUserAccount$: BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>;
|
||||
let userAccounts$: BehaviorSubject<ObservedValueOf<AccountService["accounts$"]>>;
|
||||
@@ -89,7 +90,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
||||
|
||||
signalRNotificationConnectionService = mock<SignalRConnectionService>();
|
||||
connectionSubjectByUser = new Map();
|
||||
(signalRNotificationConnectionService.connect$ as unknown as jest.Mock).mockImplementation(
|
||||
signalRNotificationConnectionService.connect$.mockImplementation(
|
||||
(userId: UserId, _url: string) => {
|
||||
if (!connectionSubjectByUser.has(userId)) {
|
||||
connectionSubjectByUser.set(userId, new Subject<any>());
|
||||
@@ -100,7 +101,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
||||
|
||||
authService = mock<AuthService>();
|
||||
authenticationStatusByUser = new Map();
|
||||
(authService.authStatusFor$ as unknown as jest.Mock).mockImplementation((userId: UserId) => {
|
||||
authService.authStatusFor$.mockImplementation((userId: UserId) => {
|
||||
if (!authenticationStatusByUser.has(userId)) {
|
||||
authenticationStatusByUser.set(
|
||||
userId,
|
||||
@@ -112,9 +113,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
||||
|
||||
webPushNotificationConnectionService = mock<WebPushConnectionService>();
|
||||
webPushSupportStatusByUser = new Map();
|
||||
(
|
||||
webPushNotificationConnectionService.supportStatus$ as unknown as jest.Mock
|
||||
).mockImplementation((userId: UserId) => {
|
||||
webPushNotificationConnectionService.supportStatus$.mockImplementation((userId: UserId) => {
|
||||
if (!webPushSupportStatusByUser.has(userId)) {
|
||||
webPushSupportStatusByUser.set(
|
||||
userId,
|
||||
@@ -127,8 +126,13 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
||||
// TODO: When PM-14943 goes in, uncomment
|
||||
// authRequestAnsweringService = mock<AuthRequestAnsweringServiceAbstraction>();
|
||||
|
||||
configurationService = mock<ConfigService>();
|
||||
configurationService.getFeatureFlag$.mockReturnValue(new BehaviorSubject(true) as any);
|
||||
configService = mock<ConfigService>();
|
||||
configService.getFeatureFlag$.mockImplementation((flag: FeatureFlag) => {
|
||||
const flagValueByFlag: Partial<Record<FeatureFlag, boolean>> = {
|
||||
[FeatureFlag.InactiveUserServerNotification]: true,
|
||||
};
|
||||
return new BehaviorSubject(flagValueByFlag[flag] ?? false) as any;
|
||||
});
|
||||
|
||||
defaultServerNotificationsService = new DefaultServerNotificationsService(
|
||||
mock<LogService>(),
|
||||
@@ -142,7 +146,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
||||
authService,
|
||||
webPushNotificationConnectionService,
|
||||
// authRequestAnsweringService,
|
||||
// configurationService,
|
||||
configService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -254,15 +258,13 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
||||
const subscription = defaultServerNotificationsService.startListening();
|
||||
|
||||
// Emit via SignalR connect$ for user2
|
||||
connectionSubjectByUser
|
||||
.get(mockUserId2)!
|
||||
.next({
|
||||
type: "ReceiveMessage",
|
||||
message: new NotificationResponse({
|
||||
type: NotificationType.AuthRequest,
|
||||
payload: { id: "auth-id-2", userId: mockUserId2 },
|
||||
}),
|
||||
});
|
||||
connectionSubjectByUser.get(mockUserId2)!.next({
|
||||
type: "ReceiveMessage",
|
||||
message: new NotificationResponse({
|
||||
type: NotificationType.AuthRequest,
|
||||
payload: { id: "auth-id-2", userId: mockUserId2 },
|
||||
}),
|
||||
});
|
||||
|
||||
// allow async queue to drain
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BehaviorSubject, bufferCount, firstValueFrom, ObservedValueOf, Subject
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
import { awaitAsync } from "../../../../spec";
|
||||
import { Matrix } from "../../../../spec/matrix";
|
||||
@@ -14,6 +15,7 @@ import { NotificationType } from "../../../enums";
|
||||
import { NotificationResponse } from "../../../models/response/notification.response";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { AppIdService } from "../../abstractions/app-id.service";
|
||||
import { ConfigService } from "../../abstractions/config/config.service";
|
||||
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
|
||||
import { LogService } from "../../abstractions/log.service";
|
||||
import { MessageSender } from "../../messaging";
|
||||
@@ -38,6 +40,7 @@ describe("NotificationsService", () => {
|
||||
let signalRNotificationConnectionService: MockProxy<SignalRConnectionService>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
let webPushNotificationConnectionService: MockProxy<WebPushConnectionService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
let activeAccount: BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>;
|
||||
let accounts: BehaviorSubject<ObservedValueOf<AccountService["accounts$"]>>;
|
||||
@@ -65,6 +68,14 @@ describe("NotificationsService", () => {
|
||||
signalRNotificationConnectionService = mock<SignalRConnectionService>();
|
||||
authService = mock<AuthService>();
|
||||
webPushNotificationConnectionService = mock<WorkerWebPushConnectionService>();
|
||||
configService = mock<ConfigService>();
|
||||
// For these tests, use the active-user implementation (feature flag disabled)
|
||||
configService.getFeatureFlag$.mockImplementation((flag: FeatureFlag) => {
|
||||
const flagValueByFlag: Partial<Record<FeatureFlag, boolean>> = {
|
||||
[FeatureFlag.InactiveUserServerNotification]: false,
|
||||
};
|
||||
return new BehaviorSubject(flagValueByFlag[flag] ?? false) as any;
|
||||
});
|
||||
|
||||
activeAccount = new BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>(null);
|
||||
accountService.activeAccount$ = activeAccount.asObservable();
|
||||
@@ -108,6 +119,7 @@ describe("NotificationsService", () => {
|
||||
signalRNotificationConnectionService,
|
||||
authService,
|
||||
webPushNotificationConnectionService,
|
||||
configService,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
@@ -57,22 +59,48 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
||||
private readonly signalRConnectionService: SignalRConnectionService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly webPushConnectionService: WebPushConnectionService,
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
this.notifications$ = this.accountService.accounts$.pipe(
|
||||
map((accounts) => Object.keys(accounts) as UserId[]),
|
||||
switchMap((userIds) => {
|
||||
if (userIds.length === 0) {
|
||||
return EMPTY;
|
||||
}
|
||||
this.notifications$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.InactiveUserServerNotification)
|
||||
.pipe(
|
||||
switchMap((inactiveUserServerNotificationEnabled) => {
|
||||
if (inactiveUserServerNotificationEnabled) {
|
||||
return this.accountService.accounts$.pipe(
|
||||
map((accounts) => Object.keys(accounts) as UserId[]),
|
||||
switchMap((userIds) => {
|
||||
if (userIds.length === 0) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const streams = userIds.map((id) =>
|
||||
this.userNotifications$(id).pipe(map((notification) => [notification, id] as const)),
|
||||
);
|
||||
const streams = userIds.map((id) =>
|
||||
this.userNotifications$(id).pipe(
|
||||
map((notification) => [notification, id] as const),
|
||||
),
|
||||
);
|
||||
|
||||
return merge(...streams);
|
||||
}),
|
||||
share(), // Multiple subscribers should only create a single connection to the server per subscriber
|
||||
);
|
||||
return merge(...streams);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
map((account) => account?.id),
|
||||
distinctUntilChanged(),
|
||||
switchMap((activeAccountId) => {
|
||||
if (activeAccountId == null) {
|
||||
// We don't emit server-notifications for inactive accounts currently
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return this.userNotifications$(activeAccountId).pipe(
|
||||
map((notification) => [notification, activeAccountId] as const),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share(), // Multiple subscribers should only create a single connection to the server
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user