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 ───────────────────────────────── */}
+
+
Verify Proof
+
+ Check a fingerprinton 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 ──────────── */}
+
+ Receipt import
+ 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({
-