diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f96a5da..6433835 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -18,6 +18,8 @@ export default function App() { return ( + + setDark((d) => !d)} /> setDark(d => !d)} /> diff --git a/frontend/src/__tests__/darkMode.test.js b/frontend/src/__tests__/darkMode.test.js new file mode 100644 index 0000000..d0b96fb --- /dev/null +++ b/frontend/src/__tests__/darkMode.test.js @@ -0,0 +1,122 @@ +/** + * Dark mode tests — issue #277 + * Covers: + * 1. useDarkMode sets data-theme="dark" on + * 2. CSS variables resolve to values that pass WCAG AA contrast (4.5:1 for normal text) + */ + +import { renderHook, act } from '@testing-library/react'; +import { useDarkMode } from '../hooks/useDarkMode'; + +// ── helpers ────────────────────────────────────────────────────────────────── + +/** sRGB channel linearisation */ +function linearise(c) { + const s = c / 255; + return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); +} + +/** Relative luminance of an #rrggbb hex colour */ +function luminance(hex) { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return 0.2126 * linearise(r) + 0.7152 * linearise(g) + 0.0722 * linearise(b); +} + +/** WCAG contrast ratio between two hex colours */ +function contrast(hex1, hex2) { + const l1 = luminance(hex1); + const l2 = luminance(hex2); + const lighter = Math.max(l1, l2); + const darker = Math.min(l1, l2); + return (lighter + 0.05) / (darker + 0.05); +} + +// ── useDarkMode ─────────────────────────────────────────────────────────────── + +describe('useDarkMode', () => { + beforeEach(() => { + localStorage.clear(); + document.documentElement.removeAttribute('data-theme'); + // default: light preference + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockReturnValue({ matches: false }), + }); + }); + + it('sets data-theme="dark" on documentElement when dark=true', () => { + const { result } = renderHook(() => useDarkMode()); + act(() => result.current[1](true)); + expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); + }); + + it('removes data-theme attribute when dark=false', () => { + document.documentElement.setAttribute('data-theme', 'dark'); + const { result } = renderHook(() => useDarkMode()); + act(() => result.current[1](false)); + expect(document.documentElement.hasAttribute('data-theme')).toBe(false); + }); + + it('persists preference to localStorage', () => { + const { result } = renderHook(() => useDarkMode()); + act(() => result.current[1](true)); + expect(localStorage.getItem('darkMode')).toBe('true'); + act(() => result.current[1](false)); + expect(localStorage.getItem('darkMode')).toBe('false'); + }); + + it('reads initial state from localStorage', () => { + localStorage.setItem('darkMode', 'true'); + const { result } = renderHook(() => useDarkMode()); + expect(result.current[0]).toBe(true); + }); + + it('falls back to prefers-color-scheme when no localStorage value', () => { + window.matchMedia = jest.fn().mockReturnValue({ matches: true }); + const { result } = renderHook(() => useDarkMode()); + expect(result.current[0]).toBe(true); + }); +}); + +// ── WCAG AA contrast checks ─────────────────────────────────────────────────── +// Values taken directly from index.css token definitions. + +describe('WCAG AA contrast — light mode tokens', () => { + const pairs = [ + { label: 'text on bg', fg: '#0f172a', bg: '#ffffff' }, + { label: 'text-muted on bg', fg: '#64748b', bg: '#ffffff' }, + { label: 'accent on bg', fg: '#0369a1', bg: '#ffffff' }, + { label: 'nav-text on nav-bg', fg: '#cbd5e1', bg: '#1e293b' }, + { label: 'error on white', fg: '#dc2626', bg: '#ffffff' }, + { label: 'success on white', fg: '#15803d', bg: '#ffffff' }, + ]; + + pairs.forEach(({ label, fg, bg }) => { + it(`${label} meets WCAG AA (≥4.5:1)`, () => { + expect(contrast(fg, bg)).toBeGreaterThanOrEqual(4.5); + }); + }); +}); + +describe('WCAG AA contrast — dark mode tokens', () => { + const pairs = [ + { label: 'text on bg', fg: '#e2e8f0', bg: '#0f172a' }, + { label: 'text-muted on bg', fg: '#94a3b8', bg: '#0f172a' }, + { label: 'accent on bg', fg: '#38bdf8', bg: '#0f172a' }, + { label: 'nav-text on nav-bg', fg: '#cbd5e1', bg: '#1e293b' }, + { label: 'error on dark bg', fg: '#f87171', bg: '#0f172a' }, + { label: 'success on dark bg', fg: '#4ade80', bg: '#0f172a' }, + { label: 'chart-bar on track', fg: '#0ea5e9', bg: '#1e293b' }, + { label: 'badge-high text/bg', fg: '#fca5a5', bg: '#7f1d1d' }, + { label: 'badge-medium text/bg',fg: '#fcd34d', bg: '#78350f' }, + { label: 'badge-low text/bg', fg: '#93c5fd', bg: '#1e3a5f' }, + ]; + + pairs.forEach(({ label, fg, bg }) => { + it(`${label} meets WCAG AA (≥4.5:1)`, () => { + expect(contrast(fg, bg)).toBeGreaterThanOrEqual(4.5); + }); + }); +}); diff --git a/frontend/src/components/FreighterBanner.jsx b/frontend/src/components/FreighterBanner.jsx index ce8f046..1c614b7 100644 --- a/frontend/src/components/FreighterBanner.jsx +++ b/frontend/src/components/FreighterBanner.jsx @@ -11,12 +11,12 @@ export default function FreighterBanner() { return (
🦊 Freighter wallet not detected.{' '} - + Install Freighter {' '} to connect your wallet and issue or view records. diff --git a/frontend/src/hooks/useDarkMode.js b/frontend/src/hooks/useDarkMode.js index 34d7e00..d208c92 100644 --- a/frontend/src/hooks/useDarkMode.js +++ b/frontend/src/hooks/useDarkMode.js @@ -8,7 +8,11 @@ export function useDarkMode() { }); useEffect(() => { - document.documentElement.classList.toggle('dark', dark); + if (dark) { + document.documentElement.setAttribute('data-theme', 'dark'); + } else { + document.documentElement.removeAttribute('data-theme'); + } localStorage.setItem('darkMode', dark); }, [dark]); diff --git a/frontend/src/index.css b/frontend/src/index.css index 040bc5b..82c6bf7 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -73,14 +73,18 @@ /* Semantic aliases for light mode */ --bg: #ffffff; + --surface: #f8fafc; + --surface-2: #f1f5f9; --text: #0f172a; --text-muted: #64748b; - --accent: #0284c7; - --btn-primary: #0284c7; + --accent: #0369a1; + --btn-primary: #0369a1; --input-bg: #f1f5f9; --border: #cbd5e1; --nav-bg: #1e293b; --nav-text: #cbd5e1; + + /* Role badges */ --role-patient-bg: #e0f2fe; --role-patient-text: #0284c7; --role-patient-border: #7dd3fc; @@ -158,6 +162,8 @@ .dark { /* Semantic aliases for dark mode */ --bg: #0f172a; + --surface: #1e293b; + --surface-2: #0f172a; --text: #e2e8f0; --text-muted: #94a3b8; --accent: #38bdf8; @@ -166,11 +172,77 @@ --border: #334155; --nav-bg: #1e293b; --nav-text: #cbd5e1; + + /* Role badges */ --role-patient-bg: #0c2a4a; --role-patient-text: #38bdf8; --role-patient-border: #0ea5e9; --role-issuer-bg: #052e16; --role-issuer-text: #4ade80; + --role-issuer-border: #22c55e; + + /* Semantic */ + --color-error: #f87171; + --color-error-bg: rgba(248, 113, 113, 0.1); + --color-error-border: rgba(248, 113, 113, 0.2); + --color-success: #4ade80; + --color-success-bg: rgba(74, 222, 128, 0.1); + --color-success-border: rgba(74, 222, 128, 0.2); + --color-warning: #fbbf24; + --color-warning-bg: #78350f; + --color-warning-text: #fcd34d; + --color-info: #60a5fa; + --color-info-bg: rgba(96, 165, 250, 0.1); + --color-info-border: rgba(96, 165, 250, 0.2); + --color-muted: #94a3b8; + --color-muted-bg: rgba(148, 163, 184, 0.1); + --color-muted-border: rgba(148, 163, 184, 0.2); + + /* Chart / data */ + --chart-bar: #0ea5e9; + --chart-track: #1e293b; + --chart-label: #94a3b8; + + /* Badge severity */ + --badge-high-bg: #7f1d1d; + --badge-high-text: #fca5a5; + --badge-medium-bg: #78350f; + --badge-medium-text: #fcd34d; + --badge-low-bg: #1e3a5f; + --badge-low-text: #93c5fd; + + /* Status badges */ + --badge-active-bg: #14532d; + --badge-active-text: #86efac; + --badge-revoked-bg: #7f1d1d; + --badge-revoked-text: #fca5a5; + --badge-pending-bg: #78350f; + --badge-pending-text: #fde68a; + --badge-approved-bg: #14532d; + --badge-approved-text: #86efac; + --badge-rejected-bg: #7f1d1d; + --badge-rejected-text: #fca5a5; + + /* Dose badges */ + --dose-complete-bg: #166534; + --dose-complete-text: #86efac; + --dose-partial-bg: #1e3a5f; + --dose-partial-text: #93c5fd; + + /* Modals / overlays */ + --modal-bg: #1e293b; + --modal-border: #334155; + --overlay-bg: rgba(0, 0, 0, 0.7); + + /* Misc */ + --freighter-banner-bg: #7c3aed; + --freighter-banner-link: #e9d5ff; + --copy-btn-color: #64748b; + --copy-btn-hover: #38bdf8; + --copy-btn-success: #4ade80; + --copy-tooltip-bg: #1e293b; + --copy-tooltip-border: #334155; + --copy-tooltip-text: #4ade80; --focus-ring: #38bdf8; --focus-ring-offset: #0f172a; } diff --git a/frontend/src/pages/AdminDashboard.jsx b/frontend/src/pages/AdminDashboard.jsx index da4dbc7..5d340a9 100644 --- a/frontend/src/pages/AdminDashboard.jsx +++ b/frontend/src/pages/AdminDashboard.jsx @@ -7,6 +7,27 @@ import Tooltip from '../components/Tooltip'; const s = { page: { maxWidth: 700, width: '100%', margin: '2rem auto', padding: '0 1rem', boxSizing: 'border-box' }, table: { width: '100%', borderCollapse: 'collapse', fontSize: '0.9rem' }, + th: { textAlign: 'left', padding: '0.5rem 0.75rem', borderBottom: '1px solid var(--border)', color: 'var(--text-muted)' }, + td: { padding: '0.5rem 0.75rem', borderBottom: '1px solid var(--border)', color: 'var(--text)', wordBreak: 'break-all' }, + btn: { padding: '0.45rem 1rem', background: 'var(--btn-primary)', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: '0.9rem' }, + btnDanger: { padding: '0.45rem 0.75rem', background: 'var(--color-error)', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: '0.85rem' }, + btnSuccess: { padding: '0.45rem 0.75rem', background: 'var(--color-success)', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: '0.85rem', marginRight: '0.4rem' }, + input: { padding: '0.5rem 0.75rem', background: 'var(--input-bg)', border: '1px solid var(--border)', borderRadius: 6, color: 'var(--text)', fontSize: '0.9rem', flex: 1 }, + row: { display: 'flex', gap: '0.75rem', marginBottom: '1.5rem', alignItems: 'center' }, + keyBox: { marginTop: '1rem', padding: '0.75rem 1rem', background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8, color: 'var(--color-success)', fontSize: '0.85rem', wordBreak: 'break-all' }, + badge: (revoked) => ({ + display: 'inline-block', padding: '0.15rem 0.5rem', borderRadius: 4, fontSize: '0.75rem', + background: revoked ? 'var(--badge-revoked-bg)' : 'var(--badge-active-bg)', + color: revoked ? 'var(--badge-revoked-text)' : 'var(--badge-active-text)', + }), + statusBadge: (status) => { + const map = { + pending: ['var(--badge-pending-bg)', 'var(--badge-pending-text)'], + approved: ['var(--badge-approved-bg)', 'var(--badge-approved-text)'], + rejected: ['var(--badge-rejected-bg)', 'var(--badge-rejected-text)'], + }; + const [bg, color] = map[status] || ['var(--surface-2)', 'var(--text-muted)']; + return { display: 'inline-block', padding: '0.15rem 0.5rem', borderRadius: 4, fontSize: '0.75rem', background: bg, color }; th: { textAlign: 'left', padding: '0.75rem', borderBottom: '1px solid #334155', color: '#94a3b8' }, td: { padding: '0.75rem', borderBottom: '1px solid #1e293b', color: '#e2e8f0', wordBreak: 'break-all' }, btn: { padding: '0.6rem 1rem', background: '#0ea5e9', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: '0.9rem', minHeight: '44px', minWidth: '44px' }, @@ -57,14 +78,14 @@ export default function AdminDashboard() { if (!publicKey) { return (
-

