1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-03-11 13:30:39 +01:00

[PM-5559] Implement User Notification Settings state provider (#8032)

* create user notification settings state provider

* replace state service get/set disableAddLoginNotification and disableChangedPasswordNotification with user notification settings service equivalents

* migrate disableAddLoginNotification and disableChangedPasswordNotification global settings to user notification settings state provider

* add content script messaging the background for enableChangedPasswordPrompt setting

* Implementing feedback to provide on PR

* Implementing feedback to provide on PR

* PR suggestions cleanup

---------

Co-authored-by: Cesar Gonzalez <cgonzalez@bitwarden.com>
This commit is contained in:
Jonathan Prusik 2024-03-04 14:12:23 -05:00 committed by GitHub
parent d87a8f9271
commit 4ba2717eb4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 409 additions and 138 deletions

View File

@ -109,6 +109,8 @@ type NotificationBackgroundExtensionMessageHandlers = {
bgReopenUnlockPopout: ({ sender }: BackgroundSenderParam) => Promise<void>; bgReopenUnlockPopout: ({ sender }: BackgroundSenderParam) => Promise<void>;
checkNotificationQueue: ({ sender }: BackgroundSenderParam) => Promise<void>; checkNotificationQueue: ({ sender }: BackgroundSenderParam) => Promise<void>;
collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise<void>; collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise<void>;
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
getWebVaultUrlForNotification: () => string; getWebVaultUrlForNotification: () => string;
}; };

View File

