-
Notifications
You must be signed in to change notification settings - Fork 1
🎨 Palette: Add copy feedback to wallet addresses #152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,25 +36,16 @@ describe("Header", () => { | |
| lifecycleAction: null, | ||
| handlePauseResume: vi.fn(), | ||
| handleRestart: vi.fn(), | ||
| copyToClipboard: vi.fn(), | ||
| setTab: vi.fn(), | ||
| dropStatus: null, | ||
| loadDropStatus: vi.fn(), | ||
| registryStatus: null, | ||
| copyToClipboard: vi.fn(), // Needed for CopyButton | ||
| }; | ||
|
|
||
| // @ts-expect-error - test uses a narrowed subset of the full app context type. | ||
| vi.spyOn(AppContext, "useApp").mockReturnValue(mockUseApp); | ||
|
|
||
| // We need to render the component. | ||
| // Note: Since we are in a non-browser environment (happy-dom/jsdom might not be set up fully for standard React testing library in this repo's specific config), | ||
| // we will check if we can use react-test-renderer or if we should rely on a basic snapshot/class check. | ||
| // However, the user's package.json includes "react-test-renderer". | ||
| // Let's try react-test-renderer first as it avoids DOM emulation issues if not configured. | ||
|
|
||
| // Actually, let's stick to the plan of using what's available. | ||
| // The previous check showed "react-test-renderer": "^19.0.0". | ||
|
|
||
| let testRenderer: ReactTestRenderer | null = null; | ||
| await act(async () => { | ||
| testRenderer = create(<Header />); | ||
|
|
@@ -68,7 +59,6 @@ describe("Header", () => { | |
| node.props.className.includes(className); | ||
|
|
||
| // Find the wallet wrapper | ||
| // It has className "wallet-wrapper relative inline-flex shrink-0 group" | ||
| const walletWrapper = root.findAll((node: ReactTestInstance) => | ||
| hasClass(node, "wallet-wrapper"), | ||
| ); | ||
|
|
@@ -77,12 +67,23 @@ describe("Header", () => { | |
| expect(walletWrapper[0].props.className).toContain("group"); | ||
|
|
||
| // Find the wallet tooltip | ||
| // It should have className containing "group-hover:block" | ||
| const walletTooltip = root.findAll((node: ReactTestInstance) => | ||
| hasClass(node, "wallet-tooltip"), | ||
| ); | ||
|
|
||
| expect(walletTooltip.length).toBe(1); | ||
| expect(walletTooltip[0].props.className).toContain("group-hover:block"); | ||
|
|
||
| // Verify CopyButtons are rendered | ||
| const copyButtons = root.findAll((node: ReactTestInstance) => { | ||
| return ( | ||
| node.type === "button" && | ||
| node.props["aria-label"] && | ||
| node.props["aria-label"].startsWith("Copy") | ||
| ); | ||
| }); | ||
|
|
||
| // Should find 2 copy buttons (one for EVM, one for SOL) | ||
| expect(copyButtons.length).toBe(2); | ||
|
Comment on lines
+78
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Insufficient interaction testing for CopyButton The test only verifies the presence of copy buttons but does not check whether the Recommended solution: copyButtons[0].props.onClick();
expect(mockUseApp.copyToClipboard).toHaveBeenCalled(); |
||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ import { | |
| import { useEffect } from "react"; | ||
| import { useApp } from "../AppContext"; | ||
| import { useBugReport } from "../hooks/useBugReport"; | ||
| import { CopyButton } from "./shared/CopyButton"; | ||
|
|
||
| export function Header() { | ||
| const { | ||
|
|
@@ -25,7 +26,6 @@ export function Header() { | |
| lifecycleAction, | ||
| handlePauseResume, | ||
| handleRestart, | ||
| copyToClipboard, | ||
| setTab, | ||
| dropStatus, | ||
| loadDropStatus, | ||
|
|
@@ -204,19 +204,10 @@ export function Header() { | |
| <code className="font-mono flex-1 truncate"> | ||
| {evmShort} | ||
| </code> | ||
| <button | ||
| type="button" | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| const evmAddress = walletAddresses?.evmAddress; | ||
| if (evmAddress) { | ||
| copyToClipboard(evmAddress); | ||
| } | ||
| }} | ||
| className="px-1.5 py-1 border border-border bg-bg text-[10px] font-mono cursor-pointer hover:border-accent hover:text-accent" | ||
| > | ||
| copy | ||
| </button> | ||
| <CopyButton | ||
| value={walletAddresses?.evmAddress ?? ""} | ||
| label="copy" | ||
| /> | ||
| </div> | ||
| )} | ||
| {solShort && ( | ||
|
|
@@ -227,19 +218,10 @@ export function Header() { | |
| <code className="font-mono flex-1 truncate"> | ||
| {solShort} | ||
| </code> | ||
| <button | ||
| type="button" | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| const solanaAddress = walletAddresses?.solanaAddress; | ||
| if (solanaAddress) { | ||
| copyToClipboard(solanaAddress); | ||
| } | ||
| }} | ||
| className="px-1.5 py-1 border border-border bg-bg text-[10px] font-mono cursor-pointer hover:border-accent hover:text-accent" | ||
| > | ||
| copy | ||
| </button> | ||
| <CopyButton | ||
| value={walletAddresses?.solanaAddress ?? ""} | ||
| label="copy" | ||
| /> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security: Potential Exposure of Wallet AddressesThe code displays EVM and Solana wallet addresses in the UI (lines 204-227). If the Recommended Solution:
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { Check, Copy } from "lucide-react"; | ||
| import { useState } from "react"; | ||
| import { useApp } from "../../AppContext"; | ||
|
|
||
| interface CopyButtonProps { | ||
| value: string; | ||
| label?: string; | ||
| className?: string; | ||
| } | ||
|
|
||
| export function CopyButton({ | ||
| value, | ||
| label = "copy", | ||
| className = "", | ||
| }: CopyButtonProps) { | ||
| const { copyToClipboard } = useApp(); | ||
| const [copied, setCopied] = useState(false); | ||
|
|
||
| const handleCopy = async (e: React.MouseEvent) => { | ||
| e.stopPropagation(); | ||
| await copyToClipboard(value); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2000); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of |
||
| }; | ||
|
Comment on lines
+19
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The try {
await copyToClipboard(value);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
// Optionally set an error state or notify the user
}
Comment on lines
+19
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This implementation has a potential memory leak. If the To fix this, you should manage the timer with a Here's a suggested refactoring (you'll also need to import useEffect(() => {
if (!copied) return;
const timerId = setTimeout(() => {
setCopied(false);
}, 2000);
return () => clearTimeout(timerId);
}, [copied]);
const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
await copyToClipboard(value);
setCopied(true);
}; |
||
|
|
||
| return ( | ||
| <button | ||
| type="button" | ||
| onClick={handleCopy} | ||
| className={`px-1.5 py-1 border border-border bg-bg text-[10px] font-mono cursor-pointer transition-all duration-200 inline-flex items-center gap-1 ${ | ||
| copied | ||
| ? "border-ok text-ok bg-ok-subtle" | ||
| : "hover:border-accent hover:text-accent" | ||
| } ${className}`} | ||
| aria-label={copied ? "Copied" : `Copy ${label}`} | ||
| > | ||
| {copied ? ( | ||
| <> | ||
| <Check className="w-3 h-3" /> | ||
| <span>copied</span> | ||
| </> | ||
| ) : ( | ||
| <> | ||
| <Copy className="w-3 h-3" /> | ||
| <span>{label}</span> | ||
| </> | ||
| )} | ||
| </button> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -18,8 +18,8 @@ | |||||
| "forceConsistentCasingInFileNames": true, | ||||||
| "experimentalDecorators": true, | ||||||
| "useUnknownInCatchVariables": true, | ||||||
| "types": ["vite/client"] | ||||||
| "types": ["vite/client", "bun-types"] | ||||||
| }, | ||||||
| "include": ["src/**/*.ts", "src/**/*.tsx"], | ||||||
| "include": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts", "test/**/*.tsx"], | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test file To keep the configuration clean, you could remove the added test paths.
Suggested change
|
||||||
| "exclude": ["node_modules", "dist", "ios", "android", "electron"] | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TypeScript type assertion bypass (
@ts-expect-error)Using
@ts-expect-errorto bypass type checking for the mocked context (line 46) can lead to brittle tests and maintenance issues if the context shape changes. It is preferable to define a proper mock type or use partial typing to ensure type safety and reduce the risk of runtime errors.Recommended solution:
Define a mock type for the context or use
Partial<AppContextType>to avoid bypassing TypeScript checks: