diff --git a/app/app/connect/page.tsx b/app/app/connect/page.tsx index 273c1e6..4a6e220 100644 --- a/app/app/connect/page.tsx +++ b/app/app/connect/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useEffect } from 'react'; @@ -21,10 +22,14 @@ export default function ConnectPage() { ← Back to home -
-
- ⛓ -
+
+

Connect your Freighter wallet

Freighter is a free browser extension that manages your Stellar keys. VeilGate diff --git a/app/app/dashboard/history/page.tsx b/app/app/dashboard/history/page.tsx index f86b20a..a504114 100644 --- a/app/app/dashboard/history/page.tsx +++ b/app/app/dashboard/history/page.tsx @@ -1,26 +1,43 @@ 'use client'; +import Image from 'next/image'; import { useWallet } from '@/lib/providers/wallet-provider'; -import { TxLog } from '@/components/tx-log'; +import { ActivityLog } from '@/components/activity-log'; export default function HistoryPage() { const { address } = useWallet(); return ( -

-

Transaction log

+
+

Activity

+

Transaction log

-
- Each receipt is stored only in your browser. The on-chain record shows that a proof was - spent — not who deposited or who received. Your deposit and the payment are unlinkable - on-chain, even to VeilGate. +
+
+ + ⊞ + +

+ Every action leaves a proof in your browser. The on-chain record proves that a payment + was made and the settlement is final. Your deposit and the payment are{' '} + unlinkable on-chain, even to the + VeilGate operator. +

+
+
- +
); diff --git a/app/app/dashboard/layout.tsx b/app/app/dashboard/layout.tsx index 8ef8680..5a9ab42 100644 --- a/app/app/dashboard/layout.tsx +++ b/app/app/dashboard/layout.tsx @@ -21,11 +21,11 @@ export default function DashboardLayout({ children }: { children: React.ReactNod return ( -
+
-
+
-
{children}
+
{children}
diff --git a/app/app/dashboard/wallet/page.tsx b/app/app/dashboard/wallet/page.tsx index 7e57242..9827bcf 100644 --- a/app/app/dashboard/wallet/page.tsx +++ b/app/app/dashboard/wallet/page.tsx @@ -2,11 +2,14 @@ import { useRouter } from 'next/navigation'; import { useWallet } from '@/lib/providers/wallet-provider'; -import { Card, CopyButton, NetworkPill } from '@/components/ui'; +import { useShieldedTotal } from '@/lib/pending'; +import { Card, CopyButton, NetworkPill, truncate } from '@/components/ui'; export default function WalletPage() { const router = useRouter(); const { address, balances, balancesLoading, disconnect } = useWallet(); + const xlm = balances.find((b) => b.asset === 'XLM'); + const shielded = useShieldedTotal(address); function handleDisconnect() { disconnect(); @@ -14,50 +17,81 @@ export default function WalletPage() { } return ( -
-

Wallet

+
+

+ Wallet + +

+

Manage your balance, wallet settings and network.

- -
- Network - -
-
- Address - - {address ? address.slice(0, 8) + '…' + address.slice(-6) : '—'} - {address && } - -
- {address && ( - - View on Stellar Explorer → - - )} -
+
+ +
+ Network + +
+
+ Address + + {address ? truncate(address, 8, 6) : '—'} + {address && } + +
+ {address && ( + + View on Stellar Explorer → + + )} +
- -

- Balances -

- {balancesLoading ? ( -

Loading…

- ) : ( -
- {balances.map((b) => ( -
- {b.asset} - {Number(b.amount).toLocaleString(undefined, { maximumFractionDigits: 7 })} -
- ))} + +

Total balance

+ {balancesLoading ? ( +

Loading…

+ ) : xlm ? ( +

+ {Number(xlm.amount).toLocaleString(undefined, { maximumFractionDigits: 7 })}{' '} + XLM +

+ ) : ( +

No balance yet.

+ )} +
+
+

Available

+

+ {xlm ? Number(xlm.amount).toLocaleString(undefined, { maximumFractionDigits: 7 }) : '—'} XLM +

+
+
+

Shielded

+

+ {shielded.toLocaleString(undefined, { maximumFractionDigits: 7 })} XLM +

+

pending payouts

+
- )} -
+ {balances.filter((b) => b.asset !== 'XLM').length > 0 && ( +
+ {balances + .filter((b) => b.asset !== 'XLM') + .map((b) => ( +
+ {b.asset} + + {Number(b.amount).toLocaleString(undefined, { maximumFractionDigits: 7 })} + +
+ ))} +
+ )} + +

Need testnet XLM?

@@ -71,13 +105,48 @@ export default function WalletPage() {
-
- +
+

Wallet actions

+
+ {address && ( +
+ + ↓ + +
+

Receive

+

{truncate(address, 6, 4)}

+
+ +
+ )} + + + ⬈ + +
+

View on explorer

+

See this wallet on Stellar Explorer

+
+
+ +
); diff --git a/app/app/favicon.ico b/app/app/favicon.ico new file mode 100644 index 0000000..e0d7ef4 Binary files /dev/null and b/app/app/favicon.ico differ diff --git a/app/app/globals.css b/app/app/globals.css index ed26024..86d244b 100644 --- a/app/app/globals.css +++ b/app/app/globals.css @@ -27,10 +27,95 @@ body { background: radial-gradient(circle, rgba(139, 92, 246, 0.55) 0%, rgba(139, 92, 246, 0) 70%); } +/* Brand gradient text — the blue→violet of the logo mark. */ +.text-veilgrad { + background: linear-gradient(100deg, #7b8cff 0%, #8b5cf6 55%, #a78bfa 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +/* Scrims for full-bleed art: keep text legible without boxing the image. */ +.scrim-l { + background: linear-gradient( + to right, + #050309 0%, + rgba(5, 3, 9, 0.92) 32%, + rgba(5, 3, 9, 0.45) 60%, + rgba(5, 3, 9, 0) 100% + ); +} +.scrim-b { + background: linear-gradient(to top, #050309 0%, rgba(5, 3, 9, 0.6) 40%, rgba(5, 3, 9, 0) 100%); +} + +/* Hairline border that reads premium on near-black. */ +.hairline { + border: 1px solid rgba(139, 92, 246, 0.16); +} + .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Cascadia Code', monospace; } +/* CSS-only violet particle field — ambient texture for slides without a + full-bleed art background (pitch deck). Base layer drifts slowly; two + pseudo-element layers of dots twinkle (opacity fade in/out) on their own + out-of-sync timers so the sparkle reads as randomized, not a single pulse. */ +.particle-field { + background-image: + radial-gradient(1.5px 1.5px at 20% 30%, rgba(167, 139, 250, 0.45) 50%, transparent), + radial-gradient(2px 2px at 75% 70%, rgba(139, 92, 246, 0.4) 50%, transparent), + radial-gradient(1px 1px at 40% 85%, rgba(167, 139, 250, 0.35) 50%, transparent), + radial-gradient(1.5px 1.5px at 88% 15%, rgba(139, 92, 246, 0.4) 50%, transparent); + background-repeat: repeat; + background-size: 640px 640px, 720px 720px, 560px 560px, 680px 680px; + animation: particle-drift 80s linear infinite; +} + +.particle-field::before, +.particle-field::after { + content: ''; + position: absolute; + inset: 0; +} + +.particle-field::before { + background-image: + radial-gradient(1.5px 1.5px at 15% 55%, rgba(196, 181, 253, 0.85) 50%, transparent), + radial-gradient(2px 2px at 65% 20%, rgba(167, 139, 250, 0.75) 50%, transparent); + background-repeat: repeat; + background-size: 700px 700px, 600px 600px; + animation: particle-twinkle 4.2s ease-in-out infinite; +} + +.particle-field::after { + background-image: + radial-gradient(1px 1px at 82% 65%, rgba(196, 181, 253, 0.8) 50%, transparent), + radial-gradient(1.5px 1.5px at 30% 90%, rgba(139, 92, 246, 0.75) 50%, transparent); + background-repeat: repeat; + background-size: 650px 650px, 750px 750px; + animation: particle-twinkle 5.6s ease-in-out infinite -2.4s; +} + +@keyframes particle-drift { + from { + background-position: 0 0, 0 0, 0 0, 0 0; + } + to { + background-position: 320px 320px, -360px 360px, 280px -280px, -340px 340px; + } +} + +@keyframes particle-twinkle { + 0%, 100% { + opacity: 0.1; + } + 50% { + opacity: 1; + } +} + @media (prefers-reduced-motion: reduce) { .animate-proof-pulse, .animate-ring-expand, @@ -40,4 +125,9 @@ body { .proof-glow { opacity: 0.6; } + .particle-field, + .particle-field::before, + .particle-field::after { + animation: none; + } } diff --git a/app/app/layout.tsx b/app/app/layout.tsx index 858b0b1..5547fad 100644 --- a/app/app/layout.tsx +++ b/app/app/layout.tsx @@ -1,11 +1,18 @@ import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; import './globals.css'; import { WalletProvider } from '@/lib/providers/wallet-provider'; +const inter = Inter({ + subsets: ['latin'], + variable: '--font-sans', + display: 'swap', +}); + export const metadata: Metadata = { - title: 'VeilGate — Private Micropayment Paywall', + title: 'VeilGate — Unlinkable Payments on Stellar', description: - 'Pay any content. Reveal nothing. A privacy-preserving paywall on Stellar.', + 'A shielded pool on Stellar. Pay anyone — your deposit and the payout cannot be linked on-chain. Zero-knowledge, verified on Soroban.', }; export default function RootLayout({ @@ -14,8 +21,8 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - + + {children} diff --git a/app/app/page.tsx b/app/app/page.tsx index 79b029f..f49c03f 100644 --- a/app/app/page.tsx +++ b/app/app/page.tsx @@ -1,120 +1,256 @@ +import Image from 'next/image'; import Link from 'next/link'; -import { TestnetPill } from '@/components/ui'; +import { HeroVideo } from '@/components/hero-video'; +/** + * Landing — the brand at full strength (art direction: near-black canvas, one + * violet accent, the veil ghost + particle streams as full-bleed hero art). + * Copy tells the real story: a shielded pool on Stellar — unlinkability, not + * amount-hiding (denominations are public by design). + */ export default function LandingPage() { return (
- {/* Topbar */} -
- VeilGate -
- + {/* Header + hero fill exactly one screen; the feature strip rests on its + bottom edge no matter the viewport height. */} +
+ {/* Topbar */} +
+ + + VeilGate + + Open app → -
-
+ - {/* Hero */} -
-
-

- Pay without revealing how much -

-

- Read anything. -
- Leave no trace. -

-

- VeilGate lets you pay for premium articles on Stellar without exposing your - payment amount — to the publisher, to the network, or to anyone. A mathematical - proof runs in your browser and confirms you paid a valid amount. The number - itself stays on your device. -

-
- - Connect Freighter to start - -

- No account. No email. Your Freighter wallet is your identity. -

+ {/* Hero — full-bleed art, golden split, one focal point */} +
+
+ {/* Static art is the base layer (and the reduced-motion / no-js fallback)… */} + + {/* …and the Kling loop plays over it for every visitor. */} + +
+
+
+ + {/* Top group: badge + headline. On mobile this is ALL that sits up here + (CTAs/trust row move to the bottom group below); on sm+ it holds the + full block (description + CTAs + trust row) as before. */} +
+
+

+ Private payments on Stellar +

+

+ Pay without +
+ leaving a trace. +

+

+ VeilGate is a shielded pool on Stellar. Your deposit and the payout are two + transactions no one can link on-chain — the + publisher gets paid in full, and nobody knows it came from you. +

+
+ + Pay privately → + + + How it works + +
+
+ Groth16 verified on Soroban + Freighter wallet + </> Open source +
+
+ + {/* Empty column — keeps the golden-rectangle split so the art breathes on the right. */} +
-
+ + {/* Bottom group: mobile-only CTAs + trust row (kept close to the feature + strip below), then the feature strip itself (all breakpoints). */} +
+
+
+ + Pay privately → + + + How it works + +
+
+ Groth16 verified on Soroban + Freighter wallet + </> Open source +
+
+ + {/* Feature strip — titles only (no descriptions) so it stays short; + same max-width as the rest of the page's content. */} +
+
+ + + + +
+
+
+
+
{/* How it works */} -
-
- - Pick any amount between 1 and 255 centi-cents. It never leaves your browser tab. +
+

How it works

+

+ Three steps. You sign once — the payout is submitted by a relayer, so your account never + appears on it. +

+
+ + Pick a denomination and lock it into the shielded pool with one Freighter signature. A + secret note is created on your device. - - A zero-knowledge proof is generated locally (a few seconds). It says “valid - amount” without revealing the number. + + Your browser proves your note is in the pool (Groth16, ~seconds). The secret never + leaves the tab. - - The proof is verified. The article opens. Nobody learned how much you paid. + + The contract verifies the proof on-chain and pays the publisher — submitted by a + relayer, not by you. Pay now, or shield and pay later.
- {/* Privacy proof strip */} -
-

- What the world sees vs. what you keep private + {/* What the chain sees vs what stays hidden */} +
+

+ What the chain sees vs. what stays hidden

-
-

- Visible to the publisher +

+

+ Public on-chain

-
    -
  • nullifier 0x4a3f…c2d1
  • -
  • timestamp 14:22 UTC
  • -
  • proof ✓ valid
  • +
      +
    • deposit tx → pool (from you)
    • +
    • payout tx → publisher (from a relayer)
    • +
    • denomination 0.1 XLM (same as everyone)
    • +
    • nullifier 0x20fc… (blocks double-spend)
    • +
    • proof ✓ valid
-
-

- Kept on your device +

+

+ Hidden by math

-
    -
  • amount ████ (hidden by design)
  • -
  • secret ████
  • -
  • your identity ████
  • +
      +
    • who paid whom ████
    • +
    • your wallet on the payout ████
    • +
    • which deposit funded it ████
    • +
    • note secret ████ (never leaves your device)
+

+ Honest by design: the denomination is public — VeilGate hides who paid whom, not + how much. +

- {/* Testnet notice */} -
-

- Running on Stellar Testnet · Proofs are real · Contracts are not audited · Do not - use real funds + {/* Ending — the other moment that matters */} +

+
+
+

+ Disappear into the crowd. +

+

+ Real payments on Stellar testnet, verified by zero-knowledge proofs on Soroban. +

+ + Connect Freighter to start + +

+ No account. No email. Your wallet is your identity. +

+
+

+ Running on Stellar Testnet · Proofs are real · Contracts are not audited · Do not use real + funds

-
+

); } +function Feature({ icon, title }: { icon: string; title: string }) { + return ( +
+ + + +

{title}

+
+ ); +} + function Step({ n, title, children }: { n: string; title: string; children: React.ReactNode }) { return ( -
-
+
+
{n}
-

{title}

-

{children}

+

{title}

+

{children}

); } diff --git a/app/app/pitchdeck/page.tsx b/app/app/pitchdeck/page.tsx new file mode 100644 index 0000000..c68736f --- /dev/null +++ b/app/app/pitchdeck/page.tsx @@ -0,0 +1,10 @@ +import { PitchDeck } from '@/components/pitch-deck'; + +export const metadata = { + title: 'VeilGate — Pitch Deck', + description: 'Private micropayments on Stellar — a zero-knowledge shielded pool.', +}; + +export default function PitchDeckPage() { + return ; +} diff --git a/app/components/activity-log.tsx b/app/components/activity-log.tsx new file mode 100644 index 0000000..4322544 --- /dev/null +++ b/app/components/activity-log.tsx @@ -0,0 +1,360 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import { loadHistory, type PaymentReceipt } from '@/lib/history'; +import { Card, CopyButton, PrivacyBadge, ExplorerLink, truncate } from '@/components/ui'; + +/** + * Full Activity page log — search, filter, date range, export, pagination, and + * expandable rows, matching `activity page ref.png`. Separate from `TxLog` + * (used as a small "recent activity" preview on Home/Wallet) so that component + * and its tests stay untouched. Reads the same `loadHistory()` store; every + * filter/derived value below comes from data already on the receipt — nothing + * invented (e.g. "type" is derived from which tx hashes are present, the same + * logic that already drives the direction icon). + */ + +type TypeFilter = 'all' | 'deposit' | 'withdraw' | 'payment'; + +function typeOf(r: PaymentReceipt): 'deposit' | 'withdraw' | 'payment' { + if (r.depositHash && r.withdrawHash) return 'payment'; + if (r.depositHash) return 'deposit'; + return 'withdraw'; +} + +function directionIcon(r: PaymentReceipt): string { + const t = typeOf(r); + return t === 'payment' ? '⇄' : t === 'deposit' ? '↓' : '↑'; +} + +const PAGE_SIZE = 5; + +export function ActivityLog({ address }: { address: string | null }) { + const [items, setItems] = useState(null); + const [query, setQuery] = useState(''); + const [typeFilter, setTypeFilter] = useState('all'); + const [dateFrom, setDateFrom] = useState(''); + const [dateTo, setDateTo] = useState(''); + const [page, setPage] = useState(1); + const [expanded, setExpanded] = useState>(new Set()); + + useEffect(() => { + const refresh = () => { + const list = address ? loadHistory(address) : []; + setItems(list); + // Expanded by default, matching the reference screenshot. + setExpanded(new Set(list.map((r, i) => `${r.nullifier}-${i}`))); + }; + refresh(); + window.addEventListener('storage', refresh); + window.addEventListener('veilgate:history', refresh as EventListener); + return () => { + window.removeEventListener('storage', refresh); + window.removeEventListener('veilgate:history', refresh as EventListener); + }; + }, [address]); + + const filtered = useMemo(() => { + if (!items) return []; + const q = query.trim().toLowerCase(); + const from = dateFrom ? new Date(dateFrom).getTime() : null; + // end-of-day so the "to" date is inclusive + const to = dateTo ? new Date(dateTo).getTime() + 24 * 60 * 60 * 1000 - 1 : null; + return items.filter((r) => { + if (typeFilter !== 'all' && typeOf(r) !== typeFilter) return false; + const t = new Date(r.timestamp).getTime(); + if (from !== null && t < from) return false; + if (to !== null && t > to) return false; + if (q) { + const haystack = [ + r.nullifier, + r.poolId, + r.denomination, + r.publisherDomain, + r.depositHash, + r.withdrawHash, + r.root, + ] + .filter(Boolean) + .join(' ') + .toLowerCase(); + if (!haystack.includes(q)) return false; + } + return true; + }); + }, [items, query, typeFilter, dateFrom, dateTo]); + + const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); + const pageItems = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE); + + useEffect(() => { + setPage(1); + }, [query, typeFilter, dateFrom, dateTo]); + + function toggle(id: string) { + setExpanded((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + } + + function exportCsv() { + const header = [ + 'timestamp', + 'type', + 'denomination', + 'publisher', + 'nullifier', + 'root', + 'depositTx', + 'withdrawTx', + 'poolId', + 'anonymitySet', + 'proofBytes', + ]; + const rows = filtered.map((r) => [ + r.timestamp, + typeOf(r), + r.denomination ?? '', + r.publisherDomain ?? '', + r.nullifier, + r.root ?? '', + r.depositHash ?? '', + r.withdrawHash ?? '', + r.poolId ?? '', + r.anonymitySet ?? '', + r.proofBytes ?? '', + ]); + const csv = [header, ...rows] + .map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(',')) + .join('\n'); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `veilgate-activity-${new Date().toISOString().slice(0, 10)}.csv`; + a.click(); + URL.revokeObjectURL(url); + } + + if (items === null) return ; + if (items.length === 0) return ; + + return ( +
+ {/* Search + filters + export */} +
+ setQuery(e.target.value)} + placeholder="Search by tx hash, pool, amount…" + className="hairline w-full rounded-xl bg-ink-900/60 px-3 py-2 text-sm text-white placeholder-gray-500 focus:border-veil-500 focus:outline-none sm:min-w-[220px] sm:w-auto sm:flex-1" + /> + +
+ setDateFrom(e.target.value)} + className="hairline min-w-0 flex-1 rounded-xl bg-ink-900/60 px-3 py-2 text-sm text-gray-200 focus:outline-none" + aria-label="From date" + /> + + setDateTo(e.target.value)} + className="hairline min-w-0 flex-1 rounded-xl bg-ink-900/60 px-3 py-2 text-sm text-gray-200 focus:outline-none" + aria-label="To date" + /> +
+ +
+ + {/* Rows */} +
+ {pageItems.length === 0 ? ( + + No activity matches these filters. + + ) : ( + pageItems.map((r, i) => { + const id = `${r.nullifier}-${(page - 1) * PAGE_SIZE + i}`; + return toggle(id)} />; + }) + )} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ + {Array.from({ length: totalPages }, (_, i) => i + 1).map((n) => ( + + ))} + +
+ )} +
+ ); +} + +function Row({ r, isOpen, onToggle }: { r: PaymentReceipt; isOpen: boolean; onToggle: () => void }) { + const onChain = Boolean(r.withdrawHash); + const t = new Date(r.timestamp); + const type = typeOf(r); + + return ( + + + + {isOpen && ( +
+ + {type} + + {r.poolId && ( + + + {`${truncate(r.poolId, 6, 5)} ↗`} + + + )} + + Stellar Testnet + + + {truncate(r.nullifier, 8, 5)} + + + {r.depositHash && ( + + + + )} + {r.withdrawHash && ( + + + + )} + {r.root && ( + + {truncate(r.root, 8, 5)} + + )} +
+ )} +
+ ); +} + +function KV({ k, children }: { k: string; children: React.ReactNode }) { + return ( +
+
{k}
+
{children}
+
+ ); +} + +function Loading() { + return ( +
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+ ); +} + +function Empty() { + return ( + +

+

No payments yet

+

+ Each payment generates an on-chain ZK proof. Your receipt appears here the moment a + withdrawal confirms. +

+
+ ); +} diff --git a/app/components/app-shell.tsx b/app/components/app-shell.tsx index 5627607..d8e4212 100644 --- a/app/components/app-shell.tsx +++ b/app/components/app-shell.tsx @@ -1,10 +1,12 @@ 'use client'; +import Image from 'next/image'; import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; import { useWallet } from '@/lib/providers/wallet-provider'; import { useHermes } from '@/components/hermes'; import { NetworkPill, CopyButton, truncate } from '@/components/ui'; +import { cn } from '@/lib/cn'; // Only the real, on-chain XLM settlement flow is surfaced. The earlier // content-unlock / shield routes are proving demos that move no value, so they @@ -26,8 +28,9 @@ export function TopBar() { router.push('/'); } return ( -
- +
+ + VeilGate
@@ -53,29 +56,31 @@ export function TopBar() { export function NavRail() { const pathname = usePathname(); const { open } = useHermes(); + return ( -