@ -4,6 +4,7 @@ import { firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -45,6 +46,7 @@ describe("NotificationBackground", () => {
const policyService = mock<PolicyService>(); const policyService = mock<PolicyService>();
const folderService = mock<FolderService>(); const folderService = mock<FolderService>();
const stateService = mock<BrowserStateService>(); const stateService = mock<BrowserStateService>();
const userNotificationSettingsService = mock<UserNotificationSettingsService>();
const environmentService = mock<EnvironmentService>(); const environmentService = mock<EnvironmentService>();
const logService = mock<LogService>(); const logService = mock<LogService>();
@ -56,6 +58,7 @@ describe("NotificationBackground", () => {
policyService, policyService,
folderService, folderService,
stateService, stateService,
userNotificationSettingsService,
environmentService, environmentService,
logService, logService,
); );
@ -235,8 +238,8 @@ describe("NotificationBackground", () => {
let tab: chrome.tabs.Tab; let tab: chrome.tabs.Tab;
let sender: chrome.runtime.MessageSender; let sender: chrome.runtime.MessageSender;
let getAuthStatusSpy: jest.SpyInstance; let getAuthStatusSpy: jest.SpyInstance;
let getDisableAddLoginNotificationSpy: jest.SpyInstance; let getEnableAddedLoginPromptSpy: jest.SpyInstance;
let getDisableChangedPasswordNotificationSpy: jest.SpyInstance; let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
let pushAddLoginToQueueSpy: jest.SpyInstance; let pushAddLoginToQueueSpy: jest.SpyInstance;
let pushChangePasswordToQueueSpy: jest.SpyInstance; let pushChangePasswordToQueueSpy: jest.SpyInstance;
let getAllDecryptedForUrlSpy: jest.SpyInstance; let getAllDecryptedForUrlSpy: jest.SpyInstance;
@ -245,13 +248,13 @@ describe("NotificationBackground", () => {
tab = createChromeTabMock(); tab = createChromeTabMock();
sender = mock<chrome.runtime.MessageSender>({ tab }); sender = mock<chrome.runtime.MessageSender>({ tab });
getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus");
getDisableAddLoginNotificationSpy = jest.spyOn( getEnableAddedLoginPromptSpy = jest.spyOn(
stateService, notificationBackground as any,
"getDisableAddLoginNotification", "getEnableAddedLoginPrompt",
); );
getDisableChangedPasswordNotificationSpy = jest.spyOn( getEnableChangedPasswordPromptSpy = jest.spyOn(
stateService, notificationBackground as any,
"getDisableChangedPasswordNotification", "getEnableChangedPasswordPrompt",
); );
pushAddLoginToQueueSpy = jest.spyOn(notificationBackground as any, "pushAddLoginToQueue"); pushAddLoginToQueueSpy = jest.spyOn(notificationBackground as any, "pushAddLoginToQueue");
pushChangePasswordToQueueSpy = jest.spyOn( pushChangePasswordToQueueSpy = jest.spyOn(
@ -272,7 +275,7 @@ describe("NotificationBackground", () => {
await flushPromises(); await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getDisableAddLoginNotificationSpy).not.toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
}); });
@ -287,7 +290,7 @@ describe("NotificationBackground", () => {
await flushPromises(); await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getDisableAddLoginNotificationSpy).not.toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
}); });
@ -297,13 +300,13 @@ describe("NotificationBackground", () => {
login: { username: "test", password: "password", url: "https://example.com" }, login: { username: "test", password: "password", url: "https://example.com" },
}; };
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(true); getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
sendExtensionRuntimeMessage(message, sender); sendExtensionRuntimeMessage(message, sender);
await flushPromises(); await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getDisableAddLoginNotificationSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).not.toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).not.toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
@ -315,14 +318,14 @@ describe("NotificationBackground", () => {
login: { username: "test", password: "password", url: "https://example.com" }, login: { username: "test", password: "password", url: "https://example.com" },
}; };
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(true); getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([]); getAllDecryptedForUrlSpy.mockResolvedValueOnce([]);
sendExtensionRuntimeMessage(message, sender); sendExtensionRuntimeMessage(message, sender);
await flushPromises(); await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getDisableAddLoginNotificationSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
@ -334,8 +337,8 @@ describe("NotificationBackground", () => {
login: { username: "test", password: "password", url: "https://example.com" }, login: { username: "test", password: "password", url: "https://example.com" },
}; };
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
getDisableChangedPasswordNotificationSpy.mockReturnValueOnce(true); getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([ getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "oldPassword" } }), mock<CipherView>({ login: { username: "test", password: "oldPassword" } }),
]); ]);
@ -344,9 +347,9 @@ describe("NotificationBackground", () => {
await flushPromises(); await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getDisableAddLoginNotificationSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(getDisableChangedPasswordNotificationSpy).toHaveBeenCalled(); expect(getEnableChangedPasswordPromptSpy).toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
}); });
@ -357,7 +360,7 @@ describe("NotificationBackground", () => {
login: { username: "test", password: "password", url: "https://example.com" }, login: { username: "test", password: "password", url: "https://example.com" },
}; };
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([ getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "password" } }), mock<CipherView>({ login: { username: "test", password: "password" } }),
]); ]);
@ -366,7 +369,7 @@ describe("NotificationBackground", () => {
await flushPromises(); await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getDisableAddLoginNotificationSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
@ -376,7 +379,7 @@ describe("NotificationBackground", () => {
const login = { username: "test", password: "password", url: "https://example.com" }; const login = { username: "test", password: "password", url: "https://example.com" };
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login }; const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
sendExtensionRuntimeMessage(message, sender); sendExtensionRuntimeMessage(message, sender);
await flushPromises(); await flushPromises();
@ -393,7 +396,7 @@ describe("NotificationBackground", () => {
} as any; } as any;
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login }; const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([ getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "anotherTestUsername", password: "password" } }), mock<CipherView>({ login: { username: "anotherTestUsername", password: "password" } }),
]); ]);
@ -409,7 +412,8 @@ describe("NotificationBackground", () => {
const login = { username: "tEsT", password: "password", url: "https://example.com" }; const login = { username: "tEsT", password: "password", url: "https://example.com" };
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login }; const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getDisableAddLoginNotificationSpy.mockReturnValueOnce(false); getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true);
getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([ getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ mock<CipherView>({
id: "cipher-id", id: "cipher-id",

View File

@ -4,6 +4,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
@ -57,6 +58,8 @@ export default class NotificationBackground {
bgUnlockPopoutOpened: ({ message, sender }) => this.unlockVault(message, sender.tab), bgUnlockPopoutOpened: ({ message, sender }) => this.unlockVault(message, sender.tab),
checkNotificationQueue: ({ sender }) => this.checkNotificationQueue(sender.tab), checkNotificationQueue: ({ sender }) => this.checkNotificationQueue(sender.tab),
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab), bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(),
bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(),
getWebVaultUrlForNotification: () => this.getWebVaultUrl(), getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
}; };
@ -67,6 +70,7 @@ export default class NotificationBackground {
private policyService: PolicyService, private policyService: PolicyService,
private folderService: FolderService, private folderService: FolderService,
private stateService: BrowserStateService, private stateService: BrowserStateService,
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private logService: LogService, private logService: LogService,
) {} ) {}
@ -81,6 +85,20 @@ export default class NotificationBackground {
this.cleanupNotificationQueue(); this.cleanupNotificationQueue();
} }
/**
* Gets the enableChangedPasswordPrompt setting from the user notification settings service.
*/
async getEnableChangedPasswordPrompt(): Promise<boolean> {
return await firstValueFrom(this.userNotificationSettingsService.enableChangedPasswordPrompt$);
}
/**
* Gets the enableAddedLoginPrompt setting from the user notification settings service.
*/
async getEnableAddedLoginPrompt(): Promise<boolean> {
return await firstValueFrom(this.userNotificationSettingsService.enableAddedLoginPrompt$);
}
/** /**
* Checks the notification queue for any messages that need to be sent to the * Checks the notification queue for any messages that need to be sent to the
* specified tab. If no tab is specified, the current tab will be used. * specified tab. If no tab is specified, the current tab will be used.
@ -194,9 +212,10 @@ export default class NotificationBackground {
return; return;
} }
const disabledAddLogin = await this.stateService.getDisableAddLoginNotification(); const addLoginIsEnabled = await this.getEnableAddedLoginPrompt();
if (authStatus === AuthenticationStatus.Locked) { if (authStatus === AuthenticationStatus.Locked) {
if (!disabledAddLogin) { if (addLoginIsEnabled) {
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab, true); await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab, true);
} }
@ -207,14 +226,15 @@ export default class NotificationBackground {
const usernameMatches = ciphers.filter( const usernameMatches = ciphers.filter(
(c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername, (c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername,
); );
if (!disabledAddLogin && usernameMatches.length === 0) { if (addLoginIsEnabled && usernameMatches.length === 0) {
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab); await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab);
return; return;
} }
const disabledChangePassword = await this.stateService.getDisableChangedPasswordNotification(); const changePasswordIsEnabled = await this.getEnableChangedPasswordPrompt();
if ( if (
!disabledChangePassword && changePasswordIsEnabled &&
usernameMatches.length === 1 && usernameMatches.length === 1 &&
usernameMatches[0].login.password !== loginInfo.password usernameMatches[0].login.password !== loginInfo.password
) { ) {

View File

@ -0,0 +1,25 @@
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../../platform/background/service-factories/factory-options";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory";
export type UserNotificationSettingsServiceInitOptions = FactoryOptions & StateProviderInitOptions;
export function userNotificationSettingsServiceFactory(
cache: { userNotificationSettingsService?: UserNotificationSettingsService } & CachedServices,
opts: UserNotificationSettingsServiceInitOptions,
): Promise<UserNotificationSettingsService> {
return factory(
cache,
"userNotificationSettingsService",
opts,
async () => new UserNotificationSettingsService(await stateProviderFactory(cache, opts)),
);
}

View File

@ -7,7 +7,11 @@ import { WatchedForm } from "../models/watched-form";
import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar"; import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar";
import { FormData } from "../services/abstractions/autofill.service"; import { FormData } from "../services/abstractions/autofill.service";
import { GlobalSettings, UserSettings } from "../types"; import { GlobalSettings, UserSettings } from "../types";
import { getFromLocalStorage, setupExtensionDisconnectAction } from "../utils"; import {
getFromLocalStorage,
sendExtensionMessage,
setupExtensionDisconnectAction,
} from "../utils";
interface HTMLElementWithFormOpId extends HTMLElement { interface HTMLElementWithFormOpId extends HTMLElement {
formOpId: string; formOpId: string;
@ -86,12 +90,11 @@ async function loadNotificationBar() {
]); ]);
const changePasswordButtonContainsNames = new Set(["pass", "change", "contras", "senha"]); const changePasswordButtonContainsNames = new Set(["pass", "change", "contras", "senha"]);
// These are preferences for whether to show the notification bar based on the user's settings const enableChangedPasswordPrompt = await sendExtensionMessage(
// and they are set in the Settings > Options page in the browser extension. "bgGetEnableChangedPasswordPrompt",
let disabledAddLoginNotification = false; );
let disabledChangedPasswordNotification = false; const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt");
let showNotificationBar = true; let showNotificationBar = true;
// Look up the active user id from storage // Look up the active user id from storage
const activeUserIdKey = "activeUserId"; const activeUserIdKey = "activeUserId";
const globalStorageKey = "global"; const globalStorageKey = "global";
@ -121,11 +124,7 @@ async function loadNotificationBar() {
// Example: '{"bitwarden.com":null}' // Example: '{"bitwarden.com":null}'
const excludedDomainsDict = globalSettings.neverDomains; const excludedDomainsDict = globalSettings.neverDomains;
if (!excludedDomainsDict || !(window.location.hostname in excludedDomainsDict)) { if (!excludedDomainsDict || !(window.location.hostname in excludedDomainsDict)) {
// Set local disabled preferences if (enableAddedLoginPrompt || enableChangedPasswordPrompt) {
disabledAddLoginNotification = globalSettings.disableAddLoginNotification;
disabledChangedPasswordNotification = globalSettings.disableChangedPasswordNotification;
if (!disabledAddLoginNotification || !disabledChangedPasswordNotification) {
// If the user has not disabled both notifications, then handle the initial page change (null -> actual page) // If the user has not disabled both notifications, then handle the initial page change (null -> actual page)
handlePageChange(); handlePageChange();
} }
@ -352,9 +351,7 @@ async function loadNotificationBar() {
// to avoid missing any forms that are added after the page loads // to avoid missing any forms that are added after the page loads
observeDom(); observeDom();
sendPlatformMessage({ void sendExtensionMessage("checkNotificationQueue");
command: "checkNotificationQueue",
});
} }
// This is a safeguard in case the observer misses a SPA page change. // This is a safeguard in case the observer misses a SPA page change.
@ -392,10 +389,7 @@ async function loadNotificationBar() {
* *
* */ * */
function collectPageDetails() { function collectPageDetails() {
sendPlatformMessage({ void sendExtensionMessage("bgCollectPageDetails", { sender: "notificationBar" });
command: "bgCollectPageDetails",
sender: "notificationBar",
});
} }
// End Page Detail Collection Methods // End Page Detail Collection Methods
@ -620,10 +614,9 @@ async function loadNotificationBar() {
continue; continue;
} }
const disabledBoth = disabledChangedPasswordNotification && disabledAddLoginNotification; // if user has enabled either add login or change password notification, and we have a username and password field
// if user has not disabled both notifications and we have a username and password field,
if ( if (
!disabledBoth && (enableChangedPasswordPrompt || enableAddedLoginPrompt) &&
watchedForms[i].usernameEl != null && watchedForms[i].usernameEl != null &&
watchedForms[i].passwordEl != null watchedForms[i].passwordEl != null
) { ) {
@ -639,10 +632,7 @@ async function loadNotificationBar() {
const passwordPopulated = login.password != null && login.password !== ""; const passwordPopulated = login.password != null && login.password !== "";
if (userNamePopulated && passwordPopulated) { if (userNamePopulated && passwordPopulated) {
processedForm(form); processedForm(form);
sendPlatformMessage({ void sendExtensionMessage("bgAddLogin", { login });
command: "bgAddLogin",
login,
});
break; break;
} else if ( } else if (
userNamePopulated && userNamePopulated &&
@ -659,7 +649,7 @@ async function loadNotificationBar() {
// if user has not disabled the password changed notification and we have multiple password fields, // if user has not disabled the password changed notification and we have multiple password fields,
// then check if the user has changed their password // then check if the user has changed their password
if (!disabledChangedPasswordNotification && watchedForms[i].passwordEls != null) { if (enableChangedPasswordPrompt && watchedForms[i].passwordEls != null) {
// Get the values of the password fields // Get the values of the password fields
const passwords: string[] = watchedForms[i].passwordEls const passwords: string[] = watchedForms[i].passwordEls
.filter((el: HTMLInputElement) => el.value != null && el.value !== "") .filter((el: HTMLInputElement) => el.value != null && el.value !== "")
@ -716,7 +706,7 @@ async function loadNotificationBar() {
currentPassword: curPass, currentPassword: curPass,
url: document.URL, url: document.URL,
}; };
sendPlatformMessage({ command: "bgChangedPassword", data }); void sendExtensionMessage("bgChangedPassword", { data });
break; break;
} }
} }
@ -954,9 +944,7 @@ async function loadNotificationBar() {
switch (barType) { switch (barType) {
case "add": case "add":
case "change": case "change":
sendPlatformMessage({ void sendExtensionMessage("bgRemoveTabFromNotificationQueue");
command: "bgRemoveTabFromNotificationQueue",
});
break; break;
default: default:
break; break;
@ -981,12 +969,6 @@ async function loadNotificationBar() {
// End Notification Bar Functions (open, close, height adjustment, etc.) // End Notification Bar Functions (open, close, height adjustment, etc.)
// Helper Functions // Helper Functions
function sendPlatformMessage(msg: any) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
chrome.runtime.sendMessage(msg);
}
function isInIframe() { function isInIframe() {
try { try {
return window.self !== window.top; return window.self !== window.top;

View File

@ -166,11 +166,10 @@ describe("AutofillService", () => {
jest jest
.spyOn(autofillService, "getOverlayVisibility") .spyOn(autofillService, "getOverlayVisibility")
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
}); });
it("accepts an extension message sender and injects the autofill scripts into the tab of the sender", async () => { it("accepts an extension message sender and injects the autofill scripts into the tab of the sender", async () => {
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, true); await autofillService.injectAutofillScripts(sender.tab, sender.frameId, true);
[autofillOverlayBootstrapScript, ...defaultAutofillScripts].forEach((scriptName) => { [autofillOverlayBootstrapScript, ...defaultAutofillScripts].forEach((scriptName) => {
@ -195,11 +194,6 @@ describe("AutofillService", () => {
}); });
it("will inject the bootstrap-autofill-overlay script if the user has the autofill overlay enabled", async () => { it("will inject the bootstrap-autofill-overlay script if the user has the autofill overlay enabled", async () => {
jest
.spyOn(autofillService, "getOverlayVisibility")
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
await autofillService.injectAutofillScripts(sender.tab, sender.frameId); await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, { expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
@ -218,7 +212,6 @@ describe("AutofillService", () => {
jest jest
.spyOn(autofillService, "getOverlayVisibility") .spyOn(autofillService, "getOverlayVisibility")
.mockResolvedValue(AutofillOverlayVisibility.Off); .mockResolvedValue(AutofillOverlayVisibility.Off);
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
await autofillService.injectAutofillScripts(sender.tab, sender.frameId); await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
@ -235,8 +228,6 @@ describe("AutofillService", () => {
}); });
it("injects the content-message-handler script if not injecting on page load", async () => { it("injects the content-message-handler script if not injecting on page load", async () => {
jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true);
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, false); await autofillService.injectAutofillScripts(sender.tab, sender.frameId, false);
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, { expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {

View File

@ -39,10 +39,7 @@ export type UserSettings = {
vaultTimeoutAction: VaultTimeoutAction; vaultTimeoutAction: VaultTimeoutAction;
}; };
export type GlobalSettings = Pick< export type GlobalSettings = Pick<GlobalState, "neverDomains">;
GlobalState,
"disableAddLoginNotification" | "disableChangedPasswordNotification" | "neverDomains"
>;
/** /**
* A HTMLElement (usually a form element) with additional custom properties added by this script * A HTMLElement (usually a form element) with additional custom properties added by this script

View File

@ -55,6 +55,10 @@ import {
BadgeSettingsServiceAbstraction, BadgeSettingsServiceAbstraction,
BadgeSettingsService, BadgeSettingsService,
} from "@bitwarden/common/autofill/services/badge-settings.service"; } from "@bitwarden/common/autofill/services/badge-settings.service";
import {
UserNotificationSettingsService,
UserNotificationSettingsServiceAbstraction,
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -248,6 +252,7 @@ export default class MainBackground {
searchService: SearchServiceAbstraction; searchService: SearchServiceAbstraction;
notificationsService: NotificationsServiceAbstraction; notificationsService: NotificationsServiceAbstraction;
stateService: StateServiceAbstraction; stateService: StateServiceAbstraction;
userNotificationSettingsService: UserNotificationSettingsServiceAbstraction;
autofillSettingsService: AutofillSettingsServiceAbstraction; autofillSettingsService: AutofillSettingsServiceAbstraction;
badgeSettingsService: BadgeSettingsServiceAbstraction; badgeSettingsService: BadgeSettingsServiceAbstraction;
systemService: SystemServiceAbstraction; systemService: SystemServiceAbstraction;
@ -419,6 +424,7 @@ export default class MainBackground {
this.environmentService, this.environmentService,
migrationRunner, migrationRunner,
); );
this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider);
this.platformUtilsService = new BrowserPlatformUtilsService( this.platformUtilsService = new BrowserPlatformUtilsService(
this.messagingService, this.messagingService,
(clipboardValue, clearMs) => { (clipboardValue, clearMs) => {
@ -846,6 +852,7 @@ export default class MainBackground {
this.policyService, this.policyService,
this.folderService, this.folderService,
this.stateService, this.stateService,
this.userNotificationSettingsService,
this.environmentService, this.environmentService,
this.logService, this.logService,
); );
@ -1088,10 +1095,12 @@ export default class MainBackground {
this.keyConnectorService.clear(), this.keyConnectorService.clear(),
this.vaultFilterService.clear(), this.vaultFilterService.clear(),
this.biometricStateService.logout(userId), this.biometricStateService.logout(userId),
/* We intentionally do not clear: /*
* - autofillSettingsService We intentionally do not clear:
* - badgeSettingsService - autofillSettingsService
*/ - badgeSettingsService
- userNotificationSettingsService
*/
]); ]);
//Needs to be checked before state is cleaned //Needs to be checked before state is cleaned

View File

@ -42,6 +42,10 @@ import {
AutofillSettingsService, AutofillSettingsService,
AutofillSettingsServiceAbstraction, AutofillSettingsServiceAbstraction,
} from "@bitwarden/common/autofill/services/autofill-settings.service"; } from "@bitwarden/common/autofill/services/autofill-settings.service";
import {
UserNotificationSettingsService,
UserNotificationSettingsServiceAbstraction,
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -551,6 +555,11 @@ function getBgService<T>(service: keyof MainBackground) {
useClass: AutofillSettingsService, useClass: AutofillSettingsService,
deps: [StateProvider, PolicyService], deps: [StateProvider, PolicyService],
}, },
{
provide: UserNotificationSettingsServiceAbstraction,
useClass: UserNotificationSettingsService,
deps: [StateProvider],
},
], ],
}) })
export class ServicesModule {} export class ServicesModule {}

View File

@ -5,6 +5,7 @@ import { AbstractThemingService } from "@bitwarden/angular/platform/services/the
import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@ -47,6 +48,7 @@ export class OptionsComponent implements OnInit {
constructor( constructor(
private messagingService: MessagingService, private messagingService: MessagingService,
private stateService: StateService, private stateService: StateService,
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private autofillSettingsService: AutofillSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction,
private badgeSettingsService: BadgeSettingsServiceAbstraction, private badgeSettingsService: BadgeSettingsServiceAbstraction,
i18nService: I18nService, i18nService: I18nService,
@ -95,10 +97,13 @@ export class OptionsComponent implements OnInit {
this.autofillSettingsService.autofillOnPageLoadDefault$, this.autofillSettingsService.autofillOnPageLoadDefault$,
); );
this.enableAddLoginNotification = !(await this.stateService.getDisableAddLoginNotification()); this.enableAddLoginNotification = await firstValueFrom(
this.userNotificationSettingsService.enableAddedLoginPrompt$,
);
this.enableChangedPasswordNotification = this.enableChangedPasswordNotification = await firstValueFrom(
!(await this.stateService.getDisableChangedPasswordNotification()); this.userNotificationSettingsService.enableChangedPasswordPrompt$,
);
this.enableContextMenuItem = !(await this.stateService.getDisableContextMenuItem()); this.enableContextMenuItem = !(await this.stateService.getDisableContextMenuItem());
@ -122,12 +127,14 @@ export class OptionsComponent implements OnInit {
} }
async updateAddLoginNotification() { async updateAddLoginNotification() {
await this.stateService.setDisableAddLoginNotification(!this.enableAddLoginNotification); await this.userNotificationSettingsService.setEnableAddedLoginPrompt(
this.enableAddLoginNotification,
);
} }
async updateChangedPasswordNotification() { async updateChangedPasswordNotification() {
await this.stateService.setDisableChangedPasswordNotification( await this.userNotificationSettingsService.setEnableChangedPasswordPrompt(
!this.enableChangedPasswordNotification, this.enableChangedPasswordNotification,
); );
} }

View File

@ -0,0 +1,60 @@
import { map, Observable } from "rxjs";
import {
USER_NOTIFICATION_SETTINGS_DISK,
GlobalState,
KeyDefinition,
StateProvider,
} from "../../platform/state";
const ENABLE_ADDED_LOGIN_PROMPT = new KeyDefinition(
USER_NOTIFICATION_SETTINGS_DISK,
"enableAddedLoginPrompt",
{
deserializer: (value: boolean) => value ?? true,
},
);
const ENABLE_CHANGED_PASSWORD_PROMPT = new KeyDefinition(
USER_NOTIFICATION_SETTINGS_DISK,
"enableChangedPasswordPrompt",
{
deserializer: (value: boolean) => value ?? true,
},
);
export abstract class UserNotificationSettingsServiceAbstraction {
enableAddedLoginPrompt$: Observable<boolean>;
setEnableAddedLoginPrompt: (newValue: boolean) => Promise<void>;
enableChangedPasswordPrompt$: Observable<boolean>;
setEnableChangedPasswordPrompt: (newValue: boolean) => Promise<void>;
}
export class UserNotificationSettingsService implements UserNotificationSettingsServiceAbstraction {
private enableAddedLoginPromptState: GlobalState<boolean>;
readonly enableAddedLoginPrompt$: Observable<boolean>;
private enableChangedPasswordPromptState: GlobalState<boolean>;
readonly enableChangedPasswordPrompt$: Observable<boolean>;
constructor(private stateProvider: StateProvider) {
this.enableAddedLoginPromptState = this.stateProvider.getGlobal(ENABLE_ADDED_LOGIN_PROMPT);
this.enableAddedLoginPrompt$ = this.enableAddedLoginPromptState.state$.pipe(
map((x) => x ?? true),
);
this.enableChangedPasswordPromptState = this.stateProvider.getGlobal(
ENABLE_CHANGED_PASSWORD_PROMPT,
);
this.enableChangedPasswordPrompt$ = this.enableChangedPasswordPromptState.state$.pipe(
map((x) => x ?? true),
);
}
async setEnableAddedLoginPrompt(newValue: boolean): Promise<void> {
await this.enableAddedLoginPromptState.update(() => newValue);
}
async setEnableChangedPasswordPrompt(newValue: boolean): Promise<void> {
await this.enableChangedPasswordPromptState.update(() => newValue);
}
}

View File

@ -200,13 +200,6 @@ export abstract class StateService<T extends Account = Account> {
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>; setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>; getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>;
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>; setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
getDisableAddLoginNotification: (options?: StorageOptions) => Promise<boolean>;
setDisableAddLoginNotification: (value: boolean, options?: StorageOptions) => Promise<void>;
getDisableChangedPasswordNotification: (options?: StorageOptions) => Promise<boolean>;
setDisableChangedPasswordNotification: (
value: boolean,
options?: StorageOptions,
) => Promise<void>;
getDisableContextMenuItem: (options?: StorageOptions) => Promise<boolean>; getDisableContextMenuItem: (options?: StorageOptions) => Promise<boolean>;
setDisableContextMenuItem: (value: boolean, options?: StorageOptions) => Promise<void>; setDisableContextMenuItem: (value: boolean, options?: StorageOptions) => Promise<void>;
/** /**

View File

@ -26,8 +26,6 @@ export class GlobalState {
enableBrowserIntegrationFingerprint?: boolean; enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean; enableDuckDuckGoBrowserIntegration?: boolean;
neverDomains?: { [id: string]: unknown }; neverDomains?: { [id: string]: unknown };
disableAddLoginNotification?: boolean;
disableChangedPasswordNotification?: boolean;
disableContextMenuItem?: boolean; disableContextMenuItem?: boolean;
deepLinkRedirectUrl?: string; deepLinkRedirectUrl?: string;
} }

View File

@ -853,45 +853,6 @@ export class StateService<
); );
} }
async getDisableAddLoginNotification(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.disableAddLoginNotification ?? false
);
}
async setDisableAddLoginNotification(value: boolean, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.disableAddLoginNotification = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getDisableChangedPasswordNotification(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.disableChangedPasswordNotification ?? false
);
}
async setDisableChangedPasswordNotification(
value: boolean,
options?: StorageOptions,
): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.disableChangedPasswordNotification = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getDisableContextMenuItem(options?: StorageOptions): Promise<boolean> { async getDisableContextMenuItem(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))

View File

@ -63,6 +63,10 @@ export const VAULT_FILTER_DISK = new StateDefinition("vaultFilter", "disk", {
web: "disk-local", web: "disk-local",
}); });
export const USER_NOTIFICATION_SETTINGS_DISK = new StateDefinition(
"userNotificationSettings",
"disk",
);
export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk"); export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk");
export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanner", "disk", { export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanner", "disk", {

View File

@ -23,6 +23,7 @@ import { ClearClipboardDelayMigrator } from "./migrations/25-move-clear-clipboar
import { RevertLastSyncMigrator } from "./migrations/26-revert-move-last-sync-to-state-provider"; import { RevertLastSyncMigrator } from "./migrations/26-revert-move-last-sync-to-state-provider";
import { BadgeSettingsMigrator } from "./migrations/27-move-badge-settings-to-state-providers"; import { BadgeSettingsMigrator } from "./migrations/27-move-badge-settings-to-state-providers";
import { MoveBiometricUnlockToStateProviders } from "./migrations/28-move-biometric-unlock-to-state-providers"; import { MoveBiometricUnlockToStateProviders } from "./migrations/28-move-biometric-unlock-to-state-providers";
import { UserNotificationSettingsKeyMigrator } from "./migrations/29-move-user-notification-settings-to-state-provider";
import { FixPremiumMigrator } from "./migrations/3-fix-premium"; import { FixPremiumMigrator } from "./migrations/3-fix-premium";
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked"; import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
@ -33,7 +34,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version"; import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 2; export const MIN_VERSION = 2;
export const CURRENT_VERSION = 28; export const CURRENT_VERSION = 29;
export type MinVersion = typeof MIN_VERSION; export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() { export function createMigrationBuilder() {
@ -64,7 +65,8 @@ export function createMigrationBuilder() {
.with(ClearClipboardDelayMigrator, 24, 25) .with(ClearClipboardDelayMigrator, 24, 25)
.with(RevertLastSyncMigrator, 25, 26) .with(RevertLastSyncMigrator, 25, 26)
.with(BadgeSettingsMigrator, 26, 27) .with(BadgeSettingsMigrator, 26, 27)
.with(MoveBiometricUnlockToStateProviders, 27, CURRENT_VERSION); .with(MoveBiometricUnlockToStateProviders, 27, 28)
.with(UserNotificationSettingsKeyMigrator, 28, CURRENT_VERSION);
} }
export async function currentVersion( export async function currentVersion(

View File

@ -0,0 +1,102 @@
import { MockProxy } from "jest-mock-extended";
import { StateDefinitionLike, MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import { UserNotificationSettingsKeyMigrator } from "./29-move-user-notification-settings-to-state-provider";
function exampleJSON() {
return {
global: {
disableAddLoginNotification: false,
disableChangedPasswordNotification: false,
otherStuff: "otherStuff1",
},
};
}
function rollbackJSON() {
return {
global_userNotificationSettings_enableAddedLoginPrompt: true,
global_userNotificationSettings_enableChangedPasswordPrompt: true,
global: {
otherStuff: "otherStuff1",
},
};
}
const userNotificationSettingsLocalStateDefinition: {
stateDefinition: StateDefinitionLike;
} = {
stateDefinition: {
name: "userNotificationSettings",
},
};
describe("ProviderKeysMigrator", () => {
let helper: MockProxy<MigrationHelper>;
let sut: UserNotificationSettingsKeyMigrator;
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(exampleJSON(), 28);
sut = new UserNotificationSettingsKeyMigrator(28, 29);
});
it("should remove disableAddLoginNotification and disableChangedPasswordNotification global setting", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledTimes(2);
expect(helper.set).toHaveBeenCalledWith("global", { otherStuff: "otherStuff1" });
expect(helper.set).toHaveBeenCalledWith("global", { otherStuff: "otherStuff1" });
});
it("should set global user notification setting values", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledTimes(2);
expect(helper.setToGlobal).toHaveBeenCalledWith(
{ ...userNotificationSettingsLocalStateDefinition, key: "enableAddedLoginPrompt" },
true,
);
expect(helper.setToGlobal).toHaveBeenCalledWith(
{ ...userNotificationSettingsLocalStateDefinition, key: "enableChangedPasswordPrompt" },
true,
);
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJSON(), 29);
sut = new UserNotificationSettingsKeyMigrator(28, 29);
});
it("should null out new global values", async () => {
await sut.rollback(helper);
expect(helper.setToGlobal).toHaveBeenCalledTimes(2);
expect(helper.setToGlobal).toHaveBeenCalledWith(
{ ...userNotificationSettingsLocalStateDefinition, key: "enableAddedLoginPrompt" },
null,
);
expect(helper.setToGlobal).toHaveBeenCalledWith(
{ ...userNotificationSettingsLocalStateDefinition, key: "enableChangedPasswordPrompt" },
null,
);
});
it("should add explicit global values back", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledTimes(2);
expect(helper.set).toHaveBeenCalledWith("global", {
disableAddLoginNotification: false,
otherStuff: "otherStuff1",
});
expect(helper.set).toHaveBeenCalledWith("global", {
disableChangedPasswordNotification: false,
otherStuff: "otherStuff1",
});
});
});
});

View File

@ -0,0 +1,105 @@
import { MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
type ExpectedGlobalState = {
disableAddLoginNotification?: boolean;
disableChangedPasswordNotification?: boolean;
};
export class UserNotificationSettingsKeyMigrator extends Migrator<28, 29> {
async migrate(helper: MigrationHelper): Promise<void> {
const globalState = await helper.get<ExpectedGlobalState>("global");
// disableAddLoginNotification -> enableAddedLoginPrompt
if (globalState?.disableAddLoginNotification != null) {
await helper.setToGlobal(
{
stateDefinition: {
name: "userNotificationSettings",
},
key: "enableAddedLoginPrompt",
},
!globalState.disableAddLoginNotification,
);
// delete `disableAddLoginNotification` from state global
delete globalState.disableAddLoginNotification;
await helper.set<ExpectedGlobalState>("global", globalState);
}
// disableChangedPasswordNotification -> enableChangedPasswordPrompt
if (globalState?.disableChangedPasswordNotification != null) {
await helper.setToGlobal(
{
stateDefinition: {
name: "userNotificationSettings",
},
key: "enableChangedPasswordPrompt",
},
!globalState.disableChangedPasswordNotification,
);
// delete `disableChangedPasswordNotification` from state global
delete globalState.disableChangedPasswordNotification;
await helper.set<ExpectedGlobalState>("global", globalState);
}
}
async rollback(helper: MigrationHelper): Promise<void> {
const globalState = (await helper.get<ExpectedGlobalState>("global")) || {};
const enableAddedLoginPrompt: boolean = await helper.getFromGlobal({
stateDefinition: {
name: "userNotificationSettings",
},
key: "enableAddedLoginPrompt",
});
const enableChangedPasswordPrompt: boolean = await helper.getFromGlobal({
stateDefinition: {
name: "userNotificationSettings",
},
key: "enableChangedPasswordPrompt",
});
// enableAddedLoginPrompt -> disableAddLoginNotification
if (enableAddedLoginPrompt) {
await helper.set<ExpectedGlobalState>("global", {
...globalState,
disableAddLoginNotification: !enableAddedLoginPrompt,
});
// remove the global state provider framework key for `enableAddedLoginPrompt`
await helper.setToGlobal(
{
stateDefinition: {
name: "userNotificationSettings",
},
key: "enableAddedLoginPrompt",
},
null,
);
}
// enableChangedPasswordPrompt -> disableChangedPasswordNotification
if (enableChangedPasswordPrompt) {
await helper.set<ExpectedGlobalState>("global", {
...globalState,
disableChangedPasswordNotification: !enableChangedPasswordPrompt,
});
// remove the global state provider framework key for `enableChangedPasswordPrompt`
await helper.setToGlobal(
{
stateDefinition: {
name: "userNotificationSettings",
},
key: "enableChangedPasswordPrompt",
},
null,
);
}
}
}