diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts index 9496d932d0..21ff91c02d 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -18,6 +18,7 @@ import { } from "../../auth/background/service-factories/auth-service.factory"; import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory"; import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; +import { autofillSettingsServiceFactory } from "../../autofill/background/service_factories/autofill-settings-service.factory"; import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory"; import { Account } from "../../models/account"; import { CachedServices } from "../../platform/background/service-factories/factory-options"; @@ -104,11 +105,14 @@ export class ContextMenuClickedHandler { stateServiceOptions: { stateFactory: stateFactory, }, + autofillSettingsServiceOptions: { + stateFactory: autofillSettingsServiceFactory, + }, }; const generatePasswordToClipboardCommand = new GeneratePasswordToClipboardCommand( await passwordGenerationServiceFactory(cachedServices, serviceOptions), - await stateServiceFactory(cachedServices, serviceOptions), + await autofillSettingsServiceFactory(cachedServices, serviceOptions), ); const autofillCommand = new AutofillTabCommand( diff --git a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts index 3001087f74..e3639ef81f 100644 --- a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts +++ b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts @@ -1,10 +1,10 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { setAlarmTime } from "../../platform/alarms/alarm-state"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service"; import { clearClipboardAlarmName } from "./clear-clipboard"; import { GeneratePasswordToClipboardCommand } from "./generate-password-to-clipboard-command"; @@ -19,13 +19,12 @@ const setAlarmTimeMock = setAlarmTime as jest.Mock; describe("GeneratePasswordToClipboardCommand", () => { let passwordGenerationService: MockProxy; - let stateService: MockProxy; + let autofillSettingsService: MockProxy; let sut: GeneratePasswordToClipboardCommand; beforeEach(() => { passwordGenerationService = mock(); - stateService = mock(); passwordGenerationService.getOptions.mockResolvedValue([{ length: 8 }, {} as any]); @@ -33,7 +32,10 @@ describe("GeneratePasswordToClipboardCommand", () => { jest.spyOn(BrowserApi, "sendTabsMessage").mockReturnValue(); - sut = new GeneratePasswordToClipboardCommand(passwordGenerationService, stateService); + sut = new GeneratePasswordToClipboardCommand( + passwordGenerationService, + autofillSettingsService, + ); }); afterEach(() => { @@ -42,7 +44,7 @@ describe("GeneratePasswordToClipboardCommand", () => { describe("generatePasswordToClipboard", () => { it("has clear clipboard value", async () => { - stateService.getClearClipboard.mockResolvedValue(5 * 60); // 5 minutes + jest.spyOn(sut as any, "getClearClipboard").mockImplementation(() => 5 * 60); // 5 minutes await sut.generatePasswordToClipboard({ id: 1 } as any); @@ -59,7 +61,7 @@ describe("GeneratePasswordToClipboardCommand", () => { }); it("does not have clear clipboard value", async () => { - stateService.getClearClipboard.mockResolvedValue(null); + jest.spyOn(sut as any, "getClearClipboard").mockImplementation(() => null); await sut.generatePasswordToClipboard({ id: 1 } as any); diff --git a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts index 7813acd179..c46718ebad 100644 --- a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts +++ b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts @@ -1,7 +1,9 @@ +import { firstValueFrom } from "rxjs"; + +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { setAlarmTime } from "../../platform/alarms/alarm-state"; -import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service"; import { clearClipboardAlarmName } from "./clear-clipboard"; import { copyToClipboard } from "./copy-to-clipboard-command"; @@ -9,9 +11,13 @@ import { copyToClipboard } from "./copy-to-clipboard-command"; export class GeneratePasswordToClipboardCommand { constructor( private passwordGenerationService: PasswordGenerationServiceAbstraction, - private stateService: BrowserStateService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, ) {} + async getClearClipboard() { + return await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); + } + async generatePasswordToClipboard(tab: chrome.tabs.Tab) { const [options] = await this.passwordGenerationService.getOptions(); const password = await this.passwordGenerationService.generatePassword(options); @@ -20,7 +26,7 @@ export class GeneratePasswordToClipboardCommand { // eslint-disable-next-line @typescript-eslint/no-floating-promises copyToClipboard(tab, password); - const clearClipboard = await this.stateService.getClearClipboard(); + const clearClipboard = await this.getClearClipboard(); if (clearClipboard != null) { await setAlarmTime(clearClipboardAlarmName, clearClipboard * 1000); diff --git a/apps/browser/src/autofill/constants.ts b/apps/browser/src/autofill/constants.ts index 2c361723ad..da4bae0183 100644 --- a/apps/browser/src/autofill/constants.ts +++ b/apps/browser/src/autofill/constants.ts @@ -22,6 +22,19 @@ export const EVENTS = { FOCUSOUT: "focusout", } as const; +export const ClearClipboardDelay = { + Never: null as null, + TenSeconds: 10, + TwentySeconds: 20, + ThirtySeconds: 30, + OneMinute: 60, + TwoMinutes: 120, + FiveMinutes: 300, +} as const; + +export type ClearClipboardDelaySetting = + (typeof ClearClipboardDelay)[keyof typeof ClearClipboardDelay]; + /* Context Menu item Ids */ export const AUTOFILL_CARD_ID = "autofill-card"; export const AUTOFILL_ID = "autofill"; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index c9831aa762..7bc6228738 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -779,6 +779,7 @@ export default class MainBackground { this.platformUtilsService, systemUtilsServiceReloadCallback, this.stateService, + this.autofillSettingsService, this.vaultTimeoutSettingsService, ); diff --git a/apps/browser/src/platform/listeners/on-command-listener.ts b/apps/browser/src/platform/listeners/on-command-listener.ts index b0dc3bf5f6..fc2939dce8 100644 --- a/apps/browser/src/platform/listeners/on-command-listener.ts +++ b/apps/browser/src/platform/listeners/on-command-listener.ts @@ -4,10 +4,10 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta import { authServiceFactory } from "../../auth/background/service-factories/auth-service.factory"; import { autofillServiceFactory } from "../../autofill/background/service_factories/autofill-service.factory"; +import { autofillSettingsServiceFactory } from "../../autofill/background/service_factories/autofill-settings-service.factory"; import { GeneratePasswordToClipboardCommand } from "../../autofill/clipboard"; import { AutofillTabCommand } from "../../autofill/commands/autofill-tab-command"; import { Account } from "../../models/account"; -import { stateServiceFactory } from "../../platform/background/service-factories/state-service.factory"; import { passwordGenerationServiceFactory, PasswordGenerationServiceInitOptions, @@ -94,11 +94,14 @@ const doGeneratePasswordToClipboard = async (tab: chrome.tabs.Tab): Promise value ?? ClearClipboardDelay.Never, + }, +); + export abstract class AutofillSettingsServiceAbstraction { autofillOnPageLoad$: Observable; setAutofillOnPageLoad: (newValue: boolean) => Promise; @@ -69,6 +81,8 @@ export abstract class AutofillSettingsServiceAbstraction { setActivateAutofillOnPageLoadFromPolicy: (newValue: boolean) => Promise; inlineMenuVisibility$: Observable; setInlineMenuVisibility: (newValue: InlineMenuVisibilitySetting) => Promise; + clearClipboardDelay$: Observable; + setClearClipboardDelay: (newValue: ClearClipboardDelaySetting) => Promise; handleActivateAutofillPolicy: (policies: Observable) => Observable; } @@ -91,6 +105,9 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti private inlineMenuVisibilityState: GlobalState; readonly inlineMenuVisibility$: Observable; + private clearClipboardDelayState: ActiveUserState; + readonly clearClipboardDelay$: Observable; + constructor( private stateProvider: StateProvider, policyService: PolicyService, @@ -125,6 +142,11 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti map((x) => x ?? AutofillOverlayVisibility.Off), ); + this.clearClipboardDelayState = this.stateProvider.getActive(CLEAR_CLIPBOARD_DELAY); + this.clearClipboardDelay$ = this.clearClipboardDelayState.state$.pipe( + map((x) => x ?? ClearClipboardDelay.Never), + ); + policyService.policies$.pipe(this.handleActivateAutofillPolicy.bind(this)).subscribe(); } @@ -152,6 +174,10 @@ export class AutofillSettingsService implements AutofillSettingsServiceAbstracti await this.inlineMenuVisibilityState.update(() => newValue); } + async setClearClipboardDelay(newValue: ClearClipboardDelaySetting): Promise { + await this.clearClipboardDelayState.update(() => newValue); + } + /** * If the ActivateAutofill policy is enabled, save a flag indicating if we need to * enable Autofill on page load. diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 96a870f93a..505595eb29 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -81,8 +81,6 @@ export abstract class StateService { setHasPremiumPersonally: (value: boolean, options?: StorageOptions) => Promise; setHasPremiumFromOrganization: (value: boolean, options?: StorageOptions) => Promise; getHasPremiumFromOrganization: (options?: StorageOptions) => Promise; - getClearClipboard: (options?: StorageOptions) => Promise; - setClearClipboard: (value: number, options?: StorageOptions) => Promise; getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise; setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise; /** diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index d161616366..e75efd34cd 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -201,7 +201,6 @@ export class AccountProfile { export class AccountSettings { autoConfirmFingerPrints?: boolean; biometricUnlock?: boolean; - clearClipboard?: number; defaultUriMatch?: UriMatchType; disableBadgeCounter?: boolean; disableGa?: boolean; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index cbbfa89b69..0e45c94228 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -462,27 +462,6 @@ export class StateService< ); } - async getClearClipboard(options?: StorageOptions): Promise { - return ( - ( - await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ) - )?.settings?.clearClipboard ?? null - ); - } - - async setClearClipboard(value: number, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.clearClipboard = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - async getConvertAccountToKeyConnector(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) diff --git a/libs/common/src/platform/services/system.service.ts b/libs/common/src/platform/services/system.service.ts index beb37be73a..06f9fcf8fb 100644 --- a/libs/common/src/platform/services/system.service.ts +++ b/libs/common/src/platform/services/system.service.ts @@ -3,6 +3,7 @@ import { firstValueFrom, timeout } from "rxjs"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { MessagingService } from "../abstractions/messaging.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; @@ -20,6 +21,7 @@ export class SystemService implements SystemServiceAbstraction { private platformUtilsService: PlatformUtilsService, private reloadCallback: () => Promise = null, private stateService: StateService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, ) {} @@ -93,26 +95,33 @@ export class SystemService implements SystemServiceAbstraction { clearTimeout(this.clearClipboardTimeout); this.clearClipboardTimeout = null; } + if (Utils.isNullOrWhitespace(clipboardValue)) { return; } - await this.stateService.getClearClipboard().then((clearSeconds) => { - if (clearSeconds == null) { - return; + + const clearClipboardDelay = await firstValueFrom( + this.autofillSettingsService.clearClipboardDelay$, + ); + + if (clearClipboardDelay == null) { + return; + } + + if (timeoutMs == null) { + timeoutMs = clearClipboardDelay * 1000; + } + + this.clearClipboardTimeoutFunction = async () => { + const clipboardValueNow = await this.platformUtilsService.readFromClipboard(); + if (clipboardValue === clipboardValueNow) { + this.platformUtilsService.copyToClipboard("", { clearing: true }); } - if (timeoutMs == null) { - timeoutMs = clearSeconds * 1000; - } - this.clearClipboardTimeoutFunction = async () => { - const clipboardValueNow = await this.platformUtilsService.readFromClipboard(); - if (clipboardValue === clipboardValueNow) { - this.platformUtilsService.copyToClipboard("", { clearing: true }); - } - }; - this.clearClipboardTimeout = setTimeout(async () => { - await this.clearPendingClipboard(); - }, timeoutMs); - }); + }; + + this.clearClipboardTimeout = setTimeout(async () => { + await this.clearPendingClipboard(); + }, timeoutMs); } async clearPendingClipboard() { diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 330c1f6a8d..182ca4ea6a 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -19,6 +19,7 @@ import { CollectionMigrator } from "./migrations/21-move-collections-state-to-st import { CollapsedGroupingsMigrator } from "./migrations/22-move-collapsed-groupings-to-state-provider"; import { MoveBiometricPromptsToStateProviders } from "./migrations/23-move-biometric-prompts-to-state-providers"; import { SmOnboardingTasksMigrator } from "./migrations/24-move-sm-onboarding-key-to-state-providers"; +import { ClearClipboardDelayMigrator } from "./migrations/25-move-clear-clipboard-to-autofill-settings-state-provider"; import { FixPremiumMigrator } from "./migrations/3-fix-premium"; import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; @@ -29,7 +30,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 2; -export const CURRENT_VERSION = 24; +export const CURRENT_VERSION = 25; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -56,7 +57,8 @@ export function createMigrationBuilder() { .with(CollectionMigrator, 20, 21) .with(CollapsedGroupingsMigrator, 21, 22) .with(MoveBiometricPromptsToStateProviders, 22, 23) - .with(SmOnboardingTasksMigrator, 23, CURRENT_VERSION); + .with(SmOnboardingTasksMigrator, 23, 24) + .with(ClearClipboardDelayMigrator, 24, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.spec.ts b/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.spec.ts index 6a346ab7a3..84ca11fde1 100644 --- a/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.spec.ts +++ b/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.spec.ts @@ -1,11 +1,16 @@ import { any, MockProxy } from "jest-mock-extended"; -import { AutofillOverlayVisibility } from "../../../../../apps/browser/src/autofill/utils/autofill-overlay.enum"; import { StateDefinitionLike, MigrationHelper } from "../migration-helper"; import { mockMigrationHelper } from "../migration-helper.spec"; import { AutofillSettingsKeyMigrator } from "./18-move-autofill-settings-to-state-providers"; +const AutofillOverlayVisibility = { + Off: 0, + OnButtonClick: 1, + OnFieldFocus: 2, +} as const; + function exampleJSON() { return { global: { diff --git a/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.spec.ts new file mode 100644 index 0000000000..083c1fb030 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.spec.ts @@ -0,0 +1,177 @@ +import { any, MockProxy } from "jest-mock-extended"; + +import { StateDefinitionLike, MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { ClearClipboardDelayMigrator } from "./25-move-clear-clipboard-to-autofill-settings-state-provider"; + +export const ClearClipboardDelay = { + Never: null as null, + TenSeconds: 10, + TwentySeconds: 20, + ThirtySeconds: 30, + OneMinute: 60, + TwoMinutes: 120, + FiveMinutes: 300, +} as const; + +const AutofillOverlayVisibility = { + Off: 0, + OnButtonClick: 1, + OnFieldFocus: 2, +} as const; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user-1", "user-2", "user-3"], + "user-1": { + settings: { + clearClipboard: ClearClipboardDelay.TenSeconds, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + "user-2": { + settings: { + clearClipboard: ClearClipboardDelay.Never, + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + "user-3": { + settings: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +function rollbackJSON() { + return { + global_autofillSettingsLocal_inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick, + "user_user-1_autofillSettingsLocal_clearClipboardDelay": ClearClipboardDelay.TenSeconds, + "user_user-2_autofillSettingsLocal_clearClipboardDelay": ClearClipboardDelay.Never, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user-1", "user-2", "user-3"], + "user-1": { + settings: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + "user-2": { + settings: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +const autofillSettingsLocalStateDefinition: { + stateDefinition: StateDefinitionLike; +} = { + stateDefinition: { + name: "autofillSettingsLocal", + }, +}; + +describe("ProviderKeysMigrator", () => { + let helper: MockProxy; + let sut: ClearClipboardDelayMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 24); + sut = new ClearClipboardDelayMigrator(24, 25); + }); + + it("should remove clearClipboard setting from all accounts", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("user-1", { + settings: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("user-2", { + settings: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + }); + + it("should set autofill setting values for each account", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledTimes(2); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-1", + { ...autofillSettingsLocalStateDefinition, key: "clearClipboardDelay" }, + ClearClipboardDelay.TenSeconds, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-2", + { ...autofillSettingsLocalStateDefinition, key: "clearClipboardDelay" }, + ClearClipboardDelay.Never, + ); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 23); + sut = new ClearClipboardDelayMigrator(24, 25); + }); + + it("should null out new values for each account", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledTimes(2); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-1", + { ...autofillSettingsLocalStateDefinition, key: "clearClipboardDelay" }, + null, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "user-2", + { ...autofillSettingsLocalStateDefinition, key: "clearClipboardDelay" }, + null, + ); + }); + + it("should add explicit value back to accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("user-1", { + settings: { + clearClipboard: ClearClipboardDelay.TenSeconds, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("user-2", { + settings: { + clearClipboard: ClearClipboardDelay.Never, + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + }); + + it("should not try to restore values to missing accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith("user-3", any()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts b/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts new file mode 100644 index 0000000000..fde7ea9037 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts @@ -0,0 +1,77 @@ +import { ClearClipboardDelaySetting } from "../../../../../apps/browser/src/autofill/constants"; +import { StateDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccountState = { + settings?: { + clearClipboard?: ClearClipboardDelaySetting; + }; +}; + +const autofillSettingsLocalStateDefinition: { + stateDefinition: StateDefinitionLike; +} = { + stateDefinition: { + name: "autofillSettingsLocal", + }, +}; + +export class ClearClipboardDelayMigrator extends Migrator<24, 25> { + async migrate(helper: MigrationHelper): Promise { + // account state (e.g. account settings -> state provider framework keys) + const accounts = await helper.getAccounts(); + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + + // migrate account state + async function migrateAccount(userId: string, account: ExpectedAccountState): Promise { + const accountSettings = account?.settings; + + if (accountSettings?.clearClipboard !== undefined) { + await helper.setToUser( + userId, + { ...autofillSettingsLocalStateDefinition, key: "clearClipboardDelay" }, + accountSettings.clearClipboard, + ); + delete account.settings.clearClipboard; + + // update the state account settings with the migrated values deleted + await helper.set(userId, account); + } + } + } + + async rollback(helper: MigrationHelper): Promise { + // account state (e.g. state provider framework keys -> account settings) + const accounts = await helper.getAccounts(); + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + + // rollback account state + async function rollbackAccount(userId: string, account: ExpectedAccountState): Promise { + let settings = account?.settings || {}; + + const clearClipboardDelay: ClearClipboardDelaySetting = await helper.getFromUser(userId, { + ...autofillSettingsLocalStateDefinition, + key: "clearClipboardDelay", + }); + + // update new settings and remove the account state provider framework keys for the rolled back values + if (clearClipboardDelay !== undefined) { + settings = { ...settings, clearClipboard: clearClipboardDelay }; + + await helper.setToUser( + userId, + { ...autofillSettingsLocalStateDefinition, key: "clearClipboardDelay" }, + null, + ); + + // commit updated settings to state + await helper.set(userId, { + ...account, + settings, + }); + } + } + } +}