diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 64d67e4c..73e6d7de 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -11,6 +11,7 @@ import { StellarProvider } from './context/StellarContext'
import { NetworkSwitcher } from './components/NetworkSwitcher'
import { LanguageSwitcher } from './components/LanguageSwitcher'
import { FundbotButton } from './components/FundbotButton'
+import { CopyButton } from './components/CopyButton'
import { useWallet } from './hooks/useWallet'
import { truncateAddress, formatXLM } from './utils/formatting'
import { NavBar } from './components/NavBar'
@@ -124,11 +125,9 @@ function AppContent() {
-
- {wallet.address && truncateAddress(wallet.address)}
+
+ {wallet.address && truncateAddress(wallet.address)}
+ {wallet.address && }
Transaction Successful
-
- {txHash.slice(0, 8)}...{txHash.slice(-8)}
-
+
)}
diff --git a/frontend/src/components/WalletConnectButton.tsx b/frontend/src/components/WalletConnectButton.tsx
index 9b987f4a..1b78f974 100644
--- a/frontend/src/components/WalletConnectButton.tsx
+++ b/frontend/src/components/WalletConnectButton.tsx
@@ -2,6 +2,7 @@ import { useWalletContext } from '../context/WalletContext'
import { truncateAddress, formatXLM } from '../utils/formatting'
import { Button } from './UI/Button'
import { Spinner } from './UI/Spinner'
+import { CopyButton } from './CopyButton'
export const WalletConnectButton: React.FC = () => {
const { wallet, isConnecting, isInstalled, connect, disconnect } = useWalletContext()
@@ -35,13 +36,11 @@ export const WalletConnectButton: React.FC = () => {
if (wallet.isConnected && wallet.address) {
return (
-
-
- {truncateAddress(wallet.address)}
-
+
+
+ {truncateAddress(wallet.address)}
+
+
{wallet.balance !== undefined ? (
{formatXLM(wallet.balance)}
diff --git a/frontend/src/hooks/useClipboard.ts b/frontend/src/hooks/useClipboard.ts
index 89c6e1a7..e30e7c51 100644
--- a/frontend/src/hooks/useClipboard.ts
+++ b/frontend/src/hooks/useClipboard.ts
@@ -9,44 +9,43 @@ export const useClipboard = (resetDelay = 2000) => {
const [copied, setCopied] = useState(false)
const timeoutRef = useRef | null>(null)
+ const fallbackCopy = (text: string) => {
+ const textArea = document.createElement('textarea')
+ textArea.value = text
+ textArea.style.position = 'fixed'
+ textArea.style.left = '-9999px'
+ textArea.style.top = '0'
+ document.body.appendChild(textArea)
+
+ textArea.focus()
+ textArea.select()
+
+ const successful = document.execCommand?.('copy')
+ document.body.removeChild(textArea)
+
+ return Boolean(successful)
+ }
+
const copy = useCallback(
async (text: string) => {
if (!text) return
try {
- // Clear any existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
- // Try modern navigator.clipboard API first
+ let copiedSuccessfully = false
+
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text)
- setCopied(true)
- } else {
- // Fallback to execCommand for older browsers or non-secure contexts
- const textArea = document.createElement('textarea')
- textArea.value = text
-
- // Ensure textarea is not visible but part of DOM
- textArea.style.position = 'fixed'
- textArea.style.left = '-9999px'
- textArea.style.top = '0'
- document.body.appendChild(textArea)
-
- textArea.focus()
- textArea.select()
-
- const successful = document.execCommand('copy')
- document.body.removeChild(textArea)
-
- if (successful) {
- setCopied(true)
- }
- // Fallback copy silently failed — nothing to surface to the user
+ copiedSuccessfully = true
+ } else if (typeof document !== 'undefined') {
+ copiedSuccessfully = fallbackCopy(text)
}
- // Reset copied state after delay
+ setCopied(copiedSuccessfully)
+
timeoutRef.current = setTimeout(() => {
setCopied(false)
timeoutRef.current = null
diff --git a/package-lock.json b/package-lock.json
index e031d347..41043813 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -955,6 +955,21 @@
"semantic-release": ">=20.1.0"
}
},
+ "node_modules/@simple-libs/child-process-utils": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz",
+ "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==",
+ "dev": true,
+ "dependencies": {
+ "@simple-libs/stream-utils": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/dangreen"
+ }
+ },
"node_modules/@simple-libs/stream-utils": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz",
@@ -1610,6 +1625,18 @@
"node": ">=18"
}
},
+ "node_modules/convert-hrtime": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz",
+ "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -1673,6 +1700,33 @@
"node": ">= 8"
}
},
+ "node_modules/crypto-random-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz",
+ "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/crypto-random-string/node_modules/type-fest": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
+ "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -2138,6 +2192,18 @@
"node": ">=14.14"
}
},
+ "node_modules/function-timeout": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-1.0.2.tgz",
+ "integrity": "sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -2838,6 +2904,23 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true
},
+ "node_modules/make-asynchronous": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.1.0.tgz",
+ "integrity": "sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==",
+ "dev": true,
+ "dependencies": {
+ "p-event": "^6.0.0",
+ "type-fest": "^4.6.0",
+ "web-worker": "^1.5.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/marked": {
"version": "15.0.12",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
@@ -4985,6 +5068,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/p-event": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz",
+ "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==",
+ "dev": true,
+ "dependencies": {
+ "p-timeout": "^6.1.2"
+ },
+ "engines": {
+ "node": ">=16.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/p-filter": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-filter/-/p-filter-4.1.0.tgz",
@@ -5054,6 +5152,18 @@
"node": ">=8"
}
},
+ "node_modules/p-timeout": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz",
+ "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
@@ -5363,6 +5473,34 @@
"node": ">=8"
}
},
+ "node_modules/restore-cursor": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+ "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+ "dev": true,
+ "dependencies": {
+ "onetime": "^7.0.0",
+ "signal-exit": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
@@ -6264,6 +6402,21 @@
"xtend": "~4.0.1"
}
},
+ "node_modules/time-span": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz",
+ "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==",
+ "dev": true,
+ "dependencies": {
+ "convert-hrtime": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/tinyexec": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz",
@@ -6329,6 +6482,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/tunnel": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+ "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
+ }
+ },
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
@@ -6461,6 +6623,12 @@
"spdx-expression-parse": "^3.0.0"
}
},
+ "node_modules/web-worker": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
+ "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==",
+ "dev": true
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -6619,6 +6787,12 @@
"node": ">=12"
}
},
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
"node_modules/yargs/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",