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:
parent
d87a8f9271
commit
4ba2717eb4
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
@ -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)),
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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, {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 {}
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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>;
|
||||||
/**
|
/**
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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())))
|
||||||
|
@ -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", {
|
||||||
|
@ -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(
|
||||||
|
@ -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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user