diff --git a/docs/superpowers/specs/2026-07-05-pwa-tab-restructure-design.md b/docs/superpowers/specs/2026-07-05-pwa-tab-restructure-design.md new file mode 100644 index 0000000..65b8068 --- /dev/null +++ b/docs/superpowers/specs/2026-07-05-pwa-tab-restructure-design.md @@ -0,0 +1,56 @@ +# OpenProof PWA Tab Restructure + +Date: 2026-07-05 +Status: Approved + +## Problem + +1. WagmiProviderNotFoundError on Create/Verify pages in PWA mode. `WalletProvider` lives in `AppShell` but PWA mode uses `PwaShell` which lacks it. +2. `/app` homepage has a splash screen + action cards + history + testnet notice — too much for a focused app. +3. About / Privacy / Terms are only accessible on the website, not inside the PWA. +4. Each PWA tab should open directly to its functional content. + +## Solution + +### 1. Move WalletProvider to root layout + +`WalletProvider` moves from `AppShell` → root `layout.tsx`, wrapping `ConditionalShell`. Removed from `AppShell` to prevent nesting. Fixes wagmi context for all routes in both PWA and browser mode. + +### 2. Tab routing + +| Tab | Route | Content | +|-----|-------|---------| +| **Create** | `/app` | App title + one-line summary + full create-proof flow | +| **Verify** | `/app/verify` | Full verify-proof flow | +| **History** | `/app/history` | Proof history + bundle proofs + testnet notice | +| **More** | `/app/more` | About, Privacy, Terms, Docs links | + +### 3. Page changes + +- **`/app/page.tsx`**: Remove `AppSplash`, remove action cards. Show "Create Proof" title + summary + redirect to or embed the create flow from `/create`. +- **`/app/history/page.tsx`**: Add bundle proofs card + testnet notice (moved from `/app/page.tsx`). +- **`/app/more/page.tsx`**: Expand with proper sections linking to About, Privacy, Terms, Docs page content. +- **`/app/verify/page.tsx`**: Currently redirects to `/verify`. Change to embed the full verify flow from `/verify`. + +### 4. Wallet provider architecture + +``` +Root layout +├── Script (theme) +├── Script (SW) +├── WalletProvider ← MOVED HERE +│ └── ThemeProvider +│ └── ConditionalShell +│ ├── PwaShell (PWA mode, all routes) +│ └── AppShell (browser mode, non-/app routes) +``` + +### 5. No layout duplication + +- `/create` and `/verify` standalone pages keep working via their existing layout chain. +- `/app/*` PWA routes use `PwaShell` via `ConditionalShell`. +- Shared wallet context at root level means components using `useAccount()`, `usePublicClient()`, etc. work everywhere. + +### 6. Desktop and mobile parity + +Same structure on both. No splash screen. Each tab opens directly to its functional content. diff --git a/public/kovina-wordmark-white.svg b/public/kovina-wordmark-white.svg index aaaafbc..6b84a84 100644 --- a/public/kovina-wordmark-white.svg +++ b/public/kovina-wordmark-white.svg @@ -6,5 +6,5 @@ text { font-family: 'Inter', 'Arial', sans-serif; font-weight: 700; } - KOVINA + KOVINA diff --git a/public/kovina-wordmark.svg b/public/kovina-wordmark.svg index 0ba5223..ca28e49 100644 --- a/public/kovina-wordmark.svg +++ b/public/kovina-wordmark.svg @@ -6,5 +6,5 @@ text { font-family: 'Inter', 'Arial', sans-serif; font-weight: 700; } - KOVINA + KOVINA diff --git a/public/scripts/sw-register.js b/public/scripts/sw-register.js new file mode 100644 index 0000000..26c5d3b --- /dev/null +++ b/public/scripts/sw-register.js @@ -0,0 +1,5 @@ +if ('serviceWorker' in navigator && location.hostname !== 'localhost') { + window.addEventListener('load', function() { + navigator.serviceWorker.register('/sw.js').catch(function(){}); + }); +} diff --git a/public/scripts/theme-init.js b/public/scripts/theme-init.js new file mode 100644 index 0000000..75f1242 --- /dev/null +++ b/public/scripts/theme-init.js @@ -0,0 +1,5 @@ +(function(){ + var t=localStorage.getItem('openproof-theme'); + if(!t){t=window.matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'} + document.documentElement.setAttribute('data-theme',t); +})(); diff --git a/src/app/app/history/page.tsx b/src/app/app/history/page.tsx index cdd7d02..19d2c5e 100644 --- a/src/app/app/history/page.tsx +++ b/src/app/app/history/page.tsx @@ -1,13 +1,61 @@ "use client"; +import Link from "next/link"; +import { FileBox, ShieldCheck } from "lucide-react"; import { ProofHistory } from "@/components/proof-history"; export default function AppHistoryPage() { return (
+ {/* Section header */} +
+

