Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
exports[`Copy components snapshots > CopyButton renders correctly 1`] = `
<button
aria-label="Copy test-value"
aria-pressed="false"
class="inline-flex items-center gap-1 rounded p-1 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-yellow-400 focus:ring-offset-1 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 "
title="Copy to clipboard"
>
Expand All @@ -28,6 +29,7 @@ exports[`Copy components snapshots > CopyableText renders correctly (mono) 1`] =
</span>
<button
aria-label="Copy 0x1234567890"
aria-pressed="false"
class="inline-flex items-center gap-1 rounded p-1 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-yellow-400 focus:ring-offset-1 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 "
title="Copy to clipboard"
>
Expand All @@ -54,6 +56,7 @@ exports[`Copy components snapshots > CopyableText renders correctly (non-mono) 1
</span>
<button
aria-label="Copy test"
aria-pressed="false"
class="inline-flex items-center gap-1 rounded p-1 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-yellow-400 focus:ring-offset-1 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 "
title="Copy to clipboard"
>
Expand All @@ -74,33 +77,49 @@ exports[`LanguageSwitcher snapshots > LanguageSwitcher renders correctly 1`] = `
aria-label="Language selector"
role="group"
>
<span
aria-atomic="true"
aria-live="polite"
class="sr-only"
role="status"
/>
<label
class="sr-only"
for="language-select"
>
Select language
</label>
<select
aria-label="Select language"
class="cursor-pointer appearance-none rounded-md border border-gray-200 bg-white px-2 py-1 text-xs font-medium text-gray-700 focus:outline-none focus:ring-2 focus:ring-yellow-400 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300"
id="language-select"
>
<option
aria-label="English"
value="en"
>
EN
English
</option>
<option
aria-label="Español"
value="es"
>
ES
Español
</option>
<option
aria-label="Français"
value="fr"
>
FR
Français
</option>
<option
aria-label="Deutsch"
value="de"
>
DE
Deutsch
</option>
</select>
</div>
Expand Down Expand Up @@ -234,17 +253,29 @@ exports[`Navbar snapshots > Navbar renders correctly 1`] = `
>
nav.verify
</a>
<a
class="text-sm transition-colors text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
href="/admin"
role="listitem"
>
nav.admin
</a>
</div>
<div
class="flex items-center gap-2"
>
<a
aria-label="Stellar Testnet — view network info"
class="hidden items-center rounded-full px-2 py-0.5 text-xs font-semibold md:flex bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
href="https://stellar.expert/explorer/testnet"
rel="noopener noreferrer"
target="_blank"
title="Connected to Stellar Testnet — not a production environment"
>
<span
aria-hidden="true"
class="h-1.5 w-1.5 rounded-full animate-pulse bg-yellow-500"
/>
Testnet
</a>
<div
Expand All @@ -262,6 +293,7 @@ exports[`Navbar snapshots > Navbar renders correctly 1`] = `
</span>
<button
aria-label="Copy wallet address GABC...XYZ"
aria-pressed="false"
class="inline-flex items-center gap-1 rounded p-1 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-yellow-400 focus:ring-offset-1 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 "
title="Copy to clipboard"
>
Expand Down
21 changes: 14 additions & 7 deletions apps/web/src/app/verify/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useSearchParams, useRouter } from 'next/navigation'
import { Search, CheckCircle, XCircle, Shield, ExternalLink, Copy } from 'lucide-react'
import { SectionSkeleton } from '@/components/skeleton'
import { CopyableText } from '@/components/copy-button'
import { writeToClipboard } from '@/components/copy-button'
import { useToast } from '@/components/ToastProvider'

interface ChainOfCustody {
Expand Down Expand Up @@ -106,7 +107,7 @@ export default function VerifyPage() {
const [pageError, setPageError] = useState<Error | null>(null)
const [loading, setLoading] = useState(false)
const [copied, setCopied] = useState(false)
const { pushToast: toast } = useToast()
const { pushToast } = useToast()

function handleVerify(e: React.FormEvent) {
e.preventDefault()
Expand All @@ -122,12 +123,12 @@ export default function VerifyPage() {
if (!res.ok) {
const message = data.error || 'Unable to verify certificate'
setError(message)
toast({ variant: 'error', title: 'Verification failed', description: message })
pushToast({ variant: 'error', title: 'Verification failed', description: message })
return
}

setResult(data)
toast({ variant: 'success', title: 'Certificate verified', description: 'Full chain of custody confirmed.' })
pushToast({ variant: 'success', title: 'Certificate verified', description: 'Full chain of custody confirmed.' })
} catch {
setError('Network error — please try again.')
trackEvent('verify_error', { status: 0 })
Expand All @@ -138,9 +139,14 @@ export default function VerifyPage() {
}

async function copyLink() {
await navigator.clipboard.writeText(window.location.href)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
try {
await writeToClipboard(window.location.href)
setCopied(true)
pushToast({ variant: 'success', title: 'Link copied', description: 'Paste it anywhere to share this verification.' })
setTimeout(() => setCopied(false), 2000)
} catch {
pushToast({ variant: 'error', title: 'Copy failed', description: 'Please copy the URL from your browser address bar.' })
}
}

const steps = result ? buildSteps(result) : null
Expand Down Expand Up @@ -233,11 +239,12 @@ export default function VerifyPage() {
<button
onClick={copyLink}
aria-label="Copy shareable verification link"
aria-pressed={copied}
title="Copy shareable link"
className="flex items-center gap-1 rounded-md px-2 py-1 text-xs text-gray-500 hover:bg-white/60 dark:hover:bg-gray-800/60"
>
<Copy className="h-3.5 w-3.5" aria-hidden="true" />
{copied ? 'Copied!' : 'Share'}
<span aria-live="polite">{copied ? 'Copied!' : 'Share'}</span>
</button>
</div>

Expand Down
32 changes: 28 additions & 4 deletions apps/web/src/components/copy-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,46 @@ interface CopyButtonProps {
label?: string
className?: string
iconSize?: number
onCopied?: () => void
}

export function CopyButton({ value, label, className = '', iconSize = 14 }: CopyButtonProps) {
/** Write text to clipboard with a mobile-safe fallback via a temporary textarea. */
async function writeToClipboard(text: string): Promise<void> {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text)
return
}
// Fallback for mobile browsers that don't support navigator.clipboard
const el = document.createElement('textarea')
el.value = text
el.style.cssText = 'position:fixed;top:0;left:0;opacity:0'
document.body.appendChild(el)
el.focus()
el.select()
const ok = document.execCommand('copy')
document.body.removeChild(el)
if (!ok) throw new Error('copy failed')
}

export function CopyButton({ value, label, className = '', iconSize = 14, onCopied }: CopyButtonProps) {
const [copied, setCopied] = useState(false)

async function handleCopy() {
try {
await navigator.clipboard.writeText(value)
await writeToClipboard(value)
setCopied(true)
onCopied?.()
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error('Failed to copy:', err)
} catch {
// Surface nothing to the user visually — caller can supply onCopied for toast
}
}

return (
<button
onClick={handleCopy}
aria-label={label || `Copy ${value}`}
aria-pressed={copied}
title={copied ? 'Copied!' : 'Copy to clipboard'}
className={`inline-flex items-center gap-1 rounded p-1 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-yellow-400 focus:ring-offset-1 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 ${className}`}
>
Expand Down Expand Up @@ -57,3 +78,6 @@ export function CopyableText({ value, displayValue, mono = true, className = ''
</span>
)
}

export { writeToClipboard }