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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@

All notable changes to OpenProof are documented here.

## 0.9.3 — Installed PWA App Experience

### PWA App Shell
- New `/app` route — focused workbench for installed PWA users
- App splash screen using existing icon assets (light/dark, respects reduced motion)
- Compact app header with online/offline status, Base Sepolia badge, theme toggle
- Mobile bottom tab navigation: Create, Verify, History, More
- App sub-routes: `/app/history` (combined registered + verified), `/app/more` (About, Docs, GitHub)
- `/app/verify` redirects to existing `/verify` page

### Display-Mode Detection
- New `usePwaMode()` hook — detects `display-mode: standalone`, `fullscreen`, `minimal-ui`, iOS standalone, and `?source=pwa`/`?mode=pwa` fallback
- Returns `isPwa`, `displayMode`, `isMobile`, `isDesktop`, `prefersReducedMotion`
- Safe client-only detection with media query listeners

### Manifest
- `start_url` changed from `/` to `/app?source=pwa` — installed PWA launches into the workbench
- Website mode still serves the public marketing site from `/`

### Architecture
- Route group `src/app/app/` uses its own layout with `PwaShell` (no marketing footer)
- Public routes (`/`, `/about`, `/privacy`, `/terms`, `/create`, `/verify`, `/proof/[hash]`, `/bundle/[hash]`) continue using the website `AppShell`
- History system reused — no new backend, no accounts, no sync
- Existing CSP, wallet compatibility, accessibility preserved

## 0.9.2 — GeneratedAssets & PWA Refresh

### Assets
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ OpenProof is part of a broader direction: software that helps people prove thing
| v0.9.0 | 2026-06-25 | Native Platform Readiness — MSIX packaging, Android (Capacitor), PWA polish, domain migration |
| v0.9.1 | 2026-07-02 | Open Product Family branding — lockup alignment, Kovina Collection footer, dual-theme icon overhaul |
| v0.9.2 | 2026-07-04 | GeneratedAssets replacement, Kovina footer, PWA icon refresh, README overhaul |
| v0.9.3 | 2026-07-04 | Installed PWA App Experience — `/app` workbench, splash screen, bottom nav, compact header, display-mode detection |

## Quick Links

Expand Down
53 changes: 53 additions & 0 deletions docs/pwa-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Website Mode vs Installed PWA Mode

OpenProof supports two interaction models sharing the same product identity.

## Website Mode (default)

- Accessed via any browser at `https://proof.kovina.org`
- Full marketing/editorial pages: Home, About, Privacy, Terms, Docs
- SEO-optimized metadata and OpenGraph tags
- Traditional website footer with navigation links
- Create, Verify, and proof explorer pages with explanatory copy
- PWA install prompt available but optional

**Routes:**
- `/` — Marketing homepage
- `/create` — Create proof (with explanatory copy)
- `/verify` — Verify proof (with receipt import)
- `/about`, `/privacy`, `/terms`, `/docs` — Editorial pages
- `/proof/[hash]` — Public proof page
- `/bundle/[hash]` — Bundle proof page

## Installed PWA Mode

- Accessed after installing OpenProof as a PWA (Chrome, Safari, Edge)
- Launches at `/app?source=pwa` (configured in manifest.json)
- Focused workbench with minimal copy — action-first
- No marketing footer or editorial framing
- Compact app header with online/offline status
- Mobile bottom tab navigation (Create, Verify, History, More)
- Desktop: same workbench with keyboard-friendly layout
- App splash screen on first load (light/dark theme-aware)

**Routes:**
- `/app` — App workbench (quick actions + recent proofs + testnet notice)
- `/app/verify` — Redirects to `/verify`
- `/app/history` — Combined registered + verified proof history
- `/app/more` — About, Docs, GitHub links

## Detection

The app detects PWA mode via `usePwaMode()` which checks:
- `display-mode: standalone` CSS media query
- `display-mode: fullscreen` / `minimal-ui`
- iOS `navigator.standalone`
- URL parameter `?source=pwa` / `?mode=pwa` (fallback)

## Key Constraints