+ History +

+

+ Proofs and verifications you've completed in this browser. +

+
+ + {/* Registered proofs */} + + {/* Verified proofs */} + {/* Bundle proofs CTA */} + +
+ +
+
+

Bundle proofs

+

+ Register multiple files as a single combined proof +

+
+ + + {/* Testnet notice */} +
+
+ +
+

+ Base Sepolia testnet +

+

+ OpenProof runs on Base Sepolia — a test network. Proofs here are + for experimentation and verification. No real value is involved. +

+
+
+
+ + {/* Footer spacer for bottom nav on mobile */}
); diff --git a/src/app/app/layout.tsx b/src/app/app/layout.tsx index 1077edd..b3dcec9 100644 --- a/src/app/app/layout.tsx +++ b/src/app/app/layout.tsx @@ -1,7 +1,16 @@ "use client"; +import { usePwaMode } from "@/lib/use-pwa-mode"; import { PwaShell } from "@/components/pwa-shell"; export default function AppLayout({ children }: { children: React.ReactNode }) { + const { isPwa } = usePwaMode(); + + // In installed PWA mode, ConditionalShell already wraps everything in PwaShell. + // Only apply PwaShell here when browsing /app in a regular browser tab. + if (isPwa) { + return <>{children}; + } + return {children}; } diff --git a/src/app/app/loading.tsx b/src/app/app/loading.tsx new file mode 100644 index 0000000..2472dbc --- /dev/null +++ b/src/app/app/loading.tsx @@ -0,0 +1,25 @@ +/** + * Loading boundary for the PWA app route group. + * + * Shows lightweight skeleton cards during route transitions so the user + * never sees a blank flash between /app pages. + */ +export default function AppLoading() { + return ( +
+ {/* Skeleton for quick-action grid */} +
+
+
+
+ + {/* Skeleton for section header */} +
+ + {/* Skeleton for list items */} + {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+ ); +} diff --git a/src/app/app/more/page.tsx b/src/app/app/more/page.tsx index 724cfc6..798bfa7 100644 --- a/src/app/app/more/page.tsx +++ b/src/app/app/more/page.tsx @@ -1,53 +1,109 @@ "use client"; import Link from "next/link"; -import { ExternalLink, ShieldCheck, BookOpen } from "lucide-react"; +import { ExternalLink, ShieldCheck, BookOpen, FileText, Lock, Scale } from "lucide-react"; export default function AppMorePage() { + const links = [ + { + href: "/about", + icon: BookOpen, + label: "About OpenProof", + desc: "Mission, philosophy, and how it works", + }, + { + href: "/docs", + icon: FileText, + label: "Documentation", + desc: "Guides, architecture, and API references", + }, + { + href: "/privacy", + icon: Lock, + label: "Privacy Policy", + desc: "How your data is handled", + }, + { + href: "/terms", + icon: Scale, + label: "Terms of Service", + desc: "Rules and legal stuff", + }, + ]; + return (
-

More

+ {/* Section header */} +
+

+ More +

+

+ Information and resources about OpenProof. +

+
+ {/* Links */}
- - - About OpenProof - - - - - Documentation - - - - GitHub - - + {links.map(({ href, icon: Icon, label, desc }) => ( + +
+ +
+
+

{label}

+ {desc ? ( +

{desc}

+ ) : null} +
+ + ))}
+ {/* GitHub */} + +
+ +
+
+

GitHub

+

+ Source code, issues, and contribution guide +

+
+ +
+ {/* Testnet notice */}
-

- OpenProof runs on Base Sepolia (testnet). - All proofs are for experimental and verification purposes only. -

+
+ +
+

+ Base Sepolia testnet +

+

+ OpenProof runs on Base Sepolia — a test network. + All proofs are for experimental and verification purposes only. +

+
+

OpenProof v0.9.3 · AGPL-3.0

+ {/* Footer spacer for bottom nav on mobile */}
); diff --git a/src/app/app/page.tsx b/src/app/app/page.tsx index 3f9eaa6..14176f4 100644 --- a/src/app/app/page.tsx +++ b/src/app/app/page.tsx @@ -1,96 +1,84 @@ "use client"; -import { useCallback, useState } from "react"; import Link from "next/link"; -import { FileUp, Search, ShieldCheck, FileBox } from "lucide-react"; -import { AppSplash } from "@/components/app-splash"; -import { ProofHistory } from "@/components/proof-history"; +import { FileUp, Search } from "lucide-react"; +import { CreateProofForm } from "@/components/create-proof-form"; export default function AppPage() { - const [showSplash, setShowSplash] = useState(true); - - const handleSplashDone = useCallback(() => { - setShowSplash(false); - }, []); - - if (showSplash) { - return ; - } - return ( -
- {/* Quick actions */} -
- -
- -
-
-

Create proof

-

- Hash a file and register on Base Sepolia -

-
- - - -
- -
-
-

Verify proof

-

- Check a fingerprint against the registry -

-
- -
+ <> + {/* ── Desktop: Landing page with two huge entry cards ── */} +
+ {/* Page title */} +
+

+ OpenProof +

+

+ Proof without surrender. +

+

+ Hash a file locally. Register its fingerprint on Base Sepolia. The file never leaves your browser. +

+
- {/* History */} -
- -
+ {/* Action cards — clean, no containers */} +
+ {/* Create card */} + +
+ +
+
+

+ Create Proof +

+

+ Hash a file and register its fingerprint on the blockchain +

+
+ - {/* Bundle proofs CTA */} - -
- + {/* Verify card */} + +
+ +
+
+

+ Verify Proof +

+

+ Check a fingerprint against the onchain registry +

+
+
+
+ + {/* ── Mobile: Direct create form (current behavior) ── */} +
-

Bundle proofs

-

- Register multiple files as a single combined proof +

+ Create Proof +

+

+ Drop a file to hash it locally, then register its fingerprint on + Base Sepolia. The file never leaves your browser.

- - {/* Testnet notice */} -
-
- -
-

- Base Sepolia testnet -

-

- OpenProof runs on Base Sepolia — a test network. Proofs here are - for experimentation and verification. No real value is involved. -

-
-
-
+ - {/* Footer spacer for bottom nav on mobile */} -
-
+ {/* Footer spacer for bottom nav */} +
+
+ ); } diff --git a/src/app/app/verify/page.tsx b/src/app/app/verify/page.tsx index 70b9038..7bdb5b5 100644 --- a/src/app/app/verify/page.tsx +++ b/src/app/app/verify/page.tsx @@ -1,5 +1,126 @@ -import { redirect } from "next/navigation"; +"use client"; -export default function AppVerifyRedirect() { - redirect("/verify"); +import { CheckCircle2 } from "lucide-react"; +import { useState } from "react"; +import Link from "next/link"; +import type { Dispatch, SetStateAction } from "react"; +import type { PublicClient } from "viem"; +import { usePublicClient } from "wagmi"; +import { + ExplorerLink, + Label, +} from "@/components/design-system"; +import { CopyButton } from "@/components/copy-button"; +import { ProofTimeline } from "@/components/proof-timeline"; +import { ReceiptImport } from "@/components/receipt-import"; +import { VerifyProofForm } from "@/components/verify-proof-form"; +import { transactionExplorerUrl } from "@/lib/explorer"; +import { normalizeClientError } from "@/lib/errors"; +import { openProofChain, openProofContractAddress } from "@/lib/contracts"; +import { addProofHistoryItem } from "@/lib/history"; +import { proofPath } from "@/lib/proof-url"; +import { findProofTransactionHash, isBytes32Hash, readOnchainProof } from "@/lib/proofs"; +import type { ProofReceipt } from "@/lib/receipt"; +import { formatLocalTimestamp } from "@/lib/time"; + +type VResult = + | { status: "idle" | "loading" | "not-found"; message: string } + | { status: "verified"; creator: string; timestamp: string; proofId: string; transactionHash?: string } + | { status: "error"; message: string }; + +export default function AppVerifyPage() { + const [receiptResult, setReceiptResult] = useState({ status: "idle", message: "Import a receipt to validate it onchain." }); + const pc = usePublicClient({ chainId: openProofChain.id }); + + async function verifyReceipt(receipt: ProofReceipt) { + if (!pc) return; + setReceiptResult({ status: "loading", message: "Checking receipt hash..." }); + if (!isBytes32Hash(receipt.sha256Hash)) { setReceiptResult({ status: "error", message: "Receipt hash malformed." }); return; } + if (receipt.chainId !== openProofChain.id) { setReceiptResult({ status: "error", message: `Receipt is for chain ${receipt.chainId}, not ${openProofChain.name}.` }); return; } + if (openProofContractAddress && receipt.contractAddress.toLowerCase() !== openProofContractAddress.toLowerCase()) { setReceiptResult({ status: "error", message: "Receipt contract mismatch." }); return; } + try { + const p = await readOnchainProof(pc, receipt.sha256Hash); + if (!p) { setReceiptResult({ status: "not-found", message: "No matching onchain proof." }); return; } + const nr: VResult = { status: "verified", creator: p.creator, timestamp: p.timestamp, proofId: receipt.sha256Hash, transactionHash: p.transactionHash || receipt.transactionHash }; + setReceiptResult(nr); + hydrateTx(pc, receipt.sha256Hash, setReceiptResult, receipt.transactionHash); + addProofHistoryItem({ proofType: "verified", fileName: receipt.fileName, fileHash: receipt.sha256Hash, txHash: nr.transactionHash, chainName: receipt.chainName, chainId: receipt.chainId, timestamp: p.timestamp, verificationUrl: receipt.verificationUrl, baseScanUrl: nr.transactionHash ? transactionExplorerUrl(nr.transactionHash) : receipt.transactionUrl }); + } catch (e) { setReceiptResult({ status: "error", message: normalizeClientError(e, "Verification failed.") }); } + } + + return ( +
+ {/* ── Page header ───────────────────────────────── */} +
+ +

+ Check a fingerprint
on Base Sepolia. +

+

+ Public read. No wallet required. Hash a file locally and check whether + its fingerprint exists in the onchain registry. Prove a file is unchanged + since it was registered. +

+
+ + {/* ── Scanner: file → hash → verify → result ───── */} + + + {/* ── Receipt import — secondary flow ──────────── */} +
+ +

Validate a downloaded receipt

+

+ Import an OpenProof receipt JSON. Schema checked locally, hash checked onchain. +

+
+ + {receiptResult.status === "verified" ? ( +
+
+ +
+

Receipt valid

+

Schema valid and hash found onchain.

+
+
+ +
+ + + + +
+
+ Open proof page + + + {receiptResult.transactionHash ? <>View on BaseScan : null} +
+
+ ) : receiptResult.status === "not-found" ? ( +

{receiptResult.message}

+ ) : receiptResult.status === "error" ? ( +

{receiptResult.message}

+ ) : receiptResult.status === "loading" ? ( +

Checking...

+ ) : null} +
+ + {/* Footer spacer for bottom nav on mobile */} +
+
+ ); +} + +function DataRow({ label, value }: { label: string; value: string }) { + return
{label}{value}
; +} + +function hydrateTx(pc: PublicClient, h: `0x${string}`, sr: Dispatch>, f?: string) { + findProofTransactionHash(pc, h).then((res) => { const n = res?.transactionHash || f; if (!n) return; sr((c) => c.status === "verified" && c.proofId === h ? { ...c, transactionHash: n } : c); }).catch(() => {}); } diff --git a/src/app/create/page.tsx b/src/app/create/page.tsx index 67255e5..fe177cf 100644 --- a/src/app/create/page.tsx +++ b/src/app/create/page.tsx @@ -1,128 +1,9 @@ "use client"; -import { ConnectButton } from "@rainbow-me/rainbowkit"; -import { Download, Loader2, ShieldCheck } from "lucide-react"; -import { startTransition, useEffect, useMemo, useRef, useState } from "react"; -import { - useAccount, useChainId, usePublicClient, useSwitchChain, - useWaitForTransactionReceipt, useWriteContract, -} from "wagmi"; -import { - ActionPill, ExplorerLink, Label, -} from "@/components/design-system"; -import { CopyButton } from "@/components/copy-button"; -import { FileDrop } from "@/components/file-drop"; -import { HashDisplay } from "@/components/hash-display"; -import { ProofHistory } from "@/components/proof-history"; -import { ProofTimeline } from "@/components/proof-timeline"; -import { ProofQrCode } from "@/components/qr-code"; -import { transactionExplorerUrl } from "@/lib/explorer"; -import { genericWalletErrorMessage, normalizeClientError } from "@/lib/errors"; -import { formatBytes, hashFileSha256 } from "@/lib/hash"; -import { hashBundleFiles, type BundleManifest, type BundleResult } from "@/lib/bundle"; -import { addProofHistoryItem } from "@/lib/history"; -import { - expectedChainId, isContractConfigured, openProofAbi, - openProofChain, openProofContractAddress, -} from "@/lib/contracts"; -import { isWalletConnectConfigured } from "@/components/providers/wallet-provider"; -import { buildProofReceipt, downloadJson, type ProofReceipt } from "@/lib/receipt"; -import { formatLocalTimestamp } from "@/lib/time"; -import { proofUrl } from "@/lib/proof-url"; -import { storeBundleManifest } from "@/lib/bundle-storage"; +import { Label } from "@/components/design-system"; +import { CreateProofForm } from "@/components/create-proof-form"; export default function CreateProofPage() { - const [file, setFile] = useState(null); - const [bundleFiles, setBundleFiles] = useState([]); - const [bundleManifest, setBundleManifest] = useState(null); - const [hash, setHash] = useState<`0x${string}` | null>(null); - const [hashError, setHashError] = useState(null); - const [receipt, setReceipt] = useState(null); - const autoDl = useRef(false); - const [preflightMsg, setPreflightMsg] = useState(null); - const [isChecking, setIsChecking] = useState(false); - const { address, isConnected } = useAccount(); - const chainId = useChainId(); - const pc = usePublicClient({ chainId: openProofChain.id }); - const { switchChain, isPending: isSwitching } = useSwitchChain(); - const { writeContract, data: txHash, error, isPending } = useWriteContract(); - const { data: txReceipt, isLoading: isConfirming } = useWaitForTransactionReceipt({ hash: txHash, chainId: openProofChain.id }); - - const configured = isContractConfigured(); - const isWrong = isConnected && chainId !== expectedChainId; - const isBundle = bundleFiles.length > 1; - const totalSz = bundleFiles.reduce((s, f) => s + f.size, 0); - - // ── Hash file(s) on selection ───────────────────────── - useEffect(() => { - if (!file && !bundleFiles.length) return; - startTransition(() => { setHash(null); setHashError(null); setReceipt(null); setPreflightMsg(null); setBundleManifest(null); }); - autoDl.current = false; - if (isBundle) { - hashBundleFiles(bundleFiles).then((result: BundleResult) => { setHash(result.bundleHash); setBundleManifest(result.manifest); }).catch((e) => setHashError(e instanceof Error ? e.message : "Could not hash bundle.")); - } else if (file) { - hashFileSha256(file).then(setHash).catch((e) => setHashError(e instanceof Error ? e.message : "Could not hash file.")); - } - }, [bundleFiles, file, isBundle]); - - // ── Build receipt after tx confirms ───────────────── - useEffect(() => { - async function load() { - if (!file || !hash || !address || !txHash || !txReceipt || !pc || !openProofContractAddress) return; - let ts: string; - try { const b = await pc.getBlock({ blockHash: txReceipt.blockHash }); ts = new Date(Number(b.timestamp) * 1000).toISOString(); } - catch { ts = new Date().toISOString(); } - setReceipt(buildProofReceipt({ - schemaVersion: 2, receiptVersion: 2, hashAlgorithm: "SHA-256", fileName: file.name, fileSize: isBundle ? totalSz : file.size, - fileMimeType: isBundle ? "application/vnd.openproof.bundle+json" : file.type || "unknown", proofType: isBundle ? "bundle" : "single-file", - bundleFiles: bundleManifest?.files, bundleRuleVersion: isBundle ? 1 : undefined, sha256Hash: hash, chainId: openProofChain.id, - chainName: openProofChain.name, contractAddress: openProofContractAddress, transactionHash: txHash, - transactionUrl: transactionExplorerUrl(txHash), creatorWallet: address, createdTimestamp: ts, - verificationUrl: proofUrl(hash, window.location.origin), - })); - } - load().catch(() => {}); - }, [address, bundleManifest?.files, file, hash, isBundle, pc, totalSz, txHash, txReceipt]); - - // ── Auto-download + history + bundle storage ────── - useEffect(() => { - if (!receipt || autoDl.current) return; - autoDl.current = true; - downloadJson(`openproof-${receipt.sha256Hash.slice(2, 10)}.json`, receipt); - addProofHistoryItem({ proofType: "registered", fileName: receipt.fileName, fileHash: receipt.sha256Hash, txHash: receipt.transactionHash, chainName: receipt.chainName, chainId: receipt.chainId, timestamp: receipt.createdTimestamp, verificationUrl: receipt.verificationUrl, baseScanUrl: receipt.transactionUrl }); - // Store bundle manifest for bundle proof explorer - if (isBundle && bundleManifest) { - storeBundleManifest(receipt.sha256Hash, bundleManifest); - } - }, [receipt, isBundle, bundleManifest]); - - const statusText = useMemo(() => { - if (hashError) return hashError; - if ((file || bundleFiles.length) && !hash) return "Hashing locally..."; - if (isChecking) return "Checking registry..."; - if (isPending) return "Confirm in wallet."; - if (isConfirming) return "Waiting for confirmation..."; - if (receipt) return "Registered on Base Sepolia."; - return "Select a file to begin."; - }, [bundleFiles.length, file, hash, hashError, isChecking, isConfirming, isPending, receipt]); - - async function register() { - if (!hash || !openProofContractAddress || !pc || isWrong) return; - setPreflightMsg(null); setIsChecking(true); - try { - const exists = await pc.readContract({ abi: openProofAbi, address: openProofContractAddress, functionName: "proofExists", args: [hash] }); - if (exists) { - const p = await pc.readContract({ abi: openProofAbi, address: openProofContractAddress, functionName: "getProof", args: [hash] }); - setPreflightMsg(`Already registered by ${p.creator} at ${new Date(Number(p.timestamp) * 1000).toLocaleString()}.`); - return; - } - writeContract({ abi: openProofAbi, address: openProofContractAddress, functionName: "registerProof", args: [hash], chainId: openProofChain.id }); - } catch (e) { setPreflightMsg(normalizeClientError(e, "Could not check. Try again.")); } - finally { setIsChecking(false); } - } - - const canReg = configured && Boolean(hash) && isConnected && !isWrong && isWalletConnectConfigured && !isChecking && !isPending && !isConfirming; - return (
{/* ── Header ────────────────────────────────────── */} @@ -140,136 +21,7 @@ export default function CreateProofPage() { {/* ── Transaction flow — vertical, no cards ────── */}
- {/* Step 1 + 2: File + Hash */} -
- { setHashError(null); setFile(f); setBundleFiles([f]); }} - onFiles={(fs) => { setHashError(null); setBundleFiles(fs); setFile(fs[0] || null); }} - onError={setHashError} - /> - - {file ? ( -
- Name {file.name} - Size {formatBytes(isBundle ? totalSz : file.size)} - Type {isBundle ? `${bundleFiles.length} files` : file.type || "unknown"} -
- ) : null} - - {hash ? : null} -
- - {/* Step 3: Connect + Register */} -
-
- - {isWrong ? ( - switchChain({ chainId: openProofChain.id })}> - {isSwitching ? "Switching..." : "Switch to Base Sepolia"} - - ) : ( - - {isChecking || isPending || isConfirming ? : } - {isChecking ? "Checking" : isBundle ? "Register bundle" : "Register proof"} - - )} -
-

Base Sepolia testnet · No real funds required · Only the hash is written onchain

-
- - {/* Status line */} - {statusText && !receipt ? ( -

- {statusText} -

- ) : null} - - {/* WalletConnect warning */} - {!isWalletConnectConfigured ? ( -
-

WalletConnect not configured.

-

Set NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID.

-
- ) : null} - - {/* Preflight */} - {preflightMsg ? ( -

{preflightMsg}

- ) : null} - - {/* Tx error */} - {error ? ( -

{normalizeClientError(error, genericWalletErrorMessage)}

- ) : null} - - {/* Manual fallback */} - {txHash && !receipt ? ( -
-

Tx: {txHash}

- {txReceipt ? { - if (!file || !hash || !address || !openProofContractAddress) return; - setReceipt(buildProofReceipt({ - schemaVersion: 2, receiptVersion: 2, hashAlgorithm: "SHA-256", fileName: file.name, fileSize: isBundle ? totalSz : file.size, - fileMimeType: isBundle ? "application/vnd.openproof.bundle+json" : file.type || "unknown", proofType: isBundle ? "bundle" : "single-file", - bundleFiles: bundleManifest?.files, bundleRuleVersion: isBundle ? 1 : undefined, sha256Hash: hash, chainId: openProofChain.id, - chainName: openProofChain.name, contractAddress: openProofContractAddress, transactionHash: txHash, - transactionUrl: transactionExplorerUrl(txHash), creatorWallet: address, createdTimestamp: new Date().toISOString(), - verificationUrl: proofUrl(hash, window.location.origin), - })); - }}>Generate receipt : null} -
- ) : null} - - {/* ── Ticket / Receipt ───────────────────────── */} - {receipt ? ( -
-
- -
-

Registered

-

Receipt auto-downloaded.

-
-
- - - - {/* Receipt actions */} -
- View on BaseScan - downloadJson(`openproof-${receipt.sha256Hash.slice(2, 10)}.json`, receipt)}> Download receipt JSON - - - -
- - {/* QR */} -
- - {/* Receipt data — ticket style */} -
-

Receipt

-
- {Object.entries(receipt).map(([k, v]) => ( -
-
{k}
-
{k === "createdTimestamp" ? formatLocalTimestamp(String(v)) : String(v)}
-
- ))} -
-

— OpenProof v0.9.3 —

-
-
- ) : null} -
- - {/* ── History ──────────────────────────────────── */} -
- +
); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a8c8805..98fb7ba 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { Geist_Mono } from "next/font/google"; import "@fontsource-variable/stack-sans-notch"; -import { AppShell } from "@/components/app-shell"; +import { WalletProvider } from "@/components/providers/wallet-provider"; import { ConditionalShell } from "@/components/conditional-shell"; import { ThemeProvider } from "@/components/providers/theme-provider"; import { ErrorBoundary } from "@/components/error-boundary"; @@ -100,22 +100,12 @@ export default function RootLayout({ -