Connect your admin wallet to manage API keys.

+

Connect your admin wallet to manage API keys.

); } if (role !== 'issuer') { - return

Access denied: admin role required.

; + return

Access denied: admin role required.

; } const handleCreate = async (e) => { @@ -149,7 +170,7 @@ export default function AdminDashboard() { - {error &&

{error}

} + {error &&

{error}

} {newKey && (
@@ -160,7 +181,7 @@ export default function AdminDashboard() { )} {keys.length === 0 ? ( -

No API keys yet.

+

No API keys yet.

) : ( @@ -196,12 +217,11 @@ export default function AdminDashboard() {
)} - {/* ── Issuer Onboarding Applications ── */}

Issuer Onboarding Applications

- {reviewError &&

{reviewError}

} + {reviewError &&

{reviewError}

} {applications.length === 0 ? ( -

No applications yet.

+

No applications yet.

) : ( diff --git a/frontend/src/pages/AnalyticsDashboard.jsx b/frontend/src/pages/AnalyticsDashboard.jsx index 7b23f14..5b2f2aa 100644 --- a/frontend/src/pages/AnalyticsDashboard.jsx +++ b/frontend/src/pages/AnalyticsDashboard.jsx @@ -8,13 +8,15 @@ const REFRESH_INTERVAL = 60_000; const s = { page: { maxWidth: 900, width: '100%', margin: '2rem auto', padding: '0 1rem', boxSizing: 'border-box' }, header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem', flexWrap: 'wrap', gap: '0.5rem' }, - h2: { color: '#e2e8f0', margin: 0 }, - refreshInfo: { color: '#64748b', fontSize: '0.8rem' }, - section: { background: '#0f172a', border: '1px solid #1e293b', borderRadius: 10, padding: '1.25rem', marginBottom: '1.5rem' }, - sectionTitle: { color: '#94a3b8', fontSize: '0.85rem', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '1rem' }, - error: { color: '#f87171', padding: '0.75rem', background: '#1e293b', borderRadius: 8, marginBottom: '1rem' }, - loading: { color: '#64748b', textAlign: 'center', padding: '2rem 0' }, + h2: { color: 'var(--text)', margin: 0 }, + refreshInfo: { color: 'var(--text-muted)', fontSize: '0.8rem' }, + section: { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 10, padding: '1.25rem', marginBottom: '1.5rem' }, + sectionTitle: { color: 'var(--text-muted)', fontSize: '0.85rem', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '1rem' }, + error: { color: 'var(--color-error)', padding: '0.75rem', background: 'var(--color-error-bg)', border: '1px solid var(--color-error-border)', borderRadius: 8, marginBottom: '1rem' }, + loading: { color: 'var(--text-muted)', textAlign: 'center', padding: '2rem 0' }, table: { width: '100%', borderCollapse: 'collapse', fontSize: '0.9rem' }, + th: { textAlign: 'left', color: 'var(--text-muted)', fontWeight: 500, padding: '0.5rem 0.75rem', borderBottom: '1px solid var(--border)' }, + td: { padding: '0.6rem 0.75rem', color: 'var(--text)', borderBottom: '1px solid var(--border)' }, th: { textAlign: 'left', color: '#64748b', fontWeight: 500, padding: '0.75rem', borderBottom: '1px solid #1e293b' }, td: { padding: '0.75rem', color: '#e2e8f0', borderBottom: '1px solid #1e293b' }, badge: (severity) => ({ @@ -23,16 +25,16 @@ const s = { borderRadius: 4, fontSize: '0.75rem', fontWeight: 600, - background: severity === 'high' ? '#7f1d1d' : severity === 'medium' ? '#78350f' : '#1e3a5f', - color: severity === 'high' ? '#fca5a5' : severity === 'medium' ? '#fcd34d' : '#93c5fd', + background: severity === 'high' ? 'var(--badge-high-bg)' : severity === 'medium' ? 'var(--badge-medium-bg)' : 'var(--badge-low-bg)', + color: severity === 'high' ? 'var(--badge-high-text)' : severity === 'medium' ? 'var(--badge-medium-text)' : 'var(--badge-low-text)', }), barWrap: { display: 'flex', flexDirection: 'column', gap: '0.6rem' }, barRow: { display: 'flex', alignItems: 'center', gap: '0.75rem' }, - barLabel: { color: '#94a3b8', fontSize: '0.85rem', minWidth: 140, textAlign: 'right', flexShrink: 0 }, - barTrack: { flex: 1, background: '#1e293b', borderRadius: 4, height: 18, overflow: 'hidden' }, - barFill: (pct) => ({ width: `${pct}%`, height: '100%', background: '#0ea5e9', borderRadius: 4, transition: 'width 0.4s ease' }), - barCount: { color: '#e2e8f0', fontSize: '0.85rem', minWidth: 40 }, - noData: { color: '#475569', textAlign: 'center', padding: '1.5rem 0' }, + barLabel: { color: 'var(--chart-label)', fontSize: '0.85rem', minWidth: 140, textAlign: 'right', flexShrink: 0 }, + barTrack: { flex: 1, background: 'var(--chart-track)', borderRadius: 4, height: 18, overflow: 'hidden' }, + barFill: (pct) => ({ width: `${pct}%`, height: '100%', background: 'var(--chart-bar)', borderRadius: 4, transition: 'width 0.4s ease' }), + barCount: { color: 'var(--text)', fontSize: '0.85rem', minWidth: 40 }, + noData: { color: 'var(--text-muted)', textAlign: 'center', padding: '1.5rem 0' }, }; function BarChart({ data }) { @@ -84,7 +86,7 @@ function IssuerTable({ data }) { } function AnomalyList({ data }) { - if (!data || data.length === 0) return

✅ No anomalies detected.

; + if (!data || data.length === 0) return

✅ No anomalies detected.

; return (
@@ -155,7 +157,7 @@ export default function AnalyticsDashboard() { if (role !== 'issuer') { return (
-

Access restricted to authorized issuers.

+

Access restricted to authorized issuers.

); } @@ -171,19 +173,16 @@ export default function AnalyticsDashboard() { {error &&

⚠️ {error}

} - {/* Vaccination Rates */}

Vaccination Rates by Vaccine Type

{loading && !rates ?

Loading…

: }
- {/* Issuer Activity */}

Issuer Activity

{loading && !issuers ?

Loading…

: }
- {/* Anomaly Flags */}

Anomaly Flags

{loading && !anomalies ?

Loading…

: } diff --git a/frontend/src/pages/IssuerDashboard.jsx b/frontend/src/pages/IssuerDashboard.jsx index 41f4a79..fe7596e 100644 --- a/frontend/src/pages/IssuerDashboard.jsx +++ b/frontend/src/pages/IssuerDashboard.jsx @@ -77,20 +77,20 @@ export default function IssuerDashboard() { if (!publicKey) { return (
-

Connect your issuer wallet.

+

Connect your issuer wallet.

); } if (role !== 'issuer') { - return

{t('issuer.accessDenied')}

; + return

{t('issuer.accessDenied')}

; } if (isAuthorized === null) { return (
-

Checking authorization status...

+

Checking authorization status...

); } @@ -108,17 +108,11 @@ export default function IssuerDashboard() { } }; - const fields = [ - { key: 'patient_address', label: t('issuer.patientAddress'), placeholder: 'G...', type: 'text' }, - { key: 'vaccine_name', label: t('issuer.vaccineName'), placeholder: t('issuer.vaccineNamePlaceholder'), type: 'text' }, - { key: 'date_administered', label: t('issuer.dateAdministered'), placeholder: '', type: 'date' }, - ]; - return (
-
+
-

Issue Vaccination NFT

+

Issue Vaccination NFT

@@ -147,9 +141,9 @@ export default function IssuerDashboard() {
{mintResult && ( -
+

✅ Vaccination NFT minted!

-

+

Token ID: {mintResult.tokenId}

@@ -157,7 +151,7 @@ export default function IssuerDashboard() { href={`https://stellar.expert/explorer/testnet/tx/${mintResult.transactionHash}`} target="_blank" rel="noopener noreferrer" - style={{ fontSize: '0.85rem', color: '#0ea5e9' }} + style={{ fontSize: '0.85rem', color: 'var(--accent)' }} > View on Stellar Explorer ↗ diff --git a/frontend/src/pages/Landing.jsx b/frontend/src/pages/Landing.jsx index dc97871..e0a9d3a 100644 --- a/frontend/src/pages/Landing.jsx +++ b/frontend/src/pages/Landing.jsx @@ -19,10 +19,10 @@ export default function Landing() {

{t('landing.subtitle')}

{publicKey ? ( <> -

+

{t('landing.connected', { address: `${publicKey.slice(0, 8)}…${publicKey.slice(-4)}` })}

- diff --git a/frontend/src/pages/PatientDashboard.jsx b/frontend/src/pages/PatientDashboard.jsx index 060ec5c..438e7bd 100644 --- a/frontend/src/pages/PatientDashboard.jsx +++ b/frontend/src/pages/PatientDashboard.jsx @@ -25,12 +25,29 @@ export default function PatientDashboard() { const { consented, giveConsent, loading: consentLoading } = useConsent(); const [qrRecord, setQrRecord] = useState(null); + const load = useCallback((p = 1, append = false) => { + if (!publicKey) return; + fetchRecords(publicKey, { page: p, limit: PAGE_LIMIT }) + .then((data) => { + setError(null); + if (data) { + const nextRecords = Array.isArray(data.data) ? data.data : []; + setRecords((current) => (append ? [...current, ...nextRecords] : nextRecords)); + setTotal(data.total ?? 0); + setPage(data.page ?? p); + } + }) + .catch((err) => setError(err.message || 'Failed to fetch records')); + }, [publicKey, fetchRecords]); + + useEffect(() => { load(page); }, [load]); + const handleDeclineConsent = () => disconnect(); if (!publicKey) { return (
-

Connect your wallet to view records.

+

Connect your wallet to view records.

); @@ -52,16 +69,19 @@ export default function PatientDashboard() {
-

{t('patient.title')}

+

{t('patient.title')}

+ {total > 0 && ( + + Showing {records.length} of {total} {records.length > 0 && ( {records.length} records )}
-

+

Wallet: {publicKey}

@@ -69,6 +89,14 @@ export default function PatientDashboard() { {loading && } {!loading && error && (
+

⚠️ {error}

+ +
+ )} + {!loading && !error && total === 0 && ( +
+

💉

+

No vaccination records found for this wallet.

⚠️ {error}

diff --git a/frontend/src/pages/VerifyPage.jsx b/frontend/src/pages/VerifyPage.jsx index bf088c1..2d93aab 100644 --- a/frontend/src/pages/VerifyPage.jsx +++ b/frontend/src/pages/VerifyPage.jsx @@ -77,13 +77,13 @@ export default function VerifyPage() {
- {error &&

Error: {error}

} + {error &&

Error: {error}

}
{result && (
-

+

Wallet: {wallet}