diff --git a/components/brave_wallet/browser/BUILD.gn b/components/brave_wallet/browser/BUILD.gn index b2920c7bc1e..93d6922ea39 100644 --- a/components/brave_wallet/browser/BUILD.gn +++ b/components/brave_wallet/browser/BUILD.gn @@ -343,7 +343,8 @@ static_library("browser") { "//third_party/boringssl", "//third_party/re2", "//tools/json_schema_compiler:generated_api_util", - "//ui/base:base", + "//ui/base", + "//ui/base/clipboard", "//url", ] diff --git a/components/brave_wallet/browser/brave_wallet_service.cc b/components/brave_wallet/browser/brave_wallet_service.cc index b25790dd782..e86642cac42 100644 --- a/components/brave_wallet/browser/brave_wallet_service.cc +++ b/components/brave_wallet/browser/brave_wallet_service.cc @@ -17,6 +17,7 @@ #include "base/logging.h" #include "base/notreached.h" #include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" #include "base/values.h" #include "brave/components/brave_wallet/browser/account_discovery_manager.h" #include "brave/components/brave_wallet/browser/bitcoin/bitcoin_wallet_service.h" @@ -44,6 +45,7 @@ #include "components/prefs/scoped_user_pref_update.h" #include "components/regional_capabilities/regional_capabilities_prefs.h" #include "services/network/public/cpp/shared_url_loader_factory.h" +#include "ui/base/clipboard/scoped_clipboard_writer.h" #include "ui/base/l10n/l10n_util.h" #include "url/origin.h" @@ -1959,6 +1961,26 @@ void BraveWalletService::SetTransactionSimulationOptInStatus( ::brave_wallet::SetTransactionSimulationOptInStatus(profile_prefs_, status); } +void BraveWalletService::WriteToClipboard(const std::string& text, + bool is_sensitive) { + // We manually disable the iOS builds here because of an upstream bug in how + // Chromium is adding sources to the clipboard component. It only + // conditionally adds the iOS sources when use_blink=true, which unfortunately + // leads to a whole slew of unresolved symbols during linking. + // https://source.chromium.org/chromium/chromium/src/+/066b9c51bfb0a1eddcfefa7aa809348ea181f8ac:ui/base/clipboard/BUILD.gn;l=21-27 +#if !BUILDFLAG(IS_IOS) + ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste); + std::u16string out; + base::UTF8ToUTF16(text.data(), text.size(), &out); + scw.WriteText(out); + if (is_sensitive) { + scw.MarkAsConfidential(); + } +#else + NOTREACHED(); +#endif +} + base::CallbackListSubscription BraveWalletService::RegisterSignMessageRequestAddedCallback( base::RepeatingClosure cb) { diff --git a/components/brave_wallet/browser/brave_wallet_service.h b/components/brave_wallet/browser/brave_wallet_service.h index 63f9b6de52d..11ffd954f33 100644 --- a/components/brave_wallet/browser/brave_wallet_service.h +++ b/components/brave_wallet/browser/brave_wallet_service.h @@ -269,6 +269,8 @@ class BraveWalletService : public KeyedService, void SetTransactionSimulationOptInStatus( mojom::BlowfishOptInStatus status) override; + void WriteToClipboard(const std::string& text, bool is_sensitive) override; + // BraveWalletServiceDelegate::Observer: void OnActiveOriginChanged(const mojom::OriginInfoPtr& origin_info) override; diff --git a/components/brave_wallet/common/brave_wallet.mojom b/components/brave_wallet/common/brave_wallet.mojom index 56645f20c19..360b6b78dc4 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -1328,7 +1328,7 @@ interface MeldIntegrationService { // destination_currency_code, country, source_amount, source_currency_code GetCryptoQuotes(string country, string source_currency_code, string destination_currency_code, double source_amount, string? account, - string? payment_method) + string? payment_method) => (array? crypto_quotes, array? error); // Obtains the list of payment methods @@ -2773,6 +2773,8 @@ interface BraveWalletService { GetTransactionSimulationOptInStatus() => (BlowfishOptInStatus status); SetTransactionSimulationOptInStatus(BlowfishOptInStatus status); + + WriteToClipboard(string text, bool is_sensitive); }; // For reporting wallet related P3A metrics. diff --git a/components/brave_wallet_ui/common/async/__mocks__/bridge.ts b/components/brave_wallet_ui/common/async/__mocks__/bridge.ts index 82d28f2762c..4195b27eb49 100644 --- a/components/brave_wallet_ui/common/async/__mocks__/bridge.ts +++ b/components/brave_wallet_ui/common/async/__mocks__/bridge.ts @@ -513,6 +513,12 @@ export class MockedWalletApiProxy { } return { network: mockEthMainnet } }, + + writeToClipboard: async (text: string, isConfidential: boolean) => { + return { + data: true, + } + }, } swapService: Partial> = diff --git a/components/brave_wallet_ui/common/hooks/use-copy-to-clipboard.test.tsx b/components/brave_wallet_ui/common/hooks/use-copy-to-clipboard.test.tsx index 166e6e54ecf..52bbb736705 100644 --- a/components/brave_wallet_ui/common/hooks/use-copy-to-clipboard.test.tsx +++ b/components/brave_wallet_ui/common/hooks/use-copy-to-clipboard.test.tsx @@ -8,14 +8,40 @@ import { useTemporaryCopyToClipboard, } from './use-copy-to-clipboard' +// Utils +import { + createMockStore, + renderHookOptionsWithMockStore, +} from '../../utils/test-utils' + describe('useCopyToClipboard Hook', () => { it('should have false as initial state', () => { - const { result } = renderHook(() => useCopyToClipboard()) + const store = createMockStore({}) + const renderOptions = renderHookOptionsWithMockStore(store) + + const { result } = renderHook(() => useCopyToClipboard(), renderOptions) expect(result.current.isCopied).toBe(false) }) it('should change copied to true when copyText is called', async () => { - const { result } = renderHook(() => useCopyToClipboard()) + const store = createMockStore({}) + const renderOptions = renderHookOptionsWithMockStore(store) + + const { result } = renderHook(() => useCopyToClipboard(), renderOptions) + await act(async () => { + await result.current.copyToClipboard('some text') + }) + expect(result.current.isCopied).toBe(true) + }) + + it('should copy to clipboard confidentially', async () => { + const store = createMockStore({}) + const renderOptions = renderHookOptionsWithMockStore(store) + + const { result } = renderHook( + () => useCopyToClipboard(undefined, true), + renderOptions, + ) await act(async () => { await result.current.copyToClipboard('some text') }) @@ -29,9 +55,12 @@ describe('useTemporaryCopyToClipboard Hook', () => { jest.clearAllTimers() const timeoutTime = 5000 // 5 seconds + const store = createMockStore({}) + const renderOptions = renderHookOptionsWithMockStore(store) - const { result } = renderHook(() => - useTemporaryCopyToClipboard(timeoutTime), + const { result } = renderHook( + () => useTemporaryCopyToClipboard(timeoutTime), + renderOptions, ) await act(async () => { diff --git a/components/brave_wallet_ui/common/hooks/use-copy-to-clipboard.ts b/components/brave_wallet_ui/common/hooks/use-copy-to-clipboard.ts index 5e184054d7e..2953d2c2bfb 100644 --- a/components/brave_wallet_ui/common/hooks/use-copy-to-clipboard.ts +++ b/components/brave_wallet_ui/common/hooks/use-copy-to-clipboard.ts @@ -7,21 +7,36 @@ import * as React from 'react' // utils import { copyToClipboard } from '../../utils/copy-to-clipboard' +import { + useCopyToClipboardConfidentiallyMutation, // +} from '../slices/api.slice' const temporaryCopyTimeout = 5000 // 5s const copiedMessageTimeout = 1500 // 1.5s export const useTemporaryCopyToClipboard = ( timeoutMs: number = temporaryCopyTimeout, + isConfidential = false, ) => { + // mutations + const [copyToClipboardConfidentially] = + useCopyToClipboardConfidentiallyMutation() + // state const [isCopied, setIsCopied] = React.useState(false) // methods - const temporaryCopyToClipboard = React.useCallback(async (value: string) => { - await copyToClipboard(value) - setIsCopied(true) - }, []) + const temporaryCopyToClipboard = React.useCallback( + async (value: string) => { + if (isConfidential) { + await copyToClipboardConfidentially({ text: value }) + } else { + await copyToClipboard(value) + } + setIsCopied(true) + }, + [isConfidential, copyToClipboardConfidentially], + ) // effects React.useEffect(() => { @@ -32,7 +47,11 @@ export const useTemporaryCopyToClipboard = ( // clear the clipboard after a set time const timer = window.setTimeout(async () => { - await copyToClipboard('') + if (isConfidential) { + await copyToClipboardConfidentially({ text: '' }) + } else { + await copyToClipboard('') + } setIsCopied(false) }, timeoutMs) @@ -40,7 +59,7 @@ export const useTemporaryCopyToClipboard = ( return () => { timer && clearTimeout(timer) } - }, [isCopied, timeoutMs]) + }, [isCopied, timeoutMs, isConfidential, copyToClipboardConfidentially]) return { temporaryCopyToClipboard, @@ -48,15 +67,29 @@ export const useTemporaryCopyToClipboard = ( } } -export const useCopyToClipboard = (timeoutMs = copiedMessageTimeout) => { +export const useCopyToClipboard = ( + timeoutMs = copiedMessageTimeout, + isConfidential = false, +) => { + // mutations + const [copyToClipboardConfidentially] = + useCopyToClipboardConfidentiallyMutation() + // state const [isCopied, setIsCopied] = React.useState(false) // methods - const _copyToClipboard = React.useCallback(async (value: string) => { - await copyToClipboard(value) - setIsCopied(true) - }, []) + const _copyToClipboard = React.useCallback( + async (value: string) => { + if (isConfidential) { + await copyToClipboardConfidentially({ text: value }) + } else { + await copyToClipboard(value) + } + setIsCopied(true) + }, + [isConfidential, copyToClipboardConfidentially], + ) const resetCopyState = React.useCallback(() => { setIsCopied(false) diff --git a/components/brave_wallet_ui/common/slices/api.slice.ts b/components/brave_wallet_ui/common/slices/api.slice.ts index fa270af6b88..c10893d4ab2 100644 --- a/components/brave_wallet_ui/common/slices/api.slice.ts +++ b/components/brave_wallet_ui/common/slices/api.slice.ts @@ -312,6 +312,7 @@ export const { useLazyGetTransactionsQuery, useLazyGetUserTokensRegistryQuery, useLazyGetZCashAccountInfoQuery, + useCopyToClipboardConfidentiallyMutation, useLockWalletMutation, useMakeAccountShieldedMutation, useNewUnapprovedTxAddedMutation, diff --git a/components/brave_wallet_ui/common/slices/endpoints/wallet.endpoints.ts b/components/brave_wallet_ui/common/slices/endpoints/wallet.endpoints.ts index d752d802f51..b0e436085d4 100644 --- a/components/brave_wallet_ui/common/slices/endpoints/wallet.endpoints.ts +++ b/components/brave_wallet_ui/common/slices/endpoints/wallet.endpoints.ts @@ -512,6 +512,16 @@ export const walletEndpoints = ({ } }, }), + + copyToClipboardConfidentially: mutation({ + queryFn: async ({ text }, { endpoint }, extraOptions, baseQuery) => { + const { data: api } = baseQuery(undefined) + api.braveWalletService.writeToClipboard(text, true) + return { + data: true, + } + }, + }), } } diff --git a/components/brave_wallet_ui/components/desktop/popup-modals/account-settings-modal/account-settings-modal.tsx b/components/brave_wallet_ui/components/desktop/popup-modals/account-settings-modal/account-settings-modal.tsx index f924e5bd207..fa3f7ac20f5 100644 --- a/components/brave_wallet_ui/components/desktop/popup-modals/account-settings-modal/account-settings-modal.tsx +++ b/components/brave_wallet_ui/components/desktop/popup-modals/account-settings-modal/account-settings-modal.tsx @@ -427,7 +427,10 @@ export const AccountSettingsModal = () => { === BraveWallet.CoinType.FIL && ( {filPrivateKeyFormatDescription} )} - + {privateKey} diff --git a/components/brave_wallet_ui/components/shared/copy-tooltip/copy-tooltip.tsx b/components/brave_wallet_ui/components/shared/copy-tooltip/copy-tooltip.tsx index 6de355a5716..43be60e1efe 100644 --- a/components/brave_wallet_ui/components/shared/copy-tooltip/copy-tooltip.tsx +++ b/components/brave_wallet_ui/components/shared/copy-tooltip/copy-tooltip.tsx @@ -22,6 +22,7 @@ type Props = { tooltipText?: string actionText?: string text?: string + isConfidential?: boolean } & ToolTipProps export const CopyTooltip = ({ @@ -29,9 +30,10 @@ export const CopyTooltip = ({ tooltipText, actionText, text, + isConfidential, ...tipProps }: Props) => { - const { isCopied, copyToClipboard } = useCopyToClipboard(1500) + const { isCopied, copyToClipboard } = useCopyToClipboard(1500, isConfidential) const handleClick = React.useCallback(async () => { if (text) { diff --git a/components/brave_wallet_ui/page/screens/backup-wallet/backup-recovery-phrase/backup-recovery-phrase.tsx b/components/brave_wallet_ui/page/screens/backup-wallet/backup-recovery-phrase/backup-recovery-phrase.tsx index ca1f983090b..d2333dd09f5 100644 --- a/components/brave_wallet_ui/page/screens/backup-wallet/backup-recovery-phrase/backup-recovery-phrase.tsx +++ b/components/brave_wallet_ui/page/screens/backup-wallet/backup-recovery-phrase/backup-recovery-phrase.tsx @@ -71,7 +71,10 @@ export const BackupRecoveryPhrase = () => { const [report] = useReportOnboardingActionMutation() // custom hooks - const { isCopied, temporaryCopyToClipboard } = useTemporaryCopyToClipboard() + const { isCopied, temporaryCopyToClipboard } = useTemporaryCopyToClipboard( + undefined, + true, + ) // methods const skipBackup = () => { diff --git a/ios/brave-ios/Sources/BraveWallet/Preview Content/MockBraveWalletService.swift b/ios/brave-ios/Sources/BraveWallet/Preview Content/MockBraveWalletService.swift index 3ed004d2309..ec148c8b9e9 100644 --- a/ios/brave-ios/Sources/BraveWallet/Preview Content/MockBraveWalletService.swift +++ b/ios/brave-ios/Sources/BraveWallet/Preview Content/MockBraveWalletService.swift @@ -401,6 +401,9 @@ class MockBraveWalletService: BraveWalletBraveWalletService { func discoverAssetsOnAllSupportedChains(bypassRateLimit: Bool) { } + func writeToClipboard(text: String, isSensitive: Bool) { + } + func transactionSimulationOptInStatus( completion: @escaping (BraveWallet.BlowfishOptInStatus) -> Void ) {