Skip to content

fix(clipboard): show toast feedback on copy failures - #1385

Open
Rafiat30 wants to merge 1 commit into
LabsCrypt:mainfrom
Rafiat30:fix/clipboard-copy-feedback
Open

fix(clipboard): show toast feedback on copy failures#1385
Rafiat30 wants to merge 1 commit into
LabsCrypt:mainfrom
Rafiat30:fix/clipboard-copy-feedback

Conversation

@Rafiat30

Copy link
Copy Markdown

Closes #1225

Summary

Two "copy address" buttons called navigator.clipboard.writeText(...) directly with no .then/.catch and no toast feedback (a third copy button — the settings page "Contract Address" copier — had the same problem: it fired toast.success unconditionally and never handled a rejected write). If the browser denied clipboard permission, or the Clipboard API was unavailable, the failure was swallowed silently and the user had no idea the copy didn't happen.

This PR adds a shared copyToClipboard(text, options?) utility in frontend/src/lib/clipboard.ts and uses it at every copy-address call site in the app, so a failed write always surfaces an error toast and a successful write always surfaces a success toast.

New files

  • frontend/src/lib/clipboard.ts — the shared copyToClipboard utility.
  • frontend/src/lib/clipboard.test.ts — unit tests for the utility.
  • frontend/src/app/settings/settings-content.test.tsx — render tests covering both copy buttons on the settings page.
  • frontend/src/components/wallet/WalletButton.test.tsx — render tests for the wallet dropdown's copy button.
  • frontend/src/components/dashboard/StreamDetailsModal.test.tsx — render tests for the recipient-address copy button.

Modified files

  • frontend/src/app/settings/settings-content.tsxcopyAddress (wallet address) and the "Contract Address" copy button now go through copyToClipboard.
  • frontend/src/components/wallet/WalletButton.tsxhandleCopy now uses copyToClipboard instead of a bare try/catch that silently swallowed errors.
  • frontend/src/components/dashboard/StreamDetailsModal.tsx — the recipient-address copy button now uses copyToClipboard and gets the same copied-state icon swap (checkmark, 1.5s) used elsewhere in the app.

Test files

  • frontend/src/lib/clipboard.test.ts
  • frontend/src/app/settings/settings-content.test.tsx
  • frontend/src/components/wallet/WalletButton.test.tsx
  • frontend/src/components/dashboard/StreamDetailsModal.test.tsx

(The first is new coverage for the utility itself; the other three are new render-level coverage for each call site — none of these components had a test file before.)

Implementation details

  • Why a shared util instead of fixing each call site inline: all four copy buttons need identical behavior (await the write, toast on success/failure, never throw into the caller), so a single well-tested helper avoids four slightly different try/catch blocks drifting out of sync over time.
  • copyToClipboard(text, options?) in frontend/src/lib/clipboard.ts:
    • Guards against the Clipboard API being entirely unavailable (navigator.clipboard missing, insecure context, unsupported browser) by treating that the same as a rejected write.
    • await navigator.clipboard.writeText(text); on success calls toast.success(options?.successMessage ?? "Copied to clipboard") and resolves true.
    • On any failure (rejection or unavailable API) calls toast.error(options?.errorMessage ?? "Failed to copy to clipboard") and resolves false. It never throws, so no caller needs its own try/catch.
    • options.successMessage / options.errorMessage let each call site keep its existing, more specific copy (e.g. "Address copied to clipboard", "Recipient address copied", "Contract address copied") while sharing the same success/failure plumbing.
  • Call sites all follow the same pattern now: call copyToClipboard(text, { successMessage }), and only flip local "copied" UI state (checkmark icon, "Copied!" label, tooltip) when the returned promise resolves true — so the button never shows a false "copied" state after a failed write.

Tests added

  • clipboard.test.ts: writes text and shows a success toast on success; uses a custom success message when provided; shows an error toast (and does not throw) when writeText rejects, e.g. permission denied; uses a custom error message when provided; shows an error toast when navigator.clipboard is unavailable entirely.
  • settings-content.test.tsx: for both the wallet-address copy button and the contract-address copy button — shows an error toast (not a silent failure) when the write is denied, and shows a success toast (plus the "Address copied" label swap for the wallet address button) when the write succeeds.
  • WalletButton.test.tsx: shows an error toast instead of failing silently when the clipboard write is denied (and confirms the local "Copied!" label never fires on failure); shows a success toast and toggles the "Copied!" label when the write succeeds.
  • StreamDetailsModal.test.tsx: shows an error toast instead of failing silently when the clipboard write is denied; shows a success toast (plus the checkmark icon swap) when the write succeeds.

One non-obvious detail these tests had to work around: @testing-library/user-event's setup() installs its own internal clipboard stub via Object.defineProperty on navigator.clipboard. If a test defines its own navigator.clipboard mock before calling userEvent.setup(), setup() silently clobbers it, so the click ends up going through user-event's stub instead of the test's mock (the effect is that the "denied" case looks like it "succeeds" because it never rejects, and writeText is never actually invoked/observed). All four test files call userEvent.setup() first and install the navigator.clipboard mock afterward, which is required for the mocked rejection to actually reach the component under test.

Manual test plan

For each of the three call sites, use Chrome/Edge DevTools to simulate a clipboard permission denial:

  1. Open DevTools → the "..." menu → More tools → Sensors, or open chrome://settings/content/clipboard and block clipboard access for localhost. (Alternatively, in the DevTools Console, run Object.defineProperty(navigator.clipboard, 'writeText', { value: () => Promise.reject(new Error('denied')) }) before clicking, which reliably forces the rejection path without touching browser permission state.)
  2. Settings page (/settings, wallet connected):
    • Click the copy icon next to the connected wallet address → expect a "Failed to copy to clipboard" error toast, and the icon should not switch to the checkmark/"Copied" state.
    • Click the copy icon next to "Contract Address" (under App Version / About) → expect the same error toast.
    • Restore clipboard access and repeat both clicks → expect "Address copied to clipboard" / "Contract address copied" success toasts and the checkmark/"Copied" feedback.
  3. Navbar wallet chip (any page, wallet connected): click the wallet chip to open the dropdown, click "Copy" next to the full public key → expect a "Failed to copy to clipboard" error toast and the button label should stay "Copy" (not "Copied!"). Restore clipboard access and repeat → expect a success toast and the label briefly changes to "Copied!".
  4. Stream details modal (dashboard → open a stream's details): click the copy icon next to the recipient address → expect a "Failed to copy to clipboard" error toast and no checkmark. Restore clipboard access and repeat → expect a "Recipient address copied" success toast and the checkmark icon briefly appears.

Automated coverage for all of the above (including the permission-denied path) is included in the four test files listed above; run npm test --workspace=frontend to execute them.

Copy-address buttons called navigator.clipboard.writeText directly with
no .then/.catch, so a denied clipboard permission (or an unavailable
Clipboard API) failed silently with no feedback to the user.

Add a shared copyToClipboard(text, options?) helper in lib/clipboard.ts
that awaits the write and shows a toast.success or toast.error
accordingly. It never throws, so callers don't need their own
try/catch. Use it at every copy-address call site:

- settings page: wallet address copy and contract address copy
- WalletButton dropdown: copy wallet address
- StreamDetailsModal: copy recipient address (also adds a brief
  copied-state icon swap, matching the existing wallet address pattern)

Add unit tests for the shared util and render tests for each call site
that simulate a rejected clipboard write and assert an error toast is
shown instead of failing silently.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Audit] Clipboard-copy actions give no success/failure feedback and swallow errors

2 participants