diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index c3a6357ed05b..c104ff40ad45 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -14,6 +14,7 @@ import { } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService, @@ -40,7 +41,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service"; import { AutofillOverlayElement, AutofillOverlayPort, @@ -102,7 +102,7 @@ describe("OverlayBackground", () => { let inlineMenuVisibilityMock$: BehaviorSubject; let autofillSettingsService: MockProxy; let i18nService: MockProxy; - let platformUtilsService: MockProxy; + let clipboardService: MockProxy; let enablePasskeysMock$: BehaviorSubject; let vaultSettingsServiceMock: MockProxy; let fido2ActiveRequestManager: Fido2ActiveRequestManager; @@ -181,7 +181,7 @@ describe("OverlayBackground", () => { autofillSettingsService = mock(); autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$; i18nService = mock(); - platformUtilsService = mock(); + clipboardService = mock(); enablePasskeysMock$ = new BehaviorSubject(true); vaultSettingsServiceMock = mock(); vaultSettingsServiceMock.enablePasskeys$ = enablePasskeysMock$; @@ -200,7 +200,7 @@ describe("OverlayBackground", () => { domainSettingsService, autofillSettingsService, i18nService, - platformUtilsService, + clipboardService, vaultSettingsServiceMock, fido2ActiveRequestManager, inlineMenuFieldQualificationService, @@ -3300,7 +3300,7 @@ describe("OverlayBackground", () => { ]); autofillService.isPasswordRepromptRequired.mockResolvedValue(false); const copyToClipboardSpy = jest - .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") + .spyOn(overlayBackground["clipboardService"], "copyToClipboard") .mockImplementation(); autofillService.doAutoFill.mockResolvedValue("totp-code"); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 3d2b1ec783c3..953df0d63744 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -24,6 +24,7 @@ import { DomainSettingsService } from "@bitwarden/common/autofill/services/domai import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { parseYearMonthExpiry } from "@bitwarden/common/autofill/utils"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { Fido2ActiveRequestEvents, @@ -31,7 +32,6 @@ import { } from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -219,7 +219,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private domainSettingsService: DomainSettingsService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private vaultSettingsService: VaultSettingsService, private fido2ActiveRequestManager: Fido2ActiveRequestManager, private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService, @@ -1116,9 +1116,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher); if (cipher.login?.totp) { - this.platformUtilsService.copyToClipboard( - await this.totpService.getCode(cipher.login.totp), - ); + this.clipboardService.copyToClipboard(await this.totpService.getCode(cipher.login.totp)); } return; } @@ -1144,7 +1142,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { }); if (totpCode) { - this.platformUtilsService.copyToClipboard(totpCode); + this.clipboardService.copyToClipboard(totpCode); } this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher); diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts index 2c22097f3d06..35225dd1aa0f 100644 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts @@ -12,6 +12,7 @@ import { DefaultDomainSettingsService, DomainSettingsService, } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService, @@ -34,7 +35,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; -import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service"; import { AutofillOverlayElement, AutofillOverlayPort, @@ -76,7 +76,7 @@ describe("OverlayBackground", () => { ); const autofillSettingsService = mock(); const i18nService = mock(); - const platformUtilsService = mock(); + const clipboardService = mock(); const themeStateService = mock(); const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => { const { initList, initButton } = options; @@ -108,7 +108,7 @@ describe("OverlayBackground", () => { domainSettingsService, autofillSettingsService, i18nService, - platformUtilsService, + clipboardService, themeStateService, ); @@ -1372,7 +1372,7 @@ describe("OverlayBackground", () => { ]); isPasswordRepromptRequiredSpy.mockResolvedValue(false); const copyToClipboardSpy = jest - .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") + .spyOn(overlayBackground["clipboardService"], "copyToClipboard") .mockImplementation(); doAutoFillSpy.mockReturnValueOnce("totp-code"); diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts index 5dfade0f8635..726dce27eb3f 100644 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts @@ -8,9 +8,9 @@ import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -104,7 +104,7 @@ class LegacyOverlayBackground implements OverlayBackgroundInterface { private domainSettingsService: DomainSettingsService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private themeStateService: ThemeStateService, ) {} @@ -256,7 +256,7 @@ class LegacyOverlayBackground implements OverlayBackgroundInterface { }); if (totpCode) { - this.platformUtilsService.copyToClipboard(totpCode); + this.clipboardService.copyToClipboard(totpCode); } this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 98f3867e5ffb..64088e84dd49 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -78,6 +78,7 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -251,6 +252,7 @@ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender import { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document"; import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service"; import { BrowserTaskSchedulerService } from "../platform/services/abstractions/browser-task-scheduler.service"; +import { BrowserClipboardService } from "../platform/services/browser-clipboard.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service"; @@ -258,7 +260,6 @@ import { BrowserScriptInjectorService } from "../platform/services/browser-scrip import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; -import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service"; import { BrowserSdkClientFactory } from "../platform/services/sdk/browser-sdk-client-factory"; import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service"; @@ -285,6 +286,7 @@ export default class MainBackground { largeObjectMemoryStorageForStateProviders: AbstractStorageService & ObservableStorageService; i18nService: I18nServiceAbstraction; platformUtilsService: PlatformUtilsServiceAbstraction; + clipboardService: ClipboardService; logService: LogServiceAbstraction; keyGenerationService: KeyGenerationServiceAbstraction; keyService: KeyServiceAbstraction; @@ -457,8 +459,10 @@ export default class MainBackground { this.offscreenDocumentService = new DefaultOffscreenDocumentService(this.logService); - this.platformUtilsService = new BackgroundPlatformUtilsService( - this.messagingService, + this.platformUtilsService = new BackgroundPlatformUtilsService(this.messagingService, self); + + this.clipboardService = new BrowserClipboardService( + this.platformUtilsService, (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), self, this.offscreenDocumentService, @@ -1070,7 +1074,7 @@ export default class MainBackground { }; this.systemService = new SystemService( - this.platformUtilsService, + this.clipboardService, this.autofillSettingsService, this.taskSchedulerService, ); @@ -1105,7 +1109,7 @@ export default class MainBackground { this.runtimeBackground = new RuntimeBackground( this, this.autofillService, - this.platformUtilsService as BrowserPlatformUtilsService, + this.clipboardService, this.notificationsService, this.autofillSettingsService, this.processReloadService, @@ -1180,11 +1184,11 @@ export default class MainBackground { ); const contextMenuClickedHandler = new ContextMenuClickedHandler( - (options) => this.platformUtilsService.copyToClipboard(options.text), + (options) => this.clipboardService.copyToClipboard(options.text), async (_tab) => { const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {}; const password = await this.passwordGenerationService.generatePassword(options); - this.platformUtilsService.copyToClipboard(password); + this.clipboardService.copyToClipboard(password); // 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 this.passwordGenerationService.addHistory(password); @@ -1622,7 +1626,7 @@ export default class MainBackground { this.domainSettingsService, this.autofillSettingsService, this.i18nService, - this.platformUtilsService, + this.clipboardService, this.themeStateService, ); } else { @@ -1635,7 +1639,7 @@ export default class MainBackground { this.domainSettingsService, this.autofillSettingsService, this.i18nService, - this.platformUtilsService, + this.clipboardService, this.vaultSettingsService, this.fido2ActiveRequestManager, this.inlineMenuFieldQualificationService, @@ -1663,7 +1667,7 @@ export default class MainBackground { generatePasswordToClipboard = async () => { const password = await this.generatePassword(); - this.platformUtilsService.copyToClipboard(password); + this.clipboardService.copyToClipboard(password); await this.addPasswordToHistory(password); }; diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 38bb2ec50c99..2063413fdc17 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -10,6 +10,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -28,7 +29,6 @@ import { LockedVaultPendingNotificationsData } from "../autofill/background/abst import { AutofillService } from "../autofill/services/abstractions/autofill.service"; import { BrowserApi } from "../platform/browser/browser-api"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; -import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import MainBackground from "./main.background"; @@ -41,7 +41,7 @@ export default class RuntimeBackground { constructor( private main: MainBackground, private autofillService: AutofillService, - private platformUtilsService: BrowserPlatformUtilsService, + private clipboardService: ClipboardService, private notificationsService: NotificationsService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private processReloadSerivce: ProcessReloadServiceAbstraction, @@ -143,7 +143,7 @@ export default class RuntimeBackground { msg.sender === ExtensionCommand.AutofillCommand, ); if (totpCode != null) { - this.platformUtilsService.copyToClipboard(totpCode); + this.clipboardService.copyToClipboard(totpCode); } break; } @@ -351,7 +351,7 @@ export default class RuntimeBackground { }); break; case "getClickedElementResponse": - this.platformUtilsService.copyToClipboard(msg.identifier); + this.clipboardService.copyToClipboard(msg.identifier); break; case "switchAccount": { await this.main.switchAccount(msg.userId); @@ -374,7 +374,7 @@ export default class RuntimeBackground { }); if (totpCode != null) { - this.platformUtilsService.copyToClipboard(totpCode); + this.clipboardService.copyToClipboard(totpCode); } // reset diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts index 67fa920d18de..ead48a1f445d 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts @@ -1,11 +1,11 @@ import { flushPromises, sendMockExtensionMessage } from "../../autofill/spec/testing-utils"; import { BrowserApi } from "../browser/browser-api"; -import BrowserClipboardService from "../services/browser-clipboard.service"; +import BrowserClipboardUtils from "../services/browser-clipboard.utils"; describe("OffscreenDocument", () => { const browserApiMessageListenerSpy = jest.spyOn(BrowserApi, "messageListener"); - const browserClipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy"); - const browserClipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read"); + const browserClipboardUtilsCopySpy = jest.spyOn(BrowserClipboardUtils, "copy"); + const browserClipboardUtilsReadSpy = jest.spyOn(BrowserClipboardUtils, "read"); const consoleErrorSpy = jest.spyOn(console, "error"); // FIXME: Remove when updating file. Eslint update @@ -25,18 +25,18 @@ describe("OffscreenDocument", () => { it("ignores messages that do not have a handler registered with the corresponding command", () => { sendMockExtensionMessage({ command: "notAValidCommand" }); - expect(browserClipboardServiceCopySpy).not.toHaveBeenCalled(); - expect(browserClipboardServiceReadSpy).not.toHaveBeenCalled(); + expect(browserClipboardUtilsCopySpy).not.toHaveBeenCalled(); + expect(browserClipboardUtilsReadSpy).not.toHaveBeenCalled(); }); it("shows a console message if the handler throws an error", async () => { const error = new Error("test error"); - browserClipboardServiceCopySpy.mockRejectedValueOnce(new Error("test error")); + browserClipboardUtilsCopySpy.mockRejectedValueOnce(new Error("test error")); sendMockExtensionMessage({ command: "offscreenCopyToClipboard", text: "test" }); await flushPromises(); - expect(browserClipboardServiceCopySpy).toHaveBeenCalled(); + expect(browserClipboardUtilsCopySpy).toHaveBeenCalled(); expect(consoleErrorSpy).toHaveBeenCalledWith( "Error resolving extension message response", error, @@ -50,7 +50,7 @@ describe("OffscreenDocument", () => { sendMockExtensionMessage({ command: "offscreenCopyToClipboard", text }); await flushPromises(); - expect(browserClipboardServiceCopySpy).toHaveBeenCalledWith(window, text); + expect(browserClipboardUtilsCopySpy).toHaveBeenCalledWith(window, text); }); }); @@ -59,7 +59,7 @@ describe("OffscreenDocument", () => { sendMockExtensionMessage({ command: "offscreenReadFromClipboard" }); await flushPromises(); - expect(browserClipboardServiceReadSpy).toHaveBeenCalledWith(window); + expect(browserClipboardUtilsReadSpy).toHaveBeenCalledWith(window); }); }); }); diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.ts index b1c39d349181..3bd1b3169f32 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.ts @@ -3,7 +3,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { BrowserApi } from "../browser/browser-api"; -import BrowserClipboardService from "../services/browser-clipboard.service"; +import BrowserClipboardUtils from "../services/browser-clipboard.utils"; import { OffscreenDocumentExtensionMessage, @@ -34,14 +34,14 @@ class OffscreenDocument implements OffscreenDocumentInterface { * @param message - The extension message containing the text to copy */ private async handleOffscreenCopyToClipboard(message: OffscreenDocumentExtensionMessage) { - await BrowserClipboardService.copy(self, message.text); + await BrowserClipboardUtils.copy(self, message.text); } /** * Reads the user's clipboard and returns the text. */ private async handleOffscreenReadFromClipboard() { - return await BrowserClipboardService.read(self); + return await BrowserClipboardUtils.read(self); } private handleLocalStorageGet(key: string) { diff --git a/apps/browser/src/platform/services/browser-clipboard.service.spec.ts b/apps/browser/src/platform/services/browser-clipboard.service.spec.ts index cf0d7c460041..416685bd1e5f 100644 --- a/apps/browser/src/platform/services/browser-clipboard.service.spec.ts +++ b/apps/browser/src/platform/services/browser-clipboard.service.spec.ts @@ -1,111 +1,187 @@ -import BrowserClipboardService from "./browser-clipboard.service"; +import { MockProxy, mock } from "jest-mock-extended"; -describe("BrowserClipboardService", () => { - let windowMock: any; - const consoleWarnSpy = jest.spyOn(console, "warn"); +import { DeviceType } from "@bitwarden/common/enums"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { flushPromises } from "../../autofill/spec/testing-utils"; +import { SafariApp } from "../../browser/safariApp"; +import { BrowserApi } from "../browser/browser-api"; +import { OffscreenDocumentService } from "../offscreen-document/abstractions/offscreen-document"; + +import { BrowserClipboardService } from "./browser-clipboard.service"; +import BrowserClipboardUtils from "./browser-clipboard.utils"; + +describe("Browser Clipboard Service", () => { + let platformUtilsService: MockProxy; + let browserClipboardService: BrowserClipboardService; + let offscreenDocumentService: MockProxy; + const clipboardWriteCallbackSpy = jest.fn(); beforeEach(() => { - windowMock = { - navigator: { - clipboard: { - writeText: jest.fn(), - readText: jest.fn(), - }, - }, - document: { - body: { - appendChild: jest.fn((element) => document.body.appendChild(element)), - removeChild: jest.fn((element) => document.body.removeChild(element)), - }, - createElement: jest.fn((tagName) => document.createElement(tagName)), - execCommand: jest.fn(), - queryCommandSupported: jest.fn(), - }, - }; + offscreenDocumentService = mock(); + (window as any).matchMedia = jest.fn().mockReturnValueOnce({}); + platformUtilsService = mock(); + browserClipboardService = new BrowserClipboardService( + platformUtilsService, + clipboardWriteCallbackSpy, + window, + offscreenDocumentService, + ); }); - describe("copy", () => { - it("uses the legacy copy method if the clipboard API is not available", async () => { + describe("copyToClipboard", () => { + const getManifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); + const sendMessageToAppSpy = jest.spyOn(SafariApp, "sendMessageToApp"); + const clipboardServiceCopySpy = jest.spyOn(BrowserClipboardUtils, "copy"); + let triggerOffscreenCopyToClipboardSpy: jest.SpyInstance; + + beforeEach(() => { + getManifestVersionSpy.mockReturnValue(2); + triggerOffscreenCopyToClipboardSpy = jest.spyOn( + browserClipboardService as any, + "triggerOffscreenCopyToClipboard", + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("sends a copy to clipboard message to the desktop application if a user is using the safari browser", async () => { const text = "test"; - windowMock.navigator.clipboard = {}; - windowMock.document.queryCommandSupported.mockReturnValue(true); + const clearMs = 1000; + sendMessageToAppSpy.mockResolvedValueOnce("success"); + jest.spyOn(platformUtilsService, "getDevice").mockReturnValue(DeviceType.SafariExtension); - await BrowserClipboardService.copy(windowMock as Window, text); + browserClipboardService.copyToClipboard(text, { clearMs }); + await flushPromises(); - expect(windowMock.document.execCommand).toHaveBeenCalledWith("copy"); + expect(sendMessageToAppSpy).toHaveBeenCalledWith("copyToClipboard", text); + expect(clipboardWriteCallbackSpy).toHaveBeenCalledWith(text, clearMs); + expect(clipboardServiceCopySpy).not.toHaveBeenCalled(); + expect(triggerOffscreenCopyToClipboardSpy).not.toHaveBeenCalled(); }); - it("uses the legacy copy method if the clipboard API throws an error", async () => { - windowMock.document.queryCommandSupported.mockReturnValue(true); - windowMock.navigator.clipboard.writeText.mockRejectedValue(new Error("test")); + it("sets the copied text to a unicode placeholder when the user is using Chrome if the passed text is an empty string", async () => { + const text = ""; + jest.spyOn(platformUtilsService, "getDevice").mockReturnValue(DeviceType.ChromeExtension); - await BrowserClipboardService.copy(windowMock as Window, "test"); + browserClipboardService.copyToClipboard(text); + await flushPromises(); - expect(windowMock.document.execCommand).toHaveBeenCalledWith("copy"); + expect(clipboardServiceCopySpy).toHaveBeenCalledWith(window, "\u0000"); }); - it("copies the given text to the clipboard", async () => { + it("copies the passed text using the BrowserClipboardUtils", async () => { const text = "test"; + jest.spyOn(platformUtilsService, "getDevice").mockReturnValue(DeviceType.ChromeExtension); - await BrowserClipboardService.copy(windowMock as Window, text); + browserClipboardService.copyToClipboard(text, { window: self }); + await flushPromises(); - expect(windowMock.navigator.clipboard.writeText).toHaveBeenCalledWith(text); + expect(clipboardServiceCopySpy).toHaveBeenCalledWith(self, text); + expect(triggerOffscreenCopyToClipboardSpy).not.toHaveBeenCalled(); }); - it("prints an warning message to the console if both the clipboard api and legacy method throw an error", async () => { - windowMock.document.queryCommandSupported.mockReturnValue(true); - windowMock.navigator.clipboard.writeText.mockRejectedValue(new Error("test")); - windowMock.document.execCommand.mockImplementation(() => { - throw new Error("test"); + it("copies the passed text using the offscreen document if the extension is using manifest v3", async () => { + const text = "test"; + offscreenDocumentService.offscreenApiSupported.mockReturnValue(true); + getManifestVersionSpy.mockReturnValue(3); + + browserClipboardService.copyToClipboard(text); + await flushPromises(); + + expect(triggerOffscreenCopyToClipboardSpy).toHaveBeenCalledWith(text); + expect(clipboardServiceCopySpy).not.toHaveBeenCalled(); + expect(offscreenDocumentService.withDocument).toHaveBeenCalledWith( + [chrome.offscreen.Reason.CLIPBOARD], + "Write text to the clipboard.", + expect.any(Function), + ); + + const callback = offscreenDocumentService.withDocument.mock.calls[0][2]; + await callback(); + expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenCopyToClipboard", { + text, }); + }); + + it("skips the clipboardWriteCallback if the clipboard is clearing", async () => { + jest.spyOn(platformUtilsService, "getDevice").mockReturnValue(DeviceType.ChromeExtension); - await BrowserClipboardService.copy(windowMock as Window, ""); + browserClipboardService.copyToClipboard("test", { window: self, clearing: true }); + await flushPromises(); - expect(consoleWarnSpy).toHaveBeenCalled(); + expect(clipboardWriteCallbackSpy).not.toHaveBeenCalled(); }); }); - describe("read", () => { - it("uses the legacy read method if the clipboard API is not available", async () => { - const testValue = "test"; - windowMock.navigator.clipboard = {}; - windowMock.document.queryCommandSupported.mockReturnValue(true); - windowMock.document.execCommand.mockImplementation(() => { - document.querySelector("textarea").value = testValue; - return true; - }); + describe("readFromClipboard", () => { + const getManifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); + const sendMessageToAppSpy = jest.spyOn(SafariApp, "sendMessageToApp"); + const clipboardServiceReadSpy = jest.spyOn(BrowserClipboardUtils, "read"); - const returnValue = await BrowserClipboardService.read(windowMock as Window); + beforeEach(() => { + getManifestVersionSpy.mockReturnValue(2); + }); - expect(windowMock.document.execCommand).toHaveBeenCalledWith("paste"); - expect(returnValue).toBe(testValue); + afterEach(() => { + jest.clearAllMocks(); }); - it("uses the legacy read method if the clipboard API throws an error", async () => { - windowMock.document.queryCommandSupported.mockReturnValue(true); - windowMock.navigator.clipboard.readText.mockRejectedValue(new Error("test")); + it("sends a ready from clipboard message to the desktop application if a user is using the safari browser", async () => { + sendMessageToAppSpy.mockResolvedValueOnce("test"); + jest.spyOn(platformUtilsService, "getDevice").mockReturnValue(DeviceType.SafariExtension); - await BrowserClipboardService.read(windowMock as Window); + const result = await browserClipboardService.readFromClipboard(); - expect(windowMock.document.execCommand).toHaveBeenCalledWith("paste"); + expect(sendMessageToAppSpy).toHaveBeenCalledWith("readFromClipboard"); + expect(clipboardServiceReadSpy).not.toHaveBeenCalled(); + expect(result).toBe("test"); }); - it("reads the text from the clipboard", async () => { - await BrowserClipboardService.read(windowMock as Window); + it("reads text from the clipboard using the ClipboardService", async () => { + jest.spyOn(platformUtilsService, "getDevice").mockReturnValue(DeviceType.ChromeExtension); + clipboardServiceReadSpy.mockResolvedValueOnce("test"); + + const result = await browserClipboardService.readFromClipboard({ window: self }); - expect(windowMock.navigator.clipboard.readText).toHaveBeenCalled(); + expect(clipboardServiceReadSpy).toHaveBeenCalledWith(self); + expect(sendMessageToAppSpy).not.toHaveBeenCalled(); + expect(result).toBe("test"); }); - it("prints a warning message to the console if both the clipboard api and legacy method throw an error", async () => { - windowMock.document.queryCommandSupported.mockReturnValue(true); - windowMock.navigator.clipboard.readText.mockRejectedValue(new Error("test")); - windowMock.document.execCommand.mockImplementation(() => { - throw new Error("test"); - }); + it("reads the clipboard text using the offscreen document", async () => { + offscreenDocumentService.offscreenApiSupported.mockReturnValue(true); + getManifestVersionSpy.mockReturnValue(3); + offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) => + Promise.resolve("test"), + ); + + await browserClipboardService.readFromClipboard(); + + expect(offscreenDocumentService.withDocument).toHaveBeenCalledWith( + [chrome.offscreen.Reason.CLIPBOARD], + "Read text from the clipboard.", + expect.any(Function), + ); + + const callback = offscreenDocumentService.withDocument.mock.calls[0][2]; + await callback(); + expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenReadFromClipboard"); + }); + + it("returns an empty string from the offscreen document if the response is not of type string", async () => { + jest.spyOn(platformUtilsService, "getDevice").mockReturnValue(DeviceType.ChromeExtension); + getManifestVersionSpy.mockReturnValue(3); + jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue(1); + offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) => + Promise.resolve(1), + ); - await BrowserClipboardService.read(windowMock as Window); + const result = await browserClipboardService.readFromClipboard(); - expect(consoleWarnSpy).toHaveBeenCalled(); + expect(result).toBe(""); }); }); }); diff --git a/apps/browser/src/platform/services/browser-clipboard.service.ts b/apps/browser/src/platform/services/browser-clipboard.service.ts index 8fb5a6d124a6..dfca96585d67 100644 --- a/apps/browser/src/platform/services/browser-clipboard.service.ts +++ b/apps/browser/src/platform/services/browser-clipboard.service.ts @@ -1,130 +1,117 @@ -import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; - -class BrowserClipboardService { - private static consoleLogService: ConsoleLogService = new ConsoleLogService(false); +import { + ClipboardOptions, + ClipboardService, +} from "@bitwarden/common/platform/abstractions/clipboard.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { SafariApp } from "../../browser/safariApp"; +import { BrowserApi } from "../browser/browser-api"; +import { OffscreenDocumentService } from "../offscreen-document/abstractions/offscreen-document"; + +import BrowserClipboardUtils from "./browser-clipboard.utils"; + +export class BrowserClipboardService implements ClipboardService { + constructor( + private platformUtilsService: PlatformUtilsService, + private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, + private globalContext: Window | ServiceWorkerGlobalScope, + private offscreenDocumentService: OffscreenDocumentService, + ) {} /** - * Copies the given text to the user's clipboard. + * Copies the passed text to the clipboard. For Safari, this will use + * the native messaging API to send the text to the Bitwarden app. If + * the extension is using manifest v3, the offscreen document API will + * be used to copy the text to the clipboard. Otherwise, the browser's + * clipboard API will be used. * - * @param globalContext - The global window context. - * @param text - The text to copy. + * @param text - The text to copy to the clipboard. + * @param options - Options for the clipboard operation. */ - static async copy(globalContext: Window, text: string) { - if (!BrowserClipboardService.isClipboardApiSupported(globalContext, "writeText")) { - this.useLegacyCopyMethod(globalContext, text); - return; - } - - try { - await globalContext.navigator.clipboard.writeText(text); - } catch (error) { - BrowserClipboardService.consoleLogService.debug( - `Error copying to clipboard using the clipboard API, attempting legacy method: ${error}`, - ); + copyToClipboard(text: string, options?: ClipboardOptions): void { + const windowContext = options?.window || (this.globalContext as Window); + const clearing = Boolean(options?.clearing); + const clearMs: number = options?.clearMs || null; + const handleClipboardWriteCallback = () => { + if (!clearing && this.clipboardWriteCallback != null) { + this.clipboardWriteCallback(text, clearMs); + } + }; + + if (this.platformUtilsService.isSafari()) { + void SafariApp.sendMessageToApp("copyToClipboard", text).then(handleClipboardWriteCallback); - this.useLegacyCopyMethod(globalContext, text); + return; } - } - /** - * Reads the user's clipboard and returns the text. - * - * @param globalContext - The global window context. - */ - static async read(globalContext: Window): Promise { - if (!BrowserClipboardService.isClipboardApiSupported(globalContext, "readText")) { - return this.useLegacyReadMethod(globalContext); + if (this.platformUtilsService.isChrome() && text === "") { + text = "\u0000"; } - try { - return await globalContext.navigator.clipboard.readText(); - } catch (error) { - BrowserClipboardService.consoleLogService.debug( - `Error reading from clipboard using the clipboard API, attempting legacy method: ${error}`, - ); + if (BrowserApi.isManifestVersion(3) && this.offscreenDocumentService.offscreenApiSupported()) { + void this.triggerOffscreenCopyToClipboard(text).then(handleClipboardWriteCallback); - return this.useLegacyReadMethod(globalContext); - } - } - - /** - * Copies the given text to the user's clipboard using the legacy `execCommand` method. This - * method is used as a fallback when the clipboard API is not supported or fails. - * - * @param globalContext - The global window context. - * @param text - The text to copy. - */ - private static useLegacyCopyMethod(globalContext: Window, text: string) { - if (!BrowserClipboardService.isLegacyClipboardMethodSupported(globalContext, "copy")) { - BrowserClipboardService.consoleLogService.warning("Legacy copy method not supported"); return; } - const textareaElement = globalContext.document.createElement("textarea"); - textareaElement.textContent = !text ? " " : text; - textareaElement.style.position = "fixed"; - globalContext.document.body.appendChild(textareaElement); - textareaElement.select(); - - try { - globalContext.document.execCommand("copy"); - } catch (error) { - BrowserClipboardService.consoleLogService.warning(`Error writing to clipboard: ${error}`); - } finally { - globalContext.document.body.removeChild(textareaElement); - } + void BrowserClipboardUtils.copy(windowContext, text).then(handleClipboardWriteCallback); } /** - * Reads the user's clipboard using the legacy `execCommand` method. This method is used as a - * fallback when the clipboard API is not supported or fails. + * Reads the text from the clipboard. For Safari, this will use the + * native messaging API to request the text from the Bitwarden app. If + * the extension is using manifest v3, the offscreen document API will + * be used to read the text from the clipboard. Otherwise, the browser's + * clipboard API will be used. * - * @param globalContext - The global window context. + * @param options - Options for the clipboard operation. */ - private static useLegacyReadMethod(globalContext: Window): string { - if (!BrowserClipboardService.isLegacyClipboardMethodSupported(globalContext, "paste")) { - BrowserClipboardService.consoleLogService.warning("Legacy paste method not supported"); - return ""; + async readFromClipboard(options?: ClipboardOptions): Promise { + const windowContext = options?.window || (this.globalContext as Window); + + if (this.platformUtilsService.isSafari()) { + return await SafariApp.sendMessageToApp("readFromClipboard"); } - const textareaElement = globalContext.document.createElement("textarea"); - textareaElement.style.position = "fixed"; - globalContext.document.body.appendChild(textareaElement); - textareaElement.focus(); - - try { - return globalContext.document.execCommand("paste") ? textareaElement.value : ""; - } catch (error) { - BrowserClipboardService.consoleLogService.warning(`Error reading from clipboard: ${error}`); - } finally { - globalContext.document.body.removeChild(textareaElement); + if (BrowserApi.isManifestVersion(3) && this.offscreenDocumentService.offscreenApiSupported()) { + return await this.triggerOffscreenReadFromClipboard(); } - return ""; + return await BrowserClipboardUtils.read(windowContext); + } + + clearClipboard(clipboardValue: string, timeoutMs?: number): Promise { + throw new Error("Method not implemented."); } /** - * Checks if the clipboard API is supported in the current environment. - * - * @param globalContext - The global window context. - * @param method - The clipboard API method to check for support. + * Triggers the offscreen document API to copy the text to the clipboard. */ - private static isClipboardApiSupported(globalContext: Window, method: "writeText" | "readText") { - return "clipboard" in globalContext.navigator && method in globalContext.navigator.clipboard; + private async triggerOffscreenCopyToClipboard(text: string) { + await this.offscreenDocumentService.withDocument( + [chrome.offscreen.Reason.CLIPBOARD], + "Write text to the clipboard.", + async () => { + await BrowserApi.sendMessageWithResponse("offscreenCopyToClipboard", { text }); + }, + ); } /** - * Checks if the legacy clipboard method is supported in the current environment. - * - * @param globalContext - The global window context. - * @param method - The legacy clipboard method to check for support. + * Triggers the offscreen document API to read the text from the clipboard. */ - private static isLegacyClipboardMethodSupported(globalContext: Window, method: "copy" | "paste") { - return ( - "queryCommandSupported" in globalContext.document && - globalContext.document.queryCommandSupported(method) + private async triggerOffscreenReadFromClipboard() { + const response = await this.offscreenDocumentService.withDocument( + [chrome.offscreen.Reason.CLIPBOARD], + "Read text from the clipboard.", + async () => { + return await BrowserApi.sendMessageWithResponse("offscreenReadFromClipboard"); + }, ); + if (typeof response === "string") { + return response; + } + + return ""; } } - -export default BrowserClipboardService; diff --git a/apps/browser/src/platform/services/browser-clipboard.utils.spec.ts b/apps/browser/src/platform/services/browser-clipboard.utils.spec.ts new file mode 100644 index 000000000000..2aad175f8226 --- /dev/null +++ b/apps/browser/src/platform/services/browser-clipboard.utils.spec.ts @@ -0,0 +1,111 @@ +import BrowserClipboardUtils from "./browser-clipboard.utils"; + +describe("BrowserClipboardUtils", () => { + let windowMock: any; + const consoleWarnSpy = jest.spyOn(console, "warn"); + + beforeEach(() => { + windowMock = { + navigator: { + clipboard: { + writeText: jest.fn(), + readText: jest.fn(), + }, + }, + document: { + body: { + appendChild: jest.fn((element) => document.body.appendChild(element)), + removeChild: jest.fn((element) => document.body.removeChild(element)), + }, + createElement: jest.fn((tagName) => document.createElement(tagName)), + execCommand: jest.fn(), + queryCommandSupported: jest.fn(), + }, + }; + }); + + describe("copy", () => { + it("uses the legacy copy method if the clipboard API is not available", async () => { + const text = "test"; + windowMock.navigator.clipboard = {}; + windowMock.document.queryCommandSupported.mockReturnValue(true); + + await BrowserClipboardUtils.copy(windowMock as Window, text); + + expect(windowMock.document.execCommand).toHaveBeenCalledWith("copy"); + }); + + it("uses the legacy copy method if the clipboard API throws an error", async () => { + windowMock.document.queryCommandSupported.mockReturnValue(true); + windowMock.navigator.clipboard.writeText.mockRejectedValue(new Error("test")); + + await BrowserClipboardUtils.copy(windowMock as Window, "test"); + + expect(windowMock.document.execCommand).toHaveBeenCalledWith("copy"); + }); + + it("copies the given text to the clipboard", async () => { + const text = "test"; + + await BrowserClipboardUtils.copy(windowMock as Window, text); + + expect(windowMock.navigator.clipboard.writeText).toHaveBeenCalledWith(text); + }); + + it("prints an warning message to the console if both the clipboard api and legacy method throw an error", async () => { + windowMock.document.queryCommandSupported.mockReturnValue(true); + windowMock.navigator.clipboard.writeText.mockRejectedValue(new Error("test")); + windowMock.document.execCommand.mockImplementation(() => { + throw new Error("test"); + }); + + await BrowserClipboardUtils.copy(windowMock as Window, ""); + + expect(consoleWarnSpy).toHaveBeenCalled(); + }); + }); + + describe("read", () => { + it("uses the legacy read method if the clipboard API is not available", async () => { + const testValue = "test"; + windowMock.navigator.clipboard = {}; + windowMock.document.queryCommandSupported.mockReturnValue(true); + windowMock.document.execCommand.mockImplementation(() => { + document.querySelector("textarea").value = testValue; + return true; + }); + + const returnValue = await BrowserClipboardUtils.read(windowMock as Window); + + expect(windowMock.document.execCommand).toHaveBeenCalledWith("paste"); + expect(returnValue).toBe(testValue); + }); + + it("uses the legacy read method if the clipboard API throws an error", async () => { + windowMock.document.queryCommandSupported.mockReturnValue(true); + windowMock.navigator.clipboard.readText.mockRejectedValue(new Error("test")); + + await BrowserClipboardUtils.read(windowMock as Window); + + expect(windowMock.document.execCommand).toHaveBeenCalledWith("paste"); + }); + + it("reads the text from the clipboard", async () => { + await BrowserClipboardUtils.read(windowMock as Window); + + expect(windowMock.navigator.clipboard.readText).toHaveBeenCalled(); + }); + + it("prints a warning message to the console if both the clipboard api and legacy method throw an error", async () => { + windowMock.document.queryCommandSupported.mockReturnValue(true); + windowMock.navigator.clipboard.readText.mockRejectedValue(new Error("test")); + windowMock.document.execCommand.mockImplementation(() => { + throw new Error("test"); + }); + + await BrowserClipboardUtils.read(windowMock as Window); + + expect(consoleWarnSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/platform/services/browser-clipboard.utils.ts b/apps/browser/src/platform/services/browser-clipboard.utils.ts new file mode 100644 index 000000000000..23e6ace802d1 --- /dev/null +++ b/apps/browser/src/platform/services/browser-clipboard.utils.ts @@ -0,0 +1,130 @@ +import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; + +class BrowserClipboardUtils { + private static consoleLogService: ConsoleLogService = new ConsoleLogService(false); + + /** + * Copies the given text to the user's clipboard. + * + * @param globalContext - The global window context. + * @param text - The text to copy. + */ + static async copy(globalContext: Window, text: string) { + if (!BrowserClipboardUtils.isClipboardApiSupported(globalContext, "writeText")) { + this.useLegacyCopyMethod(globalContext, text); + return; + } + + try { + await globalContext.navigator.clipboard.writeText(text); + } catch (error) { + BrowserClipboardUtils.consoleLogService.debug( + `Error copying to clipboard using the clipboard API, attempting legacy method: ${error}`, + ); + + this.useLegacyCopyMethod(globalContext, text); + } + } + + /** + * Reads the user's clipboard and returns the text. + * + * @param globalContext - The global window context. + */ + static async read(globalContext: Window): Promise { + if (!BrowserClipboardUtils.isClipboardApiSupported(globalContext, "readText")) { + return this.useLegacyReadMethod(globalContext); + } + + try { + return await globalContext.navigator.clipboard.readText(); + } catch (error) { + BrowserClipboardUtils.consoleLogService.debug( + `Error reading from clipboard using the clipboard API, attempting legacy method: ${error}`, + ); + + return this.useLegacyReadMethod(globalContext); + } + } + + /** + * Copies the given text to the user's clipboard using the legacy `execCommand` method. This + * method is used as a fallback when the clipboard API is not supported or fails. + * + * @param globalContext - The global window context. + * @param text - The text to copy. + */ + private static useLegacyCopyMethod(globalContext: Window, text: string) { + if (!BrowserClipboardUtils.isLegacyClipboardMethodSupported(globalContext, "copy")) { + BrowserClipboardUtils.consoleLogService.warning("Legacy copy method not supported"); + return; + } + + const textareaElement = globalContext.document.createElement("textarea"); + textareaElement.textContent = !text ? " " : text; + textareaElement.style.position = "fixed"; + globalContext.document.body.appendChild(textareaElement); + textareaElement.select(); + + try { + globalContext.document.execCommand("copy"); + } catch (error) { + BrowserClipboardUtils.consoleLogService.warning(`Error writing to clipboard: ${error}`); + } finally { + globalContext.document.body.removeChild(textareaElement); + } + } + + /** + * Reads the user's clipboard using the legacy `execCommand` method. This method is used as a + * fallback when the clipboard API is not supported or fails. + * + * @param globalContext - The global window context. + */ + private static useLegacyReadMethod(globalContext: Window): string { + if (!BrowserClipboardUtils.isLegacyClipboardMethodSupported(globalContext, "paste")) { + BrowserClipboardUtils.consoleLogService.warning("Legacy paste method not supported"); + return ""; + } + + const textareaElement = globalContext.document.createElement("textarea"); + textareaElement.style.position = "fixed"; + globalContext.document.body.appendChild(textareaElement); + textareaElement.focus(); + + try { + return globalContext.document.execCommand("paste") ? textareaElement.value : ""; + } catch (error) { + BrowserClipboardUtils.consoleLogService.warning(`Error reading from clipboard: ${error}`); + } finally { + globalContext.document.body.removeChild(textareaElement); + } + + return ""; + } + + /** + * Checks if the clipboard API is supported in the current environment. + * + * @param globalContext - The global window context. + * @param method - The clipboard API method to check for support. + */ + private static isClipboardApiSupported(globalContext: Window, method: "writeText" | "readText") { + return "clipboard" in globalContext.navigator && method in globalContext.navigator.clipboard; + } + + /** + * Checks if the legacy clipboard method is supported in the current environment. + * + * @param globalContext - The global window context. + * @param method - The legacy clipboard method to check for support. + */ + private static isLegacyClipboardMethodSupported(globalContext: Window, method: "copy" | "paste") { + return ( + "queryCommandSupported" in globalContext.document && + globalContext.document.queryCommandSupported(method) + ); + } +} + +export default BrowserClipboardUtils; diff --git a/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts index da6a8faf3e89..44f80418141b 100644 --- a/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts @@ -1,17 +1,13 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document"; - import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService { constructor( private messagingService: MessagingService, - clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, win: Window & typeof globalThis, - offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardWriteCallback, win, offscreenDocumentService); + super(win); } override showToast( diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts index 762380071b78..91aa2e315d3d 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts @@ -1,22 +1,12 @@ -import { MockProxy, mock } from "jest-mock-extended"; - import { DeviceType } from "@bitwarden/common/enums"; -import { flushPromises } from "../../../autofill/spec/testing-utils"; -import { SafariApp } from "../../../browser/safariApp"; import { BrowserApi } from "../../browser/browser-api"; -import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document"; -import BrowserClipboardService from "../browser-clipboard.service"; import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService { - constructor( - clipboardSpy: jest.Mock, - win: Window & typeof globalThis, - offscreenDocumentService: OffscreenDocumentService, - ) { - super(clipboardSpy, win, offscreenDocumentService); + constructor(win: Window & typeof globalThis) { + super(win); } showToast( @@ -31,17 +21,10 @@ class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService { describe("Browser Utils Service", () => { let browserPlatformUtilsService: BrowserPlatformUtilsService; - let offscreenDocumentService: MockProxy; - const clipboardWriteCallbackSpy = jest.fn(); beforeEach(() => { - offscreenDocumentService = mock(); (window as any).matchMedia = jest.fn().mockReturnValueOnce({}); - browserPlatformUtilsService = new TestBrowserPlatformUtilsService( - clipboardWriteCallbackSpy, - window, - offscreenDocumentService, - ); + browserPlatformUtilsService = new TestBrowserPlatformUtilsService(window); }); describe("getBrowser", () => { @@ -166,176 +149,6 @@ describe("Browser Utils Service", () => { expect(isViewOpen).toBe(true); }); }); - - describe("copyToClipboard", () => { - const getManifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); - const sendMessageToAppSpy = jest.spyOn(SafariApp, "sendMessageToApp"); - const clipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy"); - let triggerOffscreenCopyToClipboardSpy: jest.SpyInstance; - - beforeEach(() => { - getManifestVersionSpy.mockReturnValue(2); - triggerOffscreenCopyToClipboardSpy = jest.spyOn( - browserPlatformUtilsService as any, - "triggerOffscreenCopyToClipboard", - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it("sends a copy to clipboard message to the desktop application if a user is using the safari browser", async () => { - const text = "test"; - const clearMs = 1000; - sendMessageToAppSpy.mockResolvedValueOnce("success"); - jest - .spyOn(browserPlatformUtilsService, "getDevice") - .mockReturnValue(DeviceType.SafariExtension); - - browserPlatformUtilsService.copyToClipboard(text, { clearMs }); - await flushPromises(); - - expect(sendMessageToAppSpy).toHaveBeenCalledWith("copyToClipboard", text); - expect(clipboardWriteCallbackSpy).toHaveBeenCalledWith(text, clearMs); - expect(clipboardServiceCopySpy).not.toHaveBeenCalled(); - expect(triggerOffscreenCopyToClipboardSpy).not.toHaveBeenCalled(); - }); - - it("sets the copied text to a unicode placeholder when the user is using Chrome if the passed text is an empty string", async () => { - const text = ""; - jest - .spyOn(browserPlatformUtilsService, "getDevice") - .mockReturnValue(DeviceType.ChromeExtension); - - browserPlatformUtilsService.copyToClipboard(text); - await flushPromises(); - - expect(clipboardServiceCopySpy).toHaveBeenCalledWith(window, "\u0000"); - }); - - it("copies the passed text using the BrowserClipboardService", async () => { - const text = "test"; - jest - .spyOn(browserPlatformUtilsService, "getDevice") - .mockReturnValue(DeviceType.ChromeExtension); - - browserPlatformUtilsService.copyToClipboard(text, { window: self }); - await flushPromises(); - - expect(clipboardServiceCopySpy).toHaveBeenCalledWith(self, text); - expect(triggerOffscreenCopyToClipboardSpy).not.toHaveBeenCalled(); - }); - - it("copies the passed text using the offscreen document if the extension is using manifest v3", async () => { - const text = "test"; - offscreenDocumentService.offscreenApiSupported.mockReturnValue(true); - getManifestVersionSpy.mockReturnValue(3); - - browserPlatformUtilsService.copyToClipboard(text); - await flushPromises(); - - expect(triggerOffscreenCopyToClipboardSpy).toHaveBeenCalledWith(text); - expect(clipboardServiceCopySpy).not.toHaveBeenCalled(); - expect(offscreenDocumentService.withDocument).toHaveBeenCalledWith( - [chrome.offscreen.Reason.CLIPBOARD], - "Write text to the clipboard.", - expect.any(Function), - ); - - const callback = offscreenDocumentService.withDocument.mock.calls[0][2]; - await callback(); - expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenCopyToClipboard", { - text, - }); - }); - - it("skips the clipboardWriteCallback if the clipboard is clearing", async () => { - jest - .spyOn(browserPlatformUtilsService, "getDevice") - .mockReturnValue(DeviceType.ChromeExtension); - - browserPlatformUtilsService.copyToClipboard("test", { window: self, clearing: true }); - await flushPromises(); - - expect(clipboardWriteCallbackSpy).not.toHaveBeenCalled(); - }); - }); - - describe("readFromClipboard", () => { - const getManifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); - const sendMessageToAppSpy = jest.spyOn(SafariApp, "sendMessageToApp"); - const clipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read"); - - beforeEach(() => { - getManifestVersionSpy.mockReturnValue(2); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it("sends a ready from clipboard message to the desktop application if a user is using the safari browser", async () => { - sendMessageToAppSpy.mockResolvedValueOnce("test"); - jest - .spyOn(browserPlatformUtilsService, "getDevice") - .mockReturnValue(DeviceType.SafariExtension); - - const result = await browserPlatformUtilsService.readFromClipboard(); - - expect(sendMessageToAppSpy).toHaveBeenCalledWith("readFromClipboard"); - expect(clipboardServiceReadSpy).not.toHaveBeenCalled(); - expect(result).toBe("test"); - }); - - it("reads text from the clipboard using the ClipboardService", async () => { - jest - .spyOn(browserPlatformUtilsService, "getDevice") - .mockReturnValue(DeviceType.ChromeExtension); - clipboardServiceReadSpy.mockResolvedValueOnce("test"); - - const result = await browserPlatformUtilsService.readFromClipboard({ window: self }); - - expect(clipboardServiceReadSpy).toHaveBeenCalledWith(self); - expect(sendMessageToAppSpy).not.toHaveBeenCalled(); - expect(result).toBe("test"); - }); - - it("reads the clipboard text using the offscreen document", async () => { - offscreenDocumentService.offscreenApiSupported.mockReturnValue(true); - getManifestVersionSpy.mockReturnValue(3); - offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) => - Promise.resolve("test"), - ); - - await browserPlatformUtilsService.readFromClipboard(); - - expect(offscreenDocumentService.withDocument).toHaveBeenCalledWith( - [chrome.offscreen.Reason.CLIPBOARD], - "Read text from the clipboard.", - expect.any(Function), - ); - - const callback = offscreenDocumentService.withDocument.mock.calls[0][2]; - await callback(); - expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenReadFromClipboard"); - }); - - it("returns an empty string from the offscreen document if the response is not of type string", async () => { - jest - .spyOn(browserPlatformUtilsService, "getDevice") - .mockReturnValue(DeviceType.ChromeExtension); - getManifestVersionSpy.mockReturnValue(3); - jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue(1); - offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) => - Promise.resolve(1), - ); - - const result = await browserPlatformUtilsService.readFromClipboard(); - - expect(result).toBe(""); - }); - }); }); describe("Safari Height Fix", () => { diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts index 3679b2731e36..f5227a9fc8a5 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts @@ -2,24 +2,14 @@ // @ts-strict-ignore import { ExtensionCommand } from "@bitwarden/common/autofill/constants"; import { ClientType, DeviceType } from "@bitwarden/common/enums"; -import { - ClipboardOptions, - PlatformUtilsService, -} from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SafariApp } from "../../../browser/safariApp"; import { BrowserApi } from "../../browser/browser-api"; -import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document"; -import BrowserClipboardService from "../browser-clipboard.service"; export abstract class BrowserPlatformUtilsService implements PlatformUtilsService { private static deviceCache: DeviceType = null; - constructor( - private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, - private globalContext: Window | ServiceWorkerGlobalScope, - private offscreenDocumentService: OffscreenDocumentService, - ) {} + constructor(private globalContext: Window | ServiceWorkerGlobalScope) {} static getDevice(globalContext: Window | ServiceWorkerGlobalScope): DeviceType { if (this.deviceCache) { @@ -215,68 +205,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic return false; } - /** - * Copies the passed text to the clipboard. For Safari, this will use - * the native messaging API to send the text to the Bitwarden app. If - * the extension is using manifest v3, the offscreen document API will - * be used to copy the text to the clipboard. Otherwise, the browser's - * clipboard API will be used. - * - * @param text - The text to copy to the clipboard. - * @param options - Options for the clipboard operation. - */ - copyToClipboard(text: string, options?: ClipboardOptions): void { - const windowContext = options?.window || (this.globalContext as Window); - const clearing = Boolean(options?.clearing); - const clearMs: number = options?.clearMs || null; - const handleClipboardWriteCallback = () => { - if (!clearing && this.clipboardWriteCallback != null) { - this.clipboardWriteCallback(text, clearMs); - } - }; - - if (this.isSafari()) { - void SafariApp.sendMessageToApp("copyToClipboard", text).then(handleClipboardWriteCallback); - - return; - } - - if (this.isChrome() && text === "") { - text = "\u0000"; - } - - if (BrowserApi.isManifestVersion(3) && this.offscreenDocumentService.offscreenApiSupported()) { - void this.triggerOffscreenCopyToClipboard(text).then(handleClipboardWriteCallback); - - return; - } - - void BrowserClipboardService.copy(windowContext, text).then(handleClipboardWriteCallback); - } - - /** - * Reads the text from the clipboard. For Safari, this will use the - * native messaging API to request the text from the Bitwarden app. If - * the extension is using manifest v3, the offscreen document API will - * be used to read the text from the clipboard. Otherwise, the browser's - * clipboard API will be used. - * - * @param options - Options for the clipboard operation. - */ - async readFromClipboard(options?: ClipboardOptions): Promise { - const windowContext = options?.window || (this.globalContext as Window); - - if (this.isSafari()) { - return await SafariApp.sendMessageToApp("readFromClipboard"); - } - - if (BrowserApi.isManifestVersion(3) && this.offscreenDocumentService.offscreenApiSupported()) { - return await this.triggerOffscreenReadFromClipboard(); - } - - return await BrowserClipboardService.read(windowContext); - } - supportsSecureStorage(): boolean { return false; } @@ -309,35 +237,4 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic } return autofillCommand; } - - /** - * Triggers the offscreen document API to copy the text to the clipboard. - */ - private async triggerOffscreenCopyToClipboard(text: string) { - await this.offscreenDocumentService.withDocument( - [chrome.offscreen.Reason.CLIPBOARD], - "Write text to the clipboard.", - async () => { - await BrowserApi.sendMessageWithResponse("offscreenCopyToClipboard", { text }); - }, - ); - } - - /** - * Triggers the offscreen document API to read the text from the clipboard. - */ - private async triggerOffscreenReadFromClipboard() { - const response = await this.offscreenDocumentService.withDocument( - [chrome.offscreen.Reason.CLIPBOARD], - "Read text from the clipboard.", - async () => { - return await BrowserApi.sendMessageWithResponse("offscreenReadFromClipboard"); - }, - ); - if (typeof response === "string") { - return response; - } - - return ""; - } } diff --git a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts index 5b4b7288d197..a2431c827cbb 100644 --- a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts @@ -1,17 +1,13 @@ import { ToastService } from "@bitwarden/components"; -import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document"; - import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService { constructor( private toastService: ToastService, - clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, win: Window & typeof globalThis, - offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardWriteCallback, win, offscreenDocumentService); + super(win); } override showToast( diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 24d82ab8b67d..59704b3af1d1 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -59,6 +59,7 @@ import { AnimationControlService, DefaultAnimationControlService, } from "@bitwarden/common/platform/abstractions/animation-control.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -138,6 +139,7 @@ import { PopupCompactModeService } from "../../platform/popup/layout/popup-compa import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { PopupViewCacheService } from "../../platform/popup/view-cache/popup-view-cache.service"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; +import { BrowserClipboardService } from "../../platform/services/browser-clipboard.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import BrowserMemoryStorageService from "../../platform/services/browser-memory-storage.service"; @@ -275,12 +277,19 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: PlatformUtilsService, + useFactory: (toastService: ToastService) => { + return new ForegroundPlatformUtilsService(toastService, window); + }, + deps: [ToastService], + }), + safeProvider({ + provide: ClipboardService, useFactory: ( - toastService: ToastService, + platformUtilsService: PlatformUtilsService, offscreenDocumentService: OffscreenDocumentService, ) => { - return new ForegroundPlatformUtilsService( - toastService, + return new BrowserClipboardService( + platformUtilsService, (clipboardValue: string, clearMs: number) => { void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); }, @@ -288,7 +297,7 @@ const safeProviders: SafeProvider[] = [ offscreenDocumentService, ); }, - deps: [ToastService, OffscreenDocumentService], + deps: [PlatformUtilsService, OffscreenDocumentService], }), safeProvider({ provide: BiometricsService, diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts index 1a3df238543d..a802636f07e7 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts @@ -6,6 +6,7 @@ import { MockProxy, mock } from "jest-mock-extended"; import { BehaviorSubject, of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -27,7 +28,7 @@ import { SendCreatedComponent } from "./send-created.component"; describe("SendCreatedComponent", () => { let component: SendCreatedComponent; let fixture: ComponentFixture; - let platformUtilsService: MockProxy; + let clipboardService: MockProxy; let sendService: MockProxy; let toastService: MockProxy; let location: MockProxy; @@ -40,7 +41,7 @@ describe("SendCreatedComponent", () => { let sendViewsSubject: BehaviorSubject; beforeEach(async () => { - platformUtilsService = mock(); + clipboardService = mock(); sendService = mock(); toastService = mock(); location = mock(); @@ -104,7 +105,8 @@ describe("SendCreatedComponent", () => { }); }, }, - { provide: PlatformUtilsService, useValue: platformUtilsService }, + { provide: PlatformUtilsService, useValue: mock() }, + { provide: ClipboardService, useValue: clipboardService }, { provide: SendService, useValue: sendService }, { provide: ToastService, useValue: toastService }, { provide: Location, useValue: location }, @@ -187,7 +189,7 @@ describe("SendCreatedComponent", () => { await component.copyLink(); - expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith(link); + expect(clipboardService.copyToClipboard).toHaveBeenCalledWith(link); expect(toastService.showToast).toHaveBeenCalledWith({ variant: "success", title: null, diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts index 7191040ac6f0..c18b6d3c0045 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts @@ -7,9 +7,9 @@ import { ActivatedRoute, Router, RouterLink, RouterModule } from "@angular/route import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { ButtonModule, IconModule, ToastService } from "@bitwarden/components"; @@ -45,7 +45,7 @@ export class SendCreatedComponent { constructor( private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private sendService: SendService, private route: ActivatedRoute, private toastService: ToastService, @@ -95,7 +95,7 @@ export class SendCreatedComponent { } const env = await firstValueFrom(this.environmentService.environment$); const link = env.getSendUrl() + this.send.accessId + "/" + this.send.urlB64Key; - this.platformUtilsService.copyToClipboard(link); + this.clipboardService.copyToClipboard(link); this.toastService.showToast({ variant: "success", title: null, diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts index c3f4634a6c23..06a93a62f864 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts @@ -12,6 +12,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -112,6 +113,7 @@ describe("SendV2Component", () => { { provide: EnvironmentService, useValue: mock() }, { provide: LogService, useValue: mock() }, { provide: PlatformUtilsService, useValue: mock() }, + { provide: ClipboardService, useValue: mock() }, { provide: SendApiService, useValue: mock() }, { provide: SendItemsService, useValue: mock() }, { provide: SearchService, useValue: mock() }, diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts index 2dad1e3034c7..aae6acb3ffa7 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts @@ -5,6 +5,7 @@ import { BehaviorSubject, of } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -49,6 +50,7 @@ describe("VaultPopupAutofillService", () => { const mockI18nService = mock(); const mockToastService = mock(); const mockPlatformUtilsService = mock(); + const mockClipboardService = mock(); const mockPasswordRepromptService = mock(); const mockCipherService = mock(); const mockMessagingService = mock(); @@ -77,6 +79,7 @@ describe("VaultPopupAutofillService", () => { { provide: I18nService, useValue: mockI18nService }, { provide: ToastService, useValue: mockToastService }, { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, + { provide: ClipboardService, useValue: mockClipboardService }, { provide: PasswordRepromptService, useValue: mockPasswordRepromptService }, { provide: CipherService, useValue: mockCipherService }, { provide: MessagingService, useValue: mockMessagingService }, @@ -250,7 +253,7 @@ describe("VaultPopupAutofillService", () => { const totpCode = "123456"; mockAutofillService.doAutoFill.mockResolvedValue(totpCode); await service.doAutofill(mockCipher); - expect(mockPlatformUtilsService.copyToClipboard).toHaveBeenCalledWith( + expect(mockClipboardService.copyToClipboard).toHaveBeenCalledWith( totpCode, expect.anything(), ); diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts index ff282d7a6d06..a4c3e3314a1e 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts @@ -16,6 +16,7 @@ import { import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -198,6 +199,7 @@ export class VaultPopupAutofillService { private i18nService: I18nService, private toastService: ToastService, private platformUtilService: PlatformUtilsService, + private clipboardService: ClipboardService, private passwordRepromptService: PasswordRepromptService, private cipherService: CipherService, private messagingService: MessagingService, @@ -241,7 +243,7 @@ export class VaultPopupAutofillService { }); if (totpCode != null) { - this.platformUtilService.copyToClipboard(totpCode, { window: window }); + this.clipboardService.copyToClipboard(totpCode, { window: window }); } } catch { this.toastService.showToast({ diff --git a/apps/cli/src/platform/services/cli-clipboard.service.ts b/apps/cli/src/platform/services/cli-clipboard.service.ts new file mode 100644 index 000000000000..cddad74510a8 --- /dev/null +++ b/apps/cli/src/platform/services/cli-clipboard.service.ts @@ -0,0 +1,14 @@ +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; + +export class CliClipboardService implements ClipboardService { + copyToClipboard(text: string, options?: any): void { + throw new Error("Not implemented."); + } + + readFromClipboard(options?: any): Promise { + throw new Error("Not implemented."); + } + clearClipboard(clipboardValue: string, timeoutMs?: number): Promise { + throw new Error("Not implemented."); + } +} diff --git a/apps/cli/src/platform/services/cli-platform-utils.service.ts b/apps/cli/src/platform/services/cli-platform-utils.service.ts index 87b1a79435cb..a1e074fe40c2 100644 --- a/apps/cli/src/platform/services/cli-platform-utils.service.ts +++ b/apps/cli/src/platform/services/cli-platform-utils.service.ts @@ -125,14 +125,6 @@ export class CliPlatformUtilsService implements PlatformUtilsService { return false; } - copyToClipboard(text: string, options?: any): void { - throw new Error("Not implemented."); - } - - readFromClipboard(options?: any): Promise { - throw new Error("Not implemented."); - } - supportsSecureStorage(): boolean { return false; } diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index f57db9909d67..15a817b6f14e 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -167,6 +167,7 @@ import { import { CliBiometricsService } from "../key-management/cli-biometrics-service"; import { flagEnabled } from "../platform/flags"; +import { CliClipboardService } from "../platform/services/cli-clipboard.service"; import { CliPlatformUtilsService } from "../platform/services/cli-platform-utils.service"; import { ConsoleLogService } from "../platform/services/console-log.service"; import { I18nService } from "../platform/services/i18n.service"; @@ -194,6 +195,7 @@ export class ServiceContainer { memoryStorageForStateProviders: MemoryStorageServiceForStateProviders; i18nService: I18nService; platformUtilsService: CliPlatformUtilsService; + clipboardService: CliClipboardService; keyService: KeyService; tokenService: TokenService; appIdService: AppIdService; @@ -288,6 +290,7 @@ export class ServiceContainer { const logoutCallback = async () => await this.logout(); this.platformUtilsService = new CliPlatformUtilsService(ClientType.Cli, packageJson); + this.clipboardService = new CliClipboardService(); this.logService = new ConsoleLogService( this.platformUtilsService.isDev(), (level) => process.env.BITWARDENCLI_DEBUG !== "true" && level <= LogLevelType.Info, diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 8b8900324432..f2fc539fd937 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -51,6 +51,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { ClientType } from "@bitwarden/common/enums"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -106,6 +107,7 @@ import { DesktopBiometricsService } from "../../key-management/biometrics/deskto import { RendererBiometricsService } from "../../key-management/biometrics/renderer-biometrics.service"; import { DesktopLockComponentService } from "../../key-management/lock/services/desktop-lock-component.service"; import { flagEnabled } from "../../platform/flags"; +import { DesktopRendererClipboardService } from "../../platform/services/desktop-clipboard.renderer.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { ElectronKeyService } from "../../platform/services/electron-key.service"; import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service"; @@ -175,6 +177,11 @@ const safeProviders: SafeProvider[] = [ useClass: ElectronPlatformUtilsService, deps: [I18nServiceAbstraction, MessagingServiceAbstraction], }), + safeProvider({ + provide: ClipboardService, + useClass: DesktopRendererClipboardService, + deps: [MessagingServiceAbstraction], + }), safeProvider({ // We manually override the value of SUPPORTS_SECURE_STORAGE here to avoid // the TokenService having to inject the PlatformUtilsService which introduces a @@ -231,11 +238,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: SystemServiceAbstraction, useClass: SystemService, - deps: [ - PlatformUtilsServiceAbstraction, - AutofillSettingsServiceAbstraction, - TaskSchedulerService, - ], + deps: [ClipboardService, AutofillSettingsServiceAbstraction, TaskSchedulerService], }), safeProvider({ provide: ProcessReloadServiceAbstraction, diff --git a/apps/desktop/src/app/tools/generator.component.spec.ts b/apps/desktop/src/app/tools/generator.component.spec.ts index c259b4e52ce9..38ac01d93464 100644 --- a/apps/desktop/src/app/tools/generator.component.spec.ts +++ b/apps/desktop/src/app/tools/generator.component.spec.ts @@ -5,6 +5,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -42,6 +43,10 @@ describe("GeneratorComponent", () => { provide: PlatformUtilsService, useValue: platformUtilsServiceMock, }, + { + provide: ClipboardService, + useValue: mock(), + }, { provide: I18nService, useValue: mock(), diff --git a/apps/desktop/src/app/tools/generator.component.ts b/apps/desktop/src/app/tools/generator.component.ts index fc9ff489a1ab..2916599865df 100644 --- a/apps/desktop/src/app/tools/generator.component.ts +++ b/apps/desktop/src/app/tools/generator.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -21,7 +22,8 @@ export class GeneratorComponent extends BaseGeneratorComponent { passwordGenerationService: PasswordGenerationServiceAbstraction, usernameGenerationService: UsernameGenerationServiceAbstraction, accountService: AccountService, - platformUtilsService: PlatformUtilsService, + private platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, i18nService: I18nService, route: ActivatedRoute, ngZone: NgZone, @@ -31,7 +33,7 @@ export class GeneratorComponent extends BaseGeneratorComponent { super( passwordGenerationService, usernameGenerationService, - platformUtilsService, + clipboardService, accountService, i18nService, logService, diff --git a/apps/desktop/src/app/tools/password-generator-history.component.ts b/apps/desktop/src/app/tools/password-generator-history.component.ts index 0c7c9c4e2217..05a7eeb3a6e8 100644 --- a/apps/desktop/src/app/tools/password-generator-history.component.ts +++ b/apps/desktop/src/app/tools/password-generator-history.component.ts @@ -1,8 +1,8 @@ import { Component } from "@angular/core"; import { PasswordGeneratorHistoryComponent as BasePasswordGeneratorHistoryComponent } from "@bitwarden/angular/tools/generator/components/password-generator-history.component"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @@ -13,10 +13,10 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac export class PasswordGeneratorHistoryComponent extends BasePasswordGeneratorHistoryComponent { constructor( passwordGenerationService: PasswordGenerationServiceAbstraction, - platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, i18nService: I18nService, toastService: ToastService, ) { - super(passwordGenerationService, platformUtilsService, i18nService, window, toastService); + super(passwordGenerationService, clipboardService, i18nService, window, toastService); } } diff --git a/apps/desktop/src/app/tools/send/add-edit.component.ts b/apps/desktop/src/app/tools/send/add-edit.component.ts index 7c272ac94a7c..b77ba8b42e64 100644 --- a/apps/desktop/src/app/tools/send/add-edit.component.ts +++ b/apps/desktop/src/app/tools/send/add-edit.component.ts @@ -8,6 +8,7 @@ import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/too import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -26,6 +27,7 @@ export class AddEditComponent extends BaseAddEditComponent { constructor( i18nService: I18nService, platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, environmentService: EnvironmentService, datePipe: DatePipe, sendService: SendService, @@ -43,6 +45,7 @@ export class AddEditComponent extends BaseAddEditComponent { super( i18nService, platformUtilsService, + clipboardService, environmentService, datePipe, sendService, diff --git a/apps/desktop/src/app/tools/send/send.component.ts b/apps/desktop/src/app/tools/send/send.component.ts index b6c89978c6a7..8456f5702a9d 100644 --- a/apps/desktop/src/app/tools/send/send.component.ts +++ b/apps/desktop/src/app/tools/send/send.component.ts @@ -6,10 +6,10 @@ import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/sen import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; @@ -41,7 +41,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro constructor( sendService: SendService, i18nService: I18nService, - platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, environmentService: EnvironmentService, private broadcasterService: BroadcasterService, ngZone: NgZone, @@ -56,7 +56,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro super( sendService, i18nService, - platformUtilsService, + clipboardService, environmentService, ngZone, searchService, diff --git a/apps/desktop/src/platform/services/desktop-clipboard.renderer.service.ts b/apps/desktop/src/platform/services/desktop-clipboard.renderer.service.ts new file mode 100644 index 000000000000..63143efbef1b --- /dev/null +++ b/apps/desktop/src/platform/services/desktop-clipboard.renderer.service.ts @@ -0,0 +1,35 @@ +import { + ClipboardOptions, + ClipboardService, +} from "@bitwarden/common/platform/abstractions/clipboard.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; + +import { ClipboardWriteMessage } from "../types/clipboard"; + +export class DesktopRendererClipboardService implements ClipboardService { + constructor(private messagingService: MessagingService) {} + + copyToClipboard(text: string, options?: ClipboardOptions): void | boolean { + const clearing = options?.clearing === true; + const clearMs = options?.clearMs ?? null; + + void ipc.platform.clipboard.write({ + text: text, + password: (options?.allowHistory ?? false) === false, // default to false + } satisfies ClipboardWriteMessage); + + if (!clearing) { + this.messagingService.send("copiedToClipboard", { + clipboardValue: text, + clearMs: clearMs, + clearing: clearing, + }); + } + } + readFromClipboard(): Promise { + return ipc.platform.clipboard.read(); + } + clearClipboard(clipboardValue: string, timeoutMs?: number): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/apps/desktop/src/platform/services/electron-platform-utils.service.ts b/apps/desktop/src/platform/services/electron-platform-utils.service.ts index b61d2a0c5e98..447dbdc80888 100644 --- a/apps/desktop/src/platform/services/electron-platform-utils.service.ts +++ b/apps/desktop/src/platform/services/electron-platform-utils.service.ts @@ -3,12 +3,7 @@ import { ClientType, DeviceType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { - ClipboardOptions, - PlatformUtilsService, -} from "@bitwarden/common/platform/abstractions/platform-utils.service"; - -import { ClipboardWriteMessage } from "../types/clipboard"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; export const ELECTRON_SUPPORTS_SECURE_STORAGE = true; @@ -109,30 +104,6 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return false; } - copyToClipboard(text: string, options?: ClipboardOptions): void { - const clearing = options?.clearing === true; - const clearMs = options?.clearMs ?? null; - - // 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 - ipc.platform.clipboard.write({ - text: text, - password: (options?.allowHistory ?? false) === false, // default to false - } satisfies ClipboardWriteMessage); - - if (!clearing) { - this.messagingService.send("copiedToClipboard", { - clipboardValue: text, - clearMs: clearMs, - clearing: clearing, - }); - } - } - - readFromClipboard(): Promise { - return ipc.platform.clipboard.read(); - } - supportsSecureStorage(): boolean { return ELECTRON_SUPPORTS_SECURE_STORAGE; } diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.ts b/apps/desktop/src/vault/app/vault/add-edit.component.ts index 02fa8076086d..5a090f094366 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -14,6 +14,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -42,6 +43,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On folderService: FolderService, i18nService: I18nService, platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, auditService: AuditService, accountService: AccountService, collectionService: CollectionService, @@ -65,6 +67,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On folderService, i18nService, platformUtilsService, + clipboardService, auditService, accountService, collectionService, @@ -149,7 +152,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On } async importSshKeyFromClipboard(password: string = "") { - const key = await this.platformUtilsService.readFromClipboard(); + const key = await this.clipboardService.readFromClipboard(); const parsedKey = await ipc.platform.sshAgent.importKey(key, password); if (parsedKey == null) { this.toastService.showToast({ diff --git a/apps/desktop/src/vault/app/vault/password-history.component.ts b/apps/desktop/src/vault/app/vault/password-history.component.ts index 12701ac5527f..67ce1ebbda61 100644 --- a/apps/desktop/src/vault/app/vault/password-history.component.ts +++ b/apps/desktop/src/vault/app/vault/password-history.component.ts @@ -2,6 +2,7 @@ import { Component } from "@angular/core"; import { PasswordHistoryComponent as BasePasswordHistoryComponent } from "@bitwarden/angular/vault/components/password-history.component"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -14,9 +15,17 @@ export class PasswordHistoryComponent extends BasePasswordHistoryComponent { constructor( cipherService: CipherService, platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, i18nService: I18nService, accountService: AccountService, ) { - super(cipherService, platformUtilsService, i18nService, accountService, window); + super( + cipherService, + platformUtilsService, + clipboardService, + i18nService, + accountService, + window, + ); } } diff --git a/apps/desktop/src/vault/app/vault/vault.component.ts b/apps/desktop/src/vault/app/vault/vault.component.ts index c2260692fbd0..41e3c27b24bc 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.ts +++ b/apps/desktop/src/vault/app/vault/vault.component.ts @@ -23,6 +23,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -106,6 +107,7 @@ export class VaultComponent implements OnInit, OnDestroy { private syncService: SyncService, private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private eventCollectionService: EventCollectionService, private totpService: TotpService, private passwordRepromptService: PasswordRepromptService, @@ -808,7 +810,7 @@ export class VaultComponent implements OnInit, OnDestroy { return; } - this.platformUtilsService.copyToClipboard(value); + this.clipboardService.copyToClipboard(value); this.platformUtilsService.showToast( "info", null, diff --git a/apps/desktop/src/vault/app/vault/view.component.ts b/apps/desktop/src/vault/app/vault/view.component.ts index d3e8fff34952..e096b05459bf 100644 --- a/apps/desktop/src/vault/app/vault/view.component.ts +++ b/apps/desktop/src/vault/app/vault/view.component.ts @@ -18,6 +18,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -53,6 +54,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro keyService: KeyService, encryptService: EncryptService, platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, auditService: AuditService, broadcasterService: BroadcasterService, ngZone: NgZone, @@ -79,6 +81,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro keyService, encryptService, platformUtilsService, + clipboardService, auditService, window, broadcasterService, diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts index 1b08d081823c..cc8a38208418 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts @@ -8,9 +8,9 @@ import { Subject, takeUntil } from "rxjs"; import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @@ -73,7 +73,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { @Inject(DIALOG_DATA) protected data: ResetPasswordDialogData, private resetPasswordService: OrganizationUserResetPasswordService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private passwordGenerationService: PasswordGenerationServiceAbstraction, private policyService: PolicyService, private logService: LogService, @@ -121,7 +121,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { return; } - this.platformUtilsService.copyToClipboard(value, { window: window }); + this.clipboardService.copyToClipboard(value, { window: window }); this.toastService.showToast({ variant: "info", title: null, diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts index 78e3b805eb88..33b8d6927741 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts @@ -10,6 +10,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -41,6 +42,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent implem folderService: FolderService, i18nService: I18nService, platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, auditService: AuditService, accountService: AccountService, collectionService: CollectionService, @@ -65,6 +67,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent implem folderService, i18nService, platformUtilsService, + clipboardService, auditService, accountService, collectionService, diff --git a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts index b0b34db8a4a5..9fc0b3baffe7 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts +++ b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts @@ -11,9 +11,9 @@ import { OrganizationApiKeyRequest } from "@bitwarden/common/admin-console/model import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { ApiKeyResponse } from "@bitwarden/common/auth/models/response/api-key.response"; import { Verification } from "@bitwarden/common/auth/types/verification"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; export interface BillingSyncApiModalData { @@ -41,7 +41,7 @@ export class BillingSyncApiKeyComponent { @Inject(DIALOG_DATA) protected data: BillingSyncApiModalData, private userVerificationService: UserVerificationService, private apiService: ApiService, - private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private i18nService: I18nService, private organizationApiService: OrganizationApiServiceAbstraction, private logService: LogService, @@ -52,7 +52,7 @@ export class BillingSyncApiKeyComponent { } copy() { - this.platformUtilsService.copyToClipboard(this.clientSecret); + this.clipboardService.copyToClipboard(this.clientSecret); } submit = async () => { diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 8f21dfa2c8bc..f11490556e6d 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -53,6 +53,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/ import { ClientType } from "@bitwarden/common/enums"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -118,6 +119,7 @@ import { InitService } from "./init.service"; import { ENV_URLS } from "./injection-tokens"; import { ModalService } from "./modal.service"; import { RouterService } from "./router.service"; +import { WebClipboardService } from "./web-clipboard.service"; import { WebFileDownloadService } from "./web-file-download.service"; import { WebPlatformUtilsService } from "./web-platform-utils.service"; @@ -176,6 +178,11 @@ const safeProviders: SafeProvider[] = [ useClass: WebPlatformUtilsService, useAngularDecorators: true, }), + safeProvider({ + provide: ClipboardService, + useClass: WebClipboardService, + useAngularDecorators: true, + }), safeProvider({ provide: ModalServiceAbstraction, useClass: ModalService, diff --git a/apps/web/src/app/core/web-clipboard.service.ts b/apps/web/src/app/core/web-clipboard.service.ts new file mode 100644 index 000000000000..246db04e4d8d --- /dev/null +++ b/apps/web/src/app/core/web-clipboard.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from "@angular/core"; + +import { + ClipboardOptions, + ClipboardService, +} from "@bitwarden/common/platform/abstractions/clipboard.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +@Injectable() +export class WebClipboardService implements ClipboardService { + constructor(private logService: LogService) {} + + copyToClipboard( + text: string, + options?: ClipboardOptions & { win: any; doc: any }, + ): void | boolean { + let win = window; + let doc = window.document; + if (options && (options.window || options.win)) { + win = options.window || options.win; + doc = win.document; + } else if (options && options.doc) { + doc = options.doc; + } + if (doc.queryCommandSupported && doc.queryCommandSupported("copy")) { + const textarea = doc.createElement("textarea"); + textarea.textContent = text; + // Prevent scrolling to bottom of page in MS Edge. + textarea.style.position = "fixed"; + let copyEl = doc.body; + // For some reason copy command won't work when modal is open if appending to body + if (doc.body.classList.contains("modal-open")) { + copyEl = doc.body.querySelector(".modal"); + } + copyEl.appendChild(textarea); + textarea.select(); + let success = false; + try { + // Security exception may be thrown by some browsers. + success = doc.execCommand("copy"); + if (!success) { + this.logService.debug("Copy command unsupported or disabled."); + } + } catch (e) { + // eslint-disable-next-line + console.warn("Copy to clipboard failed.", e); + } finally { + copyEl.removeChild(textarea); + } + return success; + } + } + + readFromClipboard(): Promise { + throw new Error("Cannot read from clipboard on web."); + } + clearClipboard(clipboardValue: string, timeoutMs?: number): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/apps/web/src/app/core/web-platform-utils.service.ts b/apps/web/src/app/core/web-platform-utils.service.ts index 3df2a7d895bc..8e78eb2abe53 100644 --- a/apps/web/src/app/core/web-platform-utils.service.ts +++ b/apps/web/src/app/core/web-platform-utils.service.ts @@ -146,48 +146,6 @@ export class WebPlatformUtilsService implements PlatformUtilsService { return process.env.ENV.toString() === "selfhosted"; } - copyToClipboard(text: string, options?: any): void | boolean { - let win = window; - let doc = window.document; - if (options && (options.window || options.win)) { - win = options.window || options.win; - doc = win.document; - } else if (options && options.doc) { - doc = options.doc; - } - if (doc.queryCommandSupported && doc.queryCommandSupported("copy")) { - const textarea = doc.createElement("textarea"); - textarea.textContent = text; - // Prevent scrolling to bottom of page in MS Edge. - textarea.style.position = "fixed"; - let copyEl = doc.body; - // For some reason copy command won't work when modal is open if appending to body - if (doc.body.classList.contains("modal-open")) { - copyEl = doc.body.querySelector(".modal"); - } - copyEl.appendChild(textarea); - textarea.select(); - let success = false; - try { - // Security exception may be thrown by some browsers. - success = doc.execCommand("copy"); - if (!success) { - this.logService.debug("Copy command unsupported or disabled."); - } - } catch (e) { - // eslint-disable-next-line - console.warn("Copy to clipboard failed.", e); - } finally { - copyEl.removeChild(textarea); - } - return success; - } - } - - readFromClipboard(options?: any): Promise { - throw new Error("Cannot read from clipboard on web."); - } - supportsSecureStorage() { return false; } diff --git a/apps/web/src/app/tools/generator.component.ts b/apps/web/src/app/tools/generator.component.ts index a11c0c4a97b9..5f5982b0b277 100644 --- a/apps/web/src/app/tools/generator.component.ts +++ b/apps/web/src/app/tools/generator.component.ts @@ -5,6 +5,7 @@ import { ActivatedRoute } from "@angular/router"; import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -25,7 +26,8 @@ export class GeneratorComponent extends BaseGeneratorComponent { passwordGenerationService: PasswordGenerationServiceAbstraction, usernameGenerationService: UsernameGenerationServiceAbstraction, accountService: AccountService, - platformUtilsService: PlatformUtilsService, + private platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, i18nService: I18nService, logService: LogService, route: ActivatedRoute, @@ -36,7 +38,7 @@ export class GeneratorComponent extends BaseGeneratorComponent { super( passwordGenerationService, usernameGenerationService, - platformUtilsService, + clipboardService, accountService, i18nService, logService, diff --git a/apps/web/src/app/tools/password-generator-history.component.ts b/apps/web/src/app/tools/password-generator-history.component.ts index 0c7c9c4e2217..05a7eeb3a6e8 100644 --- a/apps/web/src/app/tools/password-generator-history.component.ts +++ b/apps/web/src/app/tools/password-generator-history.component.ts @@ -1,8 +1,8 @@ import { Component } from "@angular/core"; import { PasswordGeneratorHistoryComponent as BasePasswordGeneratorHistoryComponent } from "@bitwarden/angular/tools/generator/components/password-generator-history.component"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @@ -13,10 +13,10 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac export class PasswordGeneratorHistoryComponent extends BasePasswordGeneratorHistoryComponent { constructor( passwordGenerationService: PasswordGenerationServiceAbstraction, - platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, i18nService: I18nService, toastService: ToastService, ) { - super(passwordGenerationService, platformUtilsService, i18nService, window, toastService); + super(passwordGenerationService, clipboardService, i18nService, window, toastService); } } diff --git a/apps/web/src/app/tools/send/add-edit.component.ts b/apps/web/src/app/tools/send/add-edit.component.ts index 4ce126a33bcb..e03bfba5dbc2 100644 --- a/apps/web/src/app/tools/send/add-edit.component.ts +++ b/apps/web/src/app/tools/send/add-edit.component.ts @@ -9,6 +9,7 @@ import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/too import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -30,6 +31,7 @@ export class AddEditComponent extends BaseAddEditComponent { constructor( i18nService: I18nService, platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, environmentService: EnvironmentService, datePipe: DatePipe, sendService: SendService, @@ -49,6 +51,7 @@ export class AddEditComponent extends BaseAddEditComponent { super( i18nService, platformUtilsService, + clipboardService, environmentService, datePipe, sendService, diff --git a/apps/web/src/app/tools/send/send-access-text.component.ts b/apps/web/src/app/tools/send/send-access-text.component.ts index 6568fe482adb..b41e1c84350e 100644 --- a/apps/web/src/app/tools/send/send-access-text.component.ts +++ b/apps/web/src/app/tools/send/send-access-text.component.ts @@ -3,8 +3,8 @@ import { Component, Input } from "@angular/core"; import { FormBuilder } from "@angular/forms"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; import { ToastService } from "@bitwarden/components"; @@ -26,7 +26,7 @@ export class SendAccessTextComponent { constructor( private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private formBuilder: FormBuilder, private toastService: ToastService, ) {} @@ -49,7 +49,7 @@ export class SendAccessTextComponent { } protected copyText() { - this.platformUtilsService.copyToClipboard(this.send.text.text); + this.clipboardService.copyToClipboard(this.send.text.text); this.toastService.showToast({ variant: "success", title: null, diff --git a/apps/web/src/app/tools/send/send.component.ts b/apps/web/src/app/tools/send/send.component.ts index 1268e4bfb505..6d32f1ab48a8 100644 --- a/apps/web/src/app/tools/send/send.component.ts +++ b/apps/web/src/app/tools/send/send.component.ts @@ -7,10 +7,10 @@ import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/sen import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; @@ -55,7 +55,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro constructor( sendService: SendService, i18nService: I18nService, - platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, environmentService: EnvironmentService, ngZone: NgZone, searchService: SearchService, @@ -69,7 +69,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro super( sendService, i18nService, - platformUtilsService, + clipboardService, environmentService, ngZone, searchService, diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.ts b/apps/web/src/app/vault/individual-vault/add-edit.component.ts index 916c845e9d3d..a5912a976ebc 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.ts @@ -16,6 +16,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { ProductTierType } from "@bitwarden/common/billing/enums"; import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -58,6 +59,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On folderService: FolderService, i18nService: I18nService, platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, auditService: AuditService, accountService: AccountService, collectionService: CollectionService, @@ -82,6 +84,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On folderService, i18nService, platformUtilsService, + clipboardService, auditService, accountService, collectionService, @@ -193,7 +196,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On return false; } - this.platformUtilsService.copyToClipboard(value, { window: window }); + this.clipboardService.copyToClipboard(value, { window: window }); this.platformUtilsService.showToast( "info", null, diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 8c1d08b269c1..636b43dbf433 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -56,6 +56,7 @@ import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstract import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -257,6 +258,7 @@ export class VaultComponent implements OnInit, OnDestroy { private dialogService: DialogService, private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private broadcasterService: BroadcasterService, private ngZone: NgZone, private organizationService: OrganizationService, @@ -1284,7 +1286,7 @@ export class VaultComponent implements OnInit, OnDestroy { return; } - this.platformUtilsService.copyToClipboard(value, { window: window }); + this.clipboardService.copyToClipboard(value, { window: window }); this.toastService.showToast({ variant: "info", title: null, diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index 75042a63e91a..7ab324513e3d 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -11,6 +11,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -43,6 +44,7 @@ export class AddEditComponent extends BaseAddEditComponent { folderService: FolderService, i18nService: I18nService, platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, auditService: AuditService, accountService: AccountService, collectionService: CollectionService, @@ -68,6 +70,7 @@ export class AddEditComponent extends BaseAddEditComponent { folderService, i18nService, platformUtilsService, + clipboardService, auditService, accountService, collectionService, diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 14550968ba5e..635cead06edd 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -54,6 +54,7 @@ import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstract import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -249,6 +250,7 @@ export class VaultComponent implements OnInit, OnDestroy { private broadcasterService: BroadcasterService, private ngZone: NgZone, private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private cipherService: CipherService, private passwordRepromptService: PasswordRepromptService, private collectionAdminService: CollectionAdminService, @@ -1289,7 +1291,7 @@ export class VaultComponent implements OnInit, OnDestroy { return; } - this.platformUtilsService.copyToClipboard(value, { window: window }); + this.clipboardService.copyToClipboard(value, { window: window }); this.toastService.showToast({ variant: "info", title: null, diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts index ea24e74ac8f6..b66bbc30ec04 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/scim.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; -import { UntypedFormBuilder, FormControl } from "@angular/forms"; +import { FormControl, UntypedFormBuilder } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { firstValueFrom } from "rxjs"; @@ -16,9 +16,9 @@ import { OrganizationApiKeyRequest } from "@bitwarden/common/admin-console/model import { OrganizationConnectionRequest } from "@bitwarden/common/admin-console/models/request/organization-connection.request"; import { ScimConfigRequest } from "@bitwarden/common/admin-console/models/request/scim-config.request"; import { OrganizationConnectionResponse } from "@bitwarden/common/admin-console/models/response/organization-connection.response"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; @Component({ @@ -43,7 +43,7 @@ export class ScimComponent implements OnInit { private formBuilder: UntypedFormBuilder, private route: ActivatedRoute, private apiService: ApiService, - private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private i18nService: I18nService, private environmentService: EnvironmentService, private organizationApiService: OrganizationApiServiceAbstraction, @@ -83,7 +83,7 @@ export class ScimComponent implements OnInit { } copyScimUrl = async () => { - this.platformUtilsService.copyToClipboard(await this.getScimEndpointUrl()); + this.clipboardService.copyToClipboard(await this.getScimEndpointUrl()); this.toastService.showToast({ message: this.i18nService.t("valueCopied", this.i18nService.t("scimUrl")), variant: "success", @@ -120,7 +120,7 @@ export class ScimComponent implements OnInit { }; copyScimKey = async () => { - this.platformUtilsService.copyToClipboard(this.formData.get("clientSecret").value); + this.clipboardService.copyToClipboard(this.formData.get("clientSecret").value); this.toastService.showToast({ message: this.i18nService.t("valueCopied", this.i18nService.t("scimApiKey")), variant: "success", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index a95192e0d912..df0fefe9e6d6 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -23,6 +23,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -113,6 +114,7 @@ export class OverviewComponent implements OnInit, OnDestroy { private dialogService: DialogService, private organizationService: OrganizationService, private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private i18nService: I18nService, private smOnboardingTasksService: SMOnboardingTasksService, private logService: LogService, @@ -365,13 +367,19 @@ export class OverviewComponent implements OnInit, OnDestroy { } copySecretName(name: string) { - SecretsListComponent.copySecretName(name, this.platformUtilsService, this.i18nService); + SecretsListComponent.copySecretName( + name, + this.platformUtilsService, + this.clipboardService, + this.i18nService, + ); } async copySecretValue(id: string) { await SecretsListComponent.copySecretValue( id, this.platformUtilsService, + this.clipboardService, this.i18nService, this.secretService, this.logService, @@ -379,7 +387,12 @@ export class OverviewComponent implements OnInit, OnDestroy { } copySecretUuid(id: string) { - SecretsListComponent.copySecretUuid(id, this.platformUtilsService, this.i18nService); + SecretsListComponent.copySecretUuid( + id, + this.platformUtilsService, + this.clipboardService, + this.i18nService, + ); } protected async hideOnboarding() { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts index b9c6d86cefcd..8d70ee984e06 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts @@ -5,6 +5,7 @@ import { ActivatedRoute } from "@angular/router"; import { combineLatest, combineLatestWith, filter, Observable, startWith, switchMap } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -47,6 +48,7 @@ export class ProjectSecretsComponent implements OnInit { private secretService: SecretService, private dialogService: DialogService, private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private i18nService: I18nService, private organizationService: OrganizationService, private logService: LogService, @@ -123,13 +125,19 @@ export class ProjectSecretsComponent implements OnInit { } copySecretName(name: string) { - SecretsListComponent.copySecretName(name, this.platformUtilsService, this.i18nService); + SecretsListComponent.copySecretName( + name, + this.platformUtilsService, + this.clipboardService, + this.i18nService, + ); } async copySecretValue(id: string) { await SecretsListComponent.copySecretValue( id, this.platformUtilsService, + this.clipboardService, this.i18nService, this.secretService, this.logService, @@ -137,6 +145,11 @@ export class ProjectSecretsComponent implements OnInit { } copySecretUuid(id: string) { - SecretsListComponent.copySecretUuid(id, this.platformUtilsService, this.i18nService); + SecretsListComponent.copySecretUuid( + id, + this.platformUtilsService, + this.clipboardService, + this.i18nService, + ); } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts index 5d1ed7808163..29c7524e1f87 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts @@ -5,6 +5,7 @@ import { ActivatedRoute } from "@angular/router"; import { combineLatestWith, Observable, startWith, switchMap } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -44,6 +45,7 @@ export class SecretsComponent implements OnInit { private secretService: SecretService, private dialogService: DialogService, private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private i18nService: I18nService, private organizationService: OrganizationService, private logService: LogService, @@ -111,13 +113,19 @@ export class SecretsComponent implements OnInit { } copySecretName(name: string) { - SecretsListComponent.copySecretName(name, this.platformUtilsService, this.i18nService); + SecretsListComponent.copySecretName( + name, + this.platformUtilsService, + this.clipboardService, + this.i18nService, + ); } async copySecretValue(id: string) { await SecretsListComponent.copySecretValue( id, this.platformUtilsService, + this.clipboardService, this.i18nService, this.secretService, this.logService, @@ -125,6 +133,11 @@ export class SecretsComponent implements OnInit { } copySecretUuid(id: string) { - SecretsListComponent.copySecretUuid(id, this.platformUtilsService, this.i18nService); + SecretsListComponent.copySecretUuid( + id, + this.platformUtilsService, + this.clipboardService, + this.i18nService, + ); } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-dialog.component.ts index 4f761f7f2794..6db60ae9988e 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/access/dialogs/access-token-dialog.component.ts @@ -1,10 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject, OnInit } from "@angular/core"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; export interface AccessTokenDetails { @@ -20,7 +20,7 @@ export class AccessTokenDialogComponent implements OnInit { constructor( public dialogRef: DialogRef, @Inject(DIALOG_DATA) public data: AccessTokenDetails, - private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private toastService: ToastService, private i18nService: I18nService, ) {} @@ -34,7 +34,7 @@ export class AccessTokenDialogComponent implements OnInit { } copyAccessToken(): void { - this.platformUtilsService.copyToClipboard(this.data.accessToken); + this.clipboardService.copyToClipboard(this.data.accessToken); this.toastService.showToast({ variant: "success", title: null, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts index 96e3b58b6333..f60e49969a9d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/config/config.component.ts @@ -4,9 +4,9 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Params } from "@angular/router"; import { Subject, concatMap, takeUntil } from "rxjs"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; import { ProjectListView } from "../../models/view/project-list.view"; @@ -39,7 +39,7 @@ export class ServiceAccountConfigComponent implements OnInit, OnDestroy { constructor( private environmentService: EnvironmentService, private route: ActivatedRoute, - private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private toastService: ToastService, private i18nService: I18nService, private projectService: ProjectService, @@ -96,7 +96,7 @@ export class ServiceAccountConfigComponent implements OnInit, OnDestroy { } copyIdentityUrl = () => { - this.platformUtilsService.copyToClipboard(this.identityUrl); + this.clipboardService.copyToClipboard(this.identityUrl); this.toastService.showToast({ variant: "success", title: null, @@ -105,7 +105,7 @@ export class ServiceAccountConfigComponent implements OnInit, OnDestroy { }; copyApiUrl = () => { - this.platformUtilsService.copyToClipboard(this.apiUrl); + this.clipboardService.copyToClipboard(this.apiUrl); this.toastService.showToast({ variant: "success", title: null, @@ -114,7 +114,7 @@ export class ServiceAccountConfigComponent implements OnInit, OnDestroy { }; copyOrganizationId = () => { - this.platformUtilsService.copyToClipboard(this.organizationId); + this.clipboardService.copyToClipboard(this.organizationId); this.toastService.showToast({ variant: "success", title: null, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts index 0ca5ba090743..b3db7b4cd06d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/projects-list.component.ts @@ -4,6 +4,7 @@ import { SelectionModel } from "@angular/cdk/collections"; import { Component, EventEmitter, Input, Output } from "@angular/core"; import { map } from "rxjs"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { TableDataSource, ToastService } from "@bitwarden/components"; @@ -48,6 +49,7 @@ export class ProjectsListComponent { constructor( private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private toastService: ToastService, ) {} @@ -97,7 +99,7 @@ export class ProjectsListComponent { } copyProjectUuidToClipboard(id: string) { - this.platformUtilsService.copyToClipboard(id); + this.clipboardService.copyToClipboard(id); this.platformUtilsService.showToast( "success", null, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts index 37b9524238f2..9dde323ec4ad 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.ts @@ -4,6 +4,7 @@ import { SelectionModel } from "@angular/cdk/collections"; import { Component, EventEmitter, Input, OnDestroy, Output } from "@angular/core"; import { Subject, takeUntil } from "rxjs"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -134,9 +135,10 @@ export class SecretsListComponent implements OnDestroy { static copySecretName( name: string, platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, i18nService: I18nService, ) { - platformUtilsService.copyToClipboard(name); + clipboardService.copyToClipboard(name); platformUtilsService.showToast( "success", null, @@ -150,13 +152,14 @@ export class SecretsListComponent implements OnDestroy { static async copySecretValue( id: string, platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, i18nService: I18nService, secretService: SecretService, logService: LogService, ) { try { const value = await secretService.getBySecretId(id).then((secret) => secret.value); - platformUtilsService.copyToClipboard(value); + clipboardService.copyToClipboard(value); platformUtilsService.showToast( "success", null, @@ -170,9 +173,10 @@ export class SecretsListComponent implements OnDestroy { static copySecretUuid( id: string, platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, i18nService: I18nService, ) { - platformUtilsService.copyToClipboard(id); + clipboardService.copyToClipboard(id); platformUtilsService.showToast( "success", null, @@ -181,11 +185,12 @@ export class SecretsListComponent implements OnDestroy { } /** - * TODO: Remove in favor of updating `PlatformUtilsService.copyToClipboard` + * TODO: Remove in favor of updating `ClipboardService.copyToClipboard` */ private static copyToClipboardAsync( text: Promise, platformUtilsService: PlatformUtilsService, + clipboardService: ClipboardService, ) { if (platformUtilsService.isSafari()) { return navigator.clipboard.write([ @@ -195,6 +200,6 @@ export class SecretsListComponent implements OnDestroy { ]); } - return text.then((t) => platformUtilsService.copyToClipboard(t)); + return text.then((t) => clipboardService.copyToClipboard(t)); } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts index 3a21dbe3b68d..f7fd6712a48e 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/trash/trash.component.ts @@ -4,6 +4,7 @@ import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { combineLatestWith, Observable, startWith, switchMap } from "rxjs"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; @@ -34,6 +35,7 @@ export class TrashComponent implements OnInit { private route: ActivatedRoute, private secretService: SecretService, private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private i18nService: I18nService, private dialogService: DialogService, ) {} @@ -74,6 +76,11 @@ export class TrashComponent implements OnInit { } copySecretUuid(id: string) { - SecretsListComponent.copySecretUuid(id, this.platformUtilsService, this.i18nService); + SecretsListComponent.copySecretUuid( + id, + this.platformUtilsService, + this.clipboardService, + this.i18nService, + ); } } diff --git a/libs/angular/src/directives/copy-click.directive.spec.ts b/libs/angular/src/directives/copy-click.directive.spec.ts index 29466f7fbe3d..2cf3dd688b73 100644 --- a/libs/angular/src/directives/copy-click.directive.spec.ts +++ b/libs/angular/src/directives/copy-click.directive.spec.ts @@ -1,8 +1,8 @@ import { Component, ElementRef, ViewChild } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; import { CopyClickDirective } from "./copy-click.directive"; @@ -45,7 +45,7 @@ describe("CopyClickDirective", () => { }, }, }, - { provide: PlatformUtilsService, useValue: { copyToClipboard } }, + { provide: ClipboardService, useValue: { copyToClipboard } }, { provide: ToastService, useValue: { showToast } }, ], }).compileComponents(); diff --git a/libs/angular/src/directives/copy-click.directive.ts b/libs/angular/src/directives/copy-click.directive.ts index ece867c09fd6..66341ae8a148 100644 --- a/libs/angular/src/directives/copy-click.directive.ts +++ b/libs/angular/src/directives/copy-click.directive.ts @@ -2,8 +2,8 @@ // @ts-strict-ignore import { Directive, HostListener, Input } from "@angular/core"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService, ToastVariant } from "@bitwarden/components"; @Directive({ @@ -14,7 +14,7 @@ export class CopyClickDirective { private toastVariant: ToastVariant = "success"; constructor( - private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private toastService: ToastService, private i18nService: I18nService, ) {} @@ -51,7 +51,7 @@ export class CopyClickDirective { } @HostListener("click") onClick() { - this.platformUtilsService.copyToClipboard(this.valueToCopy); + this.clipboardService.copyToClipboard(this.valueToCopy); if (this._showToast) { const message = this.valueLabel diff --git a/libs/angular/src/directives/copy-text.directive.ts b/libs/angular/src/directives/copy-text.directive.ts index de26973e2c91..078daa398c41 100644 --- a/libs/angular/src/directives/copy-text.directive.ts +++ b/libs/angular/src/directives/copy-text.directive.ts @@ -3,6 +3,7 @@ import { Directive, ElementRef, HostListener, Input } from "@angular/core"; import { ClientType } from "@bitwarden/common/enums"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @Directive({ @@ -12,6 +13,7 @@ export class CopyTextDirective { constructor( private el: ElementRef, private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, ) {} @Input("appCopyText") copyText: string; @@ -23,7 +25,7 @@ export class CopyTextDirective { const timeout = this.platformUtilsService.getClientType() === ClientType.Desktop ? 100 : 0; setTimeout(() => { - this.platformUtilsService.copyToClipboard(this.copyText, { window: window }); + this.clipboardService.copyToClipboard(this.copyText, { window: window }); }, timeout); } } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 803808612cf6..e1eed240066d 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -150,6 +150,7 @@ import { TaxService } from "@bitwarden/common/billing/services/tax.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -1094,7 +1095,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: OrgDomainInternalServiceAbstraction, useClass: OrgDomainService, - deps: [PlatformUtilsServiceAbstraction, I18nServiceAbstraction], + deps: [ClipboardService, I18nServiceAbstraction], }), safeProvider({ provide: OrgDomainServiceAbstraction, diff --git a/libs/angular/src/tools/generator/components/generator.component.ts b/libs/angular/src/tools/generator/components/generator.component.ts index 1f3c635e499c..ffe5ce502a85 100644 --- a/libs/angular/src/tools/generator/components/generator.component.ts +++ b/libs/angular/src/tools/generator/components/generator.component.ts @@ -7,19 +7,19 @@ import { debounceTime, first, map, skipWhile, takeUntil } from "rxjs/operators"; import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; import { - GeneratorType, DefaultPasswordBoundaries as DefaultBoundaries, + GeneratorType, } from "@bitwarden/generator-core"; import { PasswordGenerationServiceAbstraction, + PasswordGeneratorOptions, UsernameGenerationServiceAbstraction, UsernameGeneratorOptions, - PasswordGeneratorOptions, } from "@bitwarden/generator-legacy"; export class EmailForwarderOptions { @@ -72,7 +72,7 @@ export class GeneratorComponent implements OnInit, OnDestroy { constructor( protected passwordGenerationService: PasswordGenerationServiceAbstraction, protected usernameGenerationService: UsernameGenerationServiceAbstraction, - protected platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, protected accountService: AccountService, protected i18nService: I18nService, protected logService: LogService, @@ -337,10 +337,7 @@ export class GeneratorComponent implements OnInit, OnDestroy { copy() { const password = this.type === "password"; const copyOptions = this.win != null ? { window: this.win } : null; - this.platformUtilsService.copyToClipboard( - password ? this.password : this.username, - copyOptions, - ); + this.clipboardService.copyToClipboard(password ? this.password : this.username, copyOptions); this.toastService.showToast({ variant: "info", title: null, diff --git a/libs/angular/src/tools/generator/components/password-generator-history.component.ts b/libs/angular/src/tools/generator/components/password-generator-history.component.ts index 2933163fce2f..37172d1d40cb 100644 --- a/libs/angular/src/tools/generator/components/password-generator-history.component.ts +++ b/libs/angular/src/tools/generator/components/password-generator-history.component.ts @@ -2,8 +2,8 @@ // @ts-strict-ignore import { Directive, OnInit } from "@angular/core"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; import { GeneratedPasswordHistory } from "@bitwarden/generator-history"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @@ -14,7 +14,7 @@ export class PasswordGeneratorHistoryComponent implements OnInit { constructor( protected passwordGenerationService: PasswordGenerationServiceAbstraction, - protected platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, protected i18nService: I18nService, private win: Window, protected toastService: ToastService, @@ -30,7 +30,7 @@ export class PasswordGeneratorHistoryComponent implements OnInit { copy(password: string) { const copyOptions = this.win != null ? { window: this.win } : null; - this.platformUtilsService.copyToClipboard(password, copyOptions); + this.clipboardService.copyToClipboard(password, copyOptions); this.toastService.showToast({ variant: "info", title: null, diff --git a/libs/angular/src/tools/send/add-edit.component.ts b/libs/angular/src/tools/send/add-edit.component.ts index aeee1fa104cc..5f614136bfd9 100644 --- a/libs/angular/src/tools/send/add-edit.component.ts +++ b/libs/angular/src/tools/send/add-edit.component.ts @@ -17,6 +17,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -118,6 +119,7 @@ export class AddEditComponent implements OnInit, OnDestroy { constructor( protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, protected environmentService: EnvironmentService, protected datePipe: DatePipe, protected sendService: SendService, @@ -372,7 +374,7 @@ export class AddEditComponent implements OnInit, OnDestroy { } async copyLinkToClipboard(link: string): Promise { - return Promise.resolve(this.platformUtilsService.copyToClipboard(link)); + return Promise.resolve(this.clipboardService.copyToClipboard(link)); } protected async delete(): Promise { diff --git a/libs/angular/src/tools/send/send.component.ts b/libs/angular/src/tools/send/send.component.ts index 6b7f911ed128..99cdae5ac11b 100644 --- a/libs/angular/src/tools/send/send.component.ts +++ b/libs/angular/src/tools/send/send.component.ts @@ -5,8 +5,8 @@ import { BehaviorSubject, Subject, firstValueFrom, - mergeMap, from, + mergeMap, switchMap, takeUntil, } from "rxjs"; @@ -14,10 +14,10 @@ import { import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; @@ -70,7 +70,7 @@ export class SendComponent implements OnInit, OnDestroy { constructor( protected sendService: SendService, protected i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, protected environmentService: EnvironmentService, protected ngZone: NgZone, protected searchService: SearchService, @@ -244,7 +244,7 @@ export class SendComponent implements OnInit, OnDestroy { async copy(s: SendView) { const env = await firstValueFrom(this.environmentService.environment$); const link = env.getSendUrl() + s.accessId + "/" + s.urlB64Key; - this.platformUtilsService.copyToClipboard(link); + this.clipboardService.copyToClipboard(link); this.toastService.showToast({ variant: "success", title: null, diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index bf2e68b71cd3..595d8404e3aa 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -19,6 +19,7 @@ import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils"; import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -120,6 +121,7 @@ export class AddEditComponent implements OnInit, OnDestroy { protected folderService: FolderService, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected clipboardService: ClipboardService, protected auditService: AuditService, protected accountService: AccountService, protected collectionService: CollectionService, @@ -778,7 +780,7 @@ export class AddEditComponent implements OnInit, OnDestroy { } const copyOptions = this.win != null ? { window: this.win } : null; - this.platformUtilsService.copyToClipboard(value, copyOptions); + this.clipboardService.copyToClipboard(value, copyOptions); this.platformUtilsService.showToast( "info", null, diff --git a/libs/angular/src/vault/components/password-history.component.ts b/libs/angular/src/vault/components/password-history.component.ts index 942a34c58bbd..cd12329a93a1 100644 --- a/libs/angular/src/vault/components/password-history.component.ts +++ b/libs/angular/src/vault/components/password-history.component.ts @@ -4,6 +4,7 @@ import { Directive, OnInit } from "@angular/core"; import { firstValueFrom, map } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -17,6 +18,7 @@ export class PasswordHistoryComponent implements OnInit { constructor( protected cipherService: CipherService, protected platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, protected i18nService: I18nService, protected accountService: AccountService, private win: Window, @@ -28,7 +30,7 @@ export class PasswordHistoryComponent implements OnInit { copy(password: string) { const copyOptions = this.win != null ? { window: this.win } : null; - this.platformUtilsService.copyToClipboard(password, copyOptions); + this.clipboardService.copyToClipboard(password, copyOptions); this.platformUtilsService.showToast( "info", null, diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index ef9aff736edd..d1ae69e9e454 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -22,6 +22,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { EventType } from "@bitwarden/common/enums"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -33,7 +34,7 @@ import { CollectionId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; -import { FieldType, CipherType } from "@bitwarden/common/vault/enums"; +import { CipherType, FieldType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { Launchable } from "@bitwarden/common/vault/interfaces/launchable"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; @@ -100,6 +101,7 @@ export class ViewComponent implements OnDestroy, OnInit { protected keyService: KeyService, protected encryptService: EncryptService, protected platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, protected auditService: AuditService, protected win: Window, protected broadcasterService: BroadcasterService, @@ -384,7 +386,7 @@ export class ViewComponent implements OnDestroy, OnInit { } const copyOptions = this.win != null ? { window: this.win } : null; - this.platformUtilsService.copyToClipboard(value, copyOptions); + this.clipboardService.copyToClipboard(value, copyOptions); this.platformUtilsService.showToast( "info", null, diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts index 1052fe504d4c..9d21ed62e084 100644 --- a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts +++ b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts @@ -1,10 +1,11 @@ import { mock } from "jest-mock-extended"; import { lastValueFrom } from "rxjs"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; + import { ApiService } from "../../../abstractions/api.service"; import { ListResponse } from "../../../models/response/list.response"; import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { OrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/organization-domain-sso-details.response"; import { OrganizationDomainResponse } from "../../abstractions/organization-domain/responses/organization-domain.response"; import { VerifiedOrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/verified-organization-domain-sso-details.response"; @@ -103,11 +104,11 @@ describe("Org Domain API Service", () => { let orgDomainService: OrgDomainService; - const platformUtilService = mock(); + const clipboardService = mock(); const i18nService = mock(); beforeEach(() => { - orgDomainService = new OrgDomainService(platformUtilService, i18nService); + orgDomainService = new OrgDomainService(clipboardService, i18nService); jest.resetAllMocks(); orgDomainApiService = new OrgDomainApiService(orgDomainService, apiService); diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain.service.spec.ts b/libs/common/src/admin-console/services/organization-domain/org-domain.service.spec.ts index 21027334fb81..b72528389592 100644 --- a/libs/common/src/admin-console/services/organization-domain/org-domain.service.spec.ts +++ b/libs/common/src/admin-console/services/organization-domain/org-domain.service.spec.ts @@ -1,8 +1,9 @@ import { mock, mockReset } from "jest-mock-extended"; import { lastValueFrom } from "rxjs"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; + import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { OrganizationDomainResponse } from "../../abstractions/organization-domain/responses/organization-domain.response"; import { OrgDomainService } from "./org-domain.service"; @@ -60,14 +61,14 @@ const mockedExtraOrgDomainResponse = new OrganizationDomainResponse( describe("Org Domain Service", () => { let orgDomainService: OrgDomainService; - const platformUtilService = mock(); + const clipboardService = mock(); const i18nService = mock(); beforeEach(() => { - mockReset(platformUtilService); + mockReset(clipboardService); mockReset(i18nService); - orgDomainService = new OrgDomainService(platformUtilService, i18nService); + orgDomainService = new OrgDomainService(clipboardService, i18nService); }); it("instantiates", () => { @@ -179,6 +180,6 @@ describe("Org Domain Service", () => { it("copyDnsTxt copies DNS TXT to clipboard and shows toast", () => { orgDomainService.copyDnsTxt("fakeTxt"); - expect(jest.spyOn(platformUtilService, "copyToClipboard")).toHaveBeenCalled(); + expect(jest.spyOn(clipboardService, "copyToClipboard")).toHaveBeenCalled(); }); }); diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain.service.ts b/libs/common/src/admin-console/services/organization-domain/org-domain.service.ts index 0ffa106d0011..8607b0f72d62 100644 --- a/libs/common/src/admin-console/services/organization-domain/org-domain.service.ts +++ b/libs/common/src/admin-console/services/organization-domain/org-domain.service.ts @@ -2,8 +2,8 @@ // @ts-strict-ignore import { BehaviorSubject } from "rxjs"; +import { ClipboardService } from "../../../platform/abstractions/clipboard.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { OrgDomainInternalServiceAbstraction } from "../../abstractions/organization-domain/org-domain.service.abstraction"; import { OrganizationDomainResponse } from "../../abstractions/organization-domain/responses/organization-domain.response"; @@ -13,7 +13,7 @@ export class OrgDomainService implements OrgDomainInternalServiceAbstraction { orgDomains$ = this._orgDomains$.asObservable(); constructor( - private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private i18nService: I18nService, ) {} @@ -24,7 +24,7 @@ export class OrgDomainService implements OrgDomainInternalServiceAbstraction { } copyDnsTxt(dnsTxt: string): void { - this.platformUtilsService.copyToClipboard(dnsTxt); + this.clipboardService.copyToClipboard(dnsTxt); } upsert(orgDomains: OrganizationDomainResponse[]): void { diff --git a/libs/common/src/platform/abstractions/clipboard.service.ts b/libs/common/src/platform/abstractions/clipboard.service.ts new file mode 100644 index 000000000000..983382960405 --- /dev/null +++ b/libs/common/src/platform/abstractions/clipboard.service.ts @@ -0,0 +1,12 @@ +export type ClipboardOptions = { + allowHistory?: boolean; + clearing?: boolean; + clearMs?: number; + window?: Window; +}; + +export abstract class ClipboardService { + abstract copyToClipboard(text: string, options?: ClipboardOptions): void | boolean; + abstract readFromClipboard(): Promise; + abstract clearClipboard(clipboardValue: string, timeoutMs?: number): Promise; +} diff --git a/libs/common/src/platform/abstractions/platform-utils.service.ts b/libs/common/src/platform/abstractions/platform-utils.service.ts index fa0fc8f2501e..c54ad668d3f0 100644 --- a/libs/common/src/platform/abstractions/platform-utils.service.ts +++ b/libs/common/src/platform/abstractions/platform-utils.service.ts @@ -41,8 +41,6 @@ export abstract class PlatformUtilsService { ): void; abstract isDev(): boolean; abstract isSelfHost(): boolean; - abstract copyToClipboard(text: string, options?: ClipboardOptions): void | boolean; - abstract readFromClipboard(): Promise; abstract supportsSecureStorage(): boolean; abstract getAutofillKeyboardShortcut(): Promise; } diff --git a/libs/common/src/platform/services/system.service.ts b/libs/common/src/platform/services/system.service.ts index b12bfbd27540..2c4cb64bc707 100644 --- a/libs/common/src/platform/services/system.service.ts +++ b/libs/common/src/platform/services/system.service.ts @@ -3,7 +3,7 @@ import { firstValueFrom, Subscription } from "rxjs"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; -import { PlatformUtilsService } from "../abstractions/platform-utils.service"; +import { ClipboardService } from "../abstractions/clipboard.service"; import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service"; import { Utils } from "../misc/utils"; import { ScheduledTaskNames } from "../scheduling/scheduled-task-name.enum"; @@ -14,7 +14,7 @@ export class SystemService implements SystemServiceAbstraction { private clearClipboardTimeoutFunction: () => Promise = null; constructor( - private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private taskSchedulerService: TaskSchedulerService, ) { @@ -44,9 +44,9 @@ export class SystemService implements SystemServiceAbstraction { } this.clearClipboardTimeoutFunction = async () => { - const clipboardValueNow = await this.platformUtilsService.readFromClipboard(); + const clipboardValueNow = await this.clipboardService.readFromClipboard(); if (clipboardValue === clipboardValueNow) { - this.platformUtilsService.copyToClipboard("", { clearing: true }); + this.clipboardService.copyToClipboard("", { clearing: true }); } }; diff --git a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.spec.ts b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.spec.ts index d3651fc12cbe..91b1d8b81fb4 100644 --- a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.spec.ts +++ b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.spec.ts @@ -5,10 +5,10 @@ import { MockProxy, mock } from "jest-mock-extended"; import { of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; @@ -58,7 +58,7 @@ describe("SendListItemsContainerComponent", () => { { provide: EnvironmentService, useValue: environmentService }, { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: LogService, useValue: mock() }, - { provide: PlatformUtilsService, useValue: { copyToClipboard } }, + { provide: ClipboardService, useValue: { copyToClipboard } }, { provide: SendApiService, useValue: { delete: deleteFn } }, { provide: ToastService, useValue: { showToast } }, { provide: SendService, useValue: sendService }, diff --git a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.ts b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.ts index ab73e71c4ab2..1d732f8311eb 100644 --- a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.ts +++ b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.ts @@ -6,10 +6,10 @@ import { RouterLink } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; @@ -58,7 +58,7 @@ export class SendListItemsContainerComponent { protected environmentService: EnvironmentService, protected i18nService: I18nService, protected logService: LogService, - protected platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, protected sendApiService: SendApiService, protected toastService: ToastService, ) {} @@ -90,7 +90,7 @@ export class SendListItemsContainerComponent { async copySendLink(send: SendView) { const env = await firstValueFrom(this.environmentService.environment$); const link = env.getSendUrl() + send.accessId + "/" + send.urlB64Key; - this.platformUtilsService.copyToClipboard(link); + this.clipboardService.copyToClipboard(link); this.toastService.showToast({ variant: "success", title: null, diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts index 9bb96d1accd7..610d38e83b07 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts @@ -9,6 +9,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { EventType } from "@bitwarden/common/enums"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { UserId } from "@bitwarden/common/types/guid"; @@ -73,6 +74,7 @@ describe("LoginCredentialsViewComponent", () => { { provide: PremiumUpgradePromptService, useValue: mock() }, { provide: EventCollectionService, useValue: mock({ collect }) }, { provide: PlatformUtilsService, useValue: mock() }, + { provide: ClipboardService, useValue: mock() }, { provide: ToastService, useValue: mock() }, { provide: I18nService, useValue: { t: (...keys: string[]) => keys.join(" ") } }, ], diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts index 21f82234b2cb..e0bc52a01334 100644 --- a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts +++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts @@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { mock } from "jest-mock-extended"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -24,6 +25,7 @@ describe("ViewIdentitySectionsComponent", () => { providers: [ { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: PlatformUtilsService, useValue: mock() }, + { provide: ClipboardService, useValue: mock() }, ], }).compileComponents(); }); diff --git a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts index fb90df4d6ace..a3617e50dd58 100644 --- a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts +++ b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts @@ -4,6 +4,7 @@ import { BehaviorSubject } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -41,6 +42,7 @@ describe("PasswordHistoryViewComponent", () => { providers: [ { provide: CipherService, useValue: mockCipherService }, { provide: PlatformUtilsService }, + { provide: ClipboardService }, { provide: AccountService, useValue: { activeAccount$ } }, { provide: I18nService, useValue: { t: (key: string) => key } }, ], diff --git a/libs/vault/src/services/copy-cipher-field.service.spec.ts b/libs/vault/src/services/copy-cipher-field.service.spec.ts index 5a273c0828f0..3bfe88b1c6f1 100644 --- a/libs/vault/src/services/copy-cipher-field.service.spec.ts +++ b/libs/vault/src/services/copy-cipher-field.service.spec.ts @@ -2,11 +2,11 @@ import { mock, MockProxy } from "jest-mock-extended"; import { of } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherRepromptType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -16,7 +16,7 @@ import { CopyAction, CopyCipherFieldService, PasswordRepromptService } from "@bi describe("CopyCipherFieldService", () => { let service: CopyCipherFieldService; - let platformUtilsService: MockProxy; + let clipboardService: MockProxy; let toastService: MockProxy; let eventCollectionService: MockProxy; let passwordRepromptService: MockProxy; @@ -27,7 +27,7 @@ describe("CopyCipherFieldService", () => { const userId = "userId"; beforeEach(() => { - platformUtilsService = mock(); + clipboardService = mock(); toastService = mock(); eventCollectionService = mock(); passwordRepromptService = mock(); @@ -39,7 +39,7 @@ describe("CopyCipherFieldService", () => { accountService.activeAccount$ = of({ id: userId } as Account); service = new CopyCipherFieldService( - platformUtilsService, + clipboardService, toastService, eventCollectionService, passwordRepromptService, @@ -67,13 +67,13 @@ describe("CopyCipherFieldService", () => { valueToCopy = null; const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); expect(result).toBeFalsy(); - expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled(); + expect(clipboardService.copyToClipboard).not.toHaveBeenCalled(); }); it("should copy value to clipboard", async () => { const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); expect(result).toBeTruthy(); - expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith(valueToCopy); + expect(clipboardService.copyToClipboard).toHaveBeenCalledWith(valueToCopy); }); it("should show a success toast on copy", async () => { @@ -107,7 +107,7 @@ describe("CopyCipherFieldService", () => { const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); expect(result).toBeTruthy(); expect(passwordRepromptService.showPasswordPrompt).not.toHaveBeenCalled(); - expect(platformUtilsService.copyToClipboard).toHaveBeenCalled(); + expect(clipboardService.copyToClipboard).toHaveBeenCalled(); }); it("should skip password prompt when skipReprompt is true", async () => { @@ -121,7 +121,7 @@ describe("CopyCipherFieldService", () => { passwordRepromptService.showPasswordPrompt.mockResolvedValue(false); const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); expect(result).toBeFalsy(); - expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled(); + expect(clipboardService.copyToClipboard).not.toHaveBeenCalled(); }); }); @@ -140,7 +140,7 @@ describe("CopyCipherFieldService", () => { const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); expect(result).toBeTruthy(); expect(totpService.getCode).toHaveBeenCalledWith(valueToCopy); - expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith("123456"); + expect(clipboardService.copyToClipboard).toHaveBeenCalledWith("123456"); expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith( userId, ); @@ -152,7 +152,7 @@ describe("CopyCipherFieldService", () => { const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); expect(result).toBeTruthy(); expect(totpService.getCode).toHaveBeenCalledWith(valueToCopy); - expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith("123456"); + expect(clipboardService.copyToClipboard).toHaveBeenCalledWith("123456"); }); it("should return early when the user is not allowed to use TOTP", async () => { @@ -160,7 +160,7 @@ describe("CopyCipherFieldService", () => { const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); expect(result).toBeFalsy(); expect(totpService.getCode).not.toHaveBeenCalled(); - expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled(); + expect(clipboardService.copyToClipboard).not.toHaveBeenCalled(); expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith( userId, ); @@ -171,7 +171,7 @@ describe("CopyCipherFieldService", () => { const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); expect(result).toBeFalsy(); expect(totpService.getCode).not.toHaveBeenCalled(); - expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled(); + expect(clipboardService.copyToClipboard).not.toHaveBeenCalled(); }); }); diff --git a/libs/vault/src/services/copy-cipher-field.service.ts b/libs/vault/src/services/copy-cipher-field.service.ts index 2805f3e7541f..6768c6b98903 100644 --- a/libs/vault/src/services/copy-cipher-field.service.ts +++ b/libs/vault/src/services/copy-cipher-field.service.ts @@ -5,8 +5,8 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; +import { ClipboardService } from "@bitwarden/common/platform/abstractions/clipboard.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherRepromptType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -81,7 +81,7 @@ const CopyActions: Record = { }) export class CopyCipherFieldService { constructor( - private platformUtilsService: PlatformUtilsService, + private clipboardService: ClipboardService, private toastService: ToastService, private eventCollectionService: EventCollectionService, private passwordRepromptService: PasswordRepromptService, @@ -127,7 +127,7 @@ export class CopyCipherFieldService { valueToCopy = await this.totpService.getCode(valueToCopy); } - this.platformUtilsService.copyToClipboard(valueToCopy); + this.clipboardService.copyToClipboard(valueToCopy); this.toastService.showToast({ variant: "success", message: this.i18nService.t("valueCopied", this.i18nService.t(action.typeI18nKey)),