- No accounts, analytics, telemetry, backend, sync, or notifications
- All proof history remains in browser localStorage only
- Same wallet (RainbowKit/wagmi) and chain (Base Sepolia)
- Same CSP and security headers
- Static export / Capacitor compatible
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openproof",
"version": "0.9.2",
"version": "0.9.3",
"private": true,
"license": "AGPL-3.0-only",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "OpenProof",
"short_name": "OpenProof",
"description": "Privacy-first proof-of-existence for files. Hash files locally, register only the SHA-256 fingerprint on Base Sepolia. No uploads, no accounts, no backend.",
"start_url": "/",
"start_url": "/app?source=pwa",
"display": "standalone",
"display_override": [
"window-controls-overlay",
Expand Down
14 changes: 14 additions & 0 deletions src/app/app/history/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use client";

import { ProofHistory } from "@/components/proof-history";

export default function AppHistoryPage() {
return (
<div className="space-y-8">
<ProofHistory title="Registered proofs" type="registered" />
<ProofHistory title="Verified proofs" type="verified" />

<div className="h-20 md:hidden" />
</div>
);
}
7 changes: 7 additions & 0 deletions src/app/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import { PwaShell } from "@/components/pwa-shell";

export default function AppLayout({ children }: { children: React.ReactNode }) {
return <PwaShell>{children}</PwaShell>;
}
54 changes: 54 additions & 0 deletions src/app/app/more/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";

import Link from "next/link";
import { ExternalLink, ShieldCheck, BookOpen } from "lucide-react";

export default function AppMorePage() {
return (
<div className="space-y-6">
<h1 className="text-xl font-bold">More</h1>

<div className="space-y-3">
<Link
href="/about"
className="flex items-center gap-4 rounded-2xl border border-border-default bg-bg-surface p-4 transition hover:border-accent/50"
>
<BookOpen className="size-5 text-accent" />
<span className="font-semibold">About OpenProof</span>
</Link>

<Link
href="/docs"
className="flex items-center gap-4 rounded-2xl border border-border-default bg-bg-surface p-4 transition hover:border-accent/50"
>
<ShieldCheck className="size-5 text-accent" />
<span className="font-semibold">Documentation</span>
</Link>

<a
href="https://github.com/sparshsam/openproof"
target="_blank"
rel="noreferrer"
className="flex items-center gap-4 rounded-2xl border border-border-default bg-bg-surface p-4 transition hover:border-accent/50"
>
<span className="font-semibold">GitHub</span>
<ExternalLink className="ml-auto size-4 text-text-muted" />
</a>
</div>

{/* Testnet notice */}
<div className="rounded-2xl border border-border-default bg-bg-surface p-4">
<p className="text-xs text-text-secondary leading-relaxed">
OpenProof runs on <strong className="text-text-primary">Base Sepolia</strong> (testnet).
All proofs are for experimental and verification purposes only.
</p>
</div>

<p className="text-center text-xs text-text-muted">
OpenProof v0.9.3 &middot; AGPL-3.0
</p>

<div className="h-20 md:hidden" />
</div>
);
}
96 changes: 96 additions & 0 deletions src/app/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"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";

export default function AppPage() {
const [showSplash, setShowSplash] = useState(true);

const handleSplashDone = useCallback(() => {
setShowSplash(false);
}, []);

if (showSplash) {
return <AppSplash onDone={handleSplashDone} />;
}

return (
<div className="space-y-8">
{/* Quick actions */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Link
href="/create"
className="flex items-center gap-4 rounded-2xl border border-border-default bg-bg-surface p-5 transition hover:border-accent/50 hover:bg-accent/[0.02]"
>
<div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-accent/10">
<FileUp className="size-6 text-accent" />
</div>
<div>
<p className="font-bold text-text-primary">Create proof</p>
<p className="mt-0.5 text-sm text-text-secondary">
Hash a file and register on Base Sepolia
</p>
</div>
</Link>

<Link
href="/verify"
className="flex items-center gap-4 rounded-2xl border border-border-default bg-bg-surface p-5 transition hover:border-accent/50 hover:bg-accent/[0.02]"
>
<div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-accent/10">
<Search className="size-6 text-accent" />
</div>
<div>
<p className="font-bold text-text-primary">Verify proof</p>
<p className="mt-0.5 text-sm text-text-secondary">
Check a fingerprint against the registry
</p>
</div>
</Link>
</div>

{/* History */}
<section>
<ProofHistory title="Recent proofs" type="registered" />
</section>

{/* Bundle proofs CTA */}
<Link
href="/create"
className="flex items-center gap-4 rounded-2xl border border-border-default bg-bg-surface p-5 transition hover:border-accent/50"
>
<div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-accent/10">
<FileBox className="size-6 text-accent" />
</div>
<div>
<p className="font-bold text-text-primary">Bundle proofs</p>
<p className="mt-0.5 text-sm text-text-secondary">
Register multiple files as a single combined proof
</p>
</div>
</Link>

{/* Testnet notice */}
<div className="rounded-2xl border border-border-default bg-bg-surface p-5">
<div className="flex items-start gap-3">
<ShieldCheck className="mt-0.5 size-5 shrink-0 text-text-muted" />
<div>
<p className="text-sm font-semibold text-text-primary">
Base Sepolia testnet
</p>
<p className="mt-1 text-xs leading-relaxed text-text-secondary">
OpenProof runs on Base Sepolia — a test network. Proofs here are
for experimentation and verification. No real value is involved.
</p>
</div>
</div>
</div>

{/* Footer spacer for bottom nav on mobile */}
<div className="h-20 md:hidden" />
</div>
);
}
5 changes: 5 additions & 0 deletions src/app/app/verify/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";

export default function AppVerifyRedirect() {
redirect("/verify");
}
2 changes: 1 addition & 1 deletion src/app/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ export default function CreateProofPage() {
</div>
))}
</dl>
<p className="mt-6 text-center text-xs text-text-muted">&mdash; OpenProof v0.9.2 &mdash;</p>
<p className="mt-6 text-center text-xs text-text-muted">&mdash; OpenProof v0.9.3 &mdash;</p>
</div>
</div>
) : null}
Expand Down
5 changes: 3 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Metadata } from "next";
import { Geist_Mono } from "next/font/google";
import "@fontsource-variable/stack-sans-notch";
import { AppShell } from "@/components/app-shell";

Check warning on line 4 in src/app/layout.tsx

View workflow job for this annotation

GitHub Actions / Validate

'AppShell' is defined but never used
import { ConditionalShell } from "@/components/conditional-shell";
import { ThemeProvider } from "@/components/providers/theme-provider";
import { ErrorBoundary } from "@/components/error-boundary";
import { OfflineNotice } from "@/components/offline-notice";
Expand Down Expand Up @@ -112,9 +113,9 @@
}}
/>
<ThemeProvider>
<AppShell>
<ConditionalShell>
<ErrorBoundary>{children}</ErrorBoundary>
</AppShell>
</ConditionalShell>
<OfflineNotice />
<PwaInstallPrompt />
</ThemeProvider>
Expand Down
77 changes: 77 additions & 0 deletions src/components/app-splash.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"use client";

import { useEffect, useState } from "react";
import { usePwaMode } from "@/lib/use-pwa-mode";

const SPLASH_DURATION = 1200; // ms

export function AppSplash({ onDone }: { onDone: () => void }) {
const [visible, setVisible] = useState(true);
const { prefersReducedMotion } = usePwaMode();

useEffect(() => {
const timer = setTimeout(() => {
setVisible(false);
// Small delay after fade for React to settle
setTimeout(onDone, 300);
}, SPLASH_DURATION);

return () => clearTimeout(timer);
}, [onDone]);

if (!visible) return null;

return (
<div
className={`fixed inset-0 z-[200] flex flex-col items-center justify-center bg-bg-base ${
prefersReducedMotion ? "" : "animate-out fade-out"
}`}
style={{ animationDuration: "300ms", animationFillMode: "forwards" }}
>
<style>{`
[data-theme="dark"] .splash-light { opacity: 0 !important; }
[data-theme="dark"] .splash-dark { opacity: 1 !important; }
@keyframes splash-fade-in {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
.splash-icon {
animation: splash-fade-in 400ms ease-out;
}
@keyframes splash-pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.8; }
}
.splash-loader {
animation: splash-pulse 1.2s ease-in-out infinite;
}
`}</style>

<div className="splash-icon relative size-24">
<img
alt=""
className="splash-light absolute inset-0 size-24 rounded-2xl"
src="/icon-192.png"
/>
<img
alt=""
className="splash-dark absolute inset-0 size-24 rounded-2xl opacity-0"
src="/icon-dark-192.png"
/>
</div>

<h1 className="mt-6 text-2xl font-black tracking-tight text-text-primary">
OpenProof
</h1>
<p className="mt-2 text-sm text-text-secondary">
Proof without surrender.
</p>

<div className="splash-loader mt-10 flex items-center gap-1.5">
<span className="size-2 rounded-full bg-accent" />
<span className="size-2 rounded-full bg-accent/60" />
<span className="size-2 rounded-full bg-accent/30" />
</div>
</div>
);
}
Loading
Loading