From 550dcccbda0efb7151f1d5fd319ea206293bf702 Mon Sep 17 00:00:00 2001 From: shaurya2k06 Date: Mon, 26 Jan 2026 03:47:12 +0530 Subject: [PATCH 1/3] Feat: Added UI for onboarding --- .github/dependabot.yml | 42 ++ .github/workflows/deploy.yml | 46 ++ .github/workflows/pr-checks.yml | 78 +++ frontend/package-lock.json | 48 +- frontend/package.json | 1 + frontend/public/fonts/Advercase.otf | Bin 0 -> 27920 bytes frontend/src/App.css | 143 +---- frontend/src/App.tsx | 63 ++- frontend/src/components/ChallengeScanner.tsx | 97 ++-- frontend/src/components/Dashboard.tsx | 126 ----- frontend/src/components/Header.tsx | 14 +- frontend/src/components/IdentityStatus.tsx | 65 ++- frontend/src/components/Layout.tsx | 57 ++ .../src/components/OnboardingFlow.old.tsx | 527 ++++++++++++++++++ frontend/src/components/OnboardingFlow.tsx | 444 +++++++++++++++ .../src/components/OnboardingFlow.tsx.backup | 527 ++++++++++++++++++ frontend/src/components/ProofsDashboard.tsx | 40 +- frontend/src/components/ProtectedRoute.tsx | 48 ++ frontend/src/components/QRScanner.tsx | 123 ++-- frontend/src/components/Sidebar.tsx | 148 +++++ frontend/src/components/ui/text-animate.tsx | 52 ++ frontend/src/index.css | 118 +++- frontend/src/lib/anchor.ts | 4 +- frontend/src/lib/crypto.ts | 24 + frontend/src/lib/onboarding.ts | 108 ++++ frontend/src/pages/ChallengeScannerPage.tsx | 5 + frontend/src/pages/Home.tsx | 209 ------- frontend/src/pages/IdentityStatusPage.tsx | 20 + frontend/src/pages/ProofsPage.tsx | 5 + frontend/src/pages/QRScannerPage.tsx | 5 + 30 files changed, 2477 insertions(+), 710 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/pr-checks.yml create mode 100644 frontend/public/fonts/Advercase.otf delete mode 100644 frontend/src/components/Dashboard.tsx create mode 100644 frontend/src/components/Layout.tsx create mode 100644 frontend/src/components/OnboardingFlow.old.tsx create mode 100644 frontend/src/components/OnboardingFlow.tsx create mode 100644 frontend/src/components/OnboardingFlow.tsx.backup create mode 100644 frontend/src/components/ProtectedRoute.tsx create mode 100644 frontend/src/components/Sidebar.tsx create mode 100644 frontend/src/components/ui/text-animate.tsx create mode 100644 frontend/src/lib/crypto.ts create mode 100644 frontend/src/lib/onboarding.ts create mode 100644 frontend/src/pages/ChallengeScannerPage.tsx delete mode 100644 frontend/src/pages/Home.tsx create mode 100644 frontend/src/pages/IdentityStatusPage.tsx create mode 100644 frontend/src/pages/ProofsPage.tsx create mode 100644 frontend/src/pages/QRScannerPage.tsx diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d06f930 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,42 @@ +version: 2 +updates: + # Frontend dependencies + - package-ecosystem: "npm" + directory: "/frontend" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "frontend" + reviewers: + - "Shaurya2k06" + commit-message: + prefix: "chore(frontend)" + include: "scope" + groups: + react: + patterns: + - "react*" + - "@types/react*" + vite: + patterns: + - "vite*" + - "@vitejs/*" + solana: + patterns: + - "@solana/*" + - "@coral-xyz/*" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "chore(ci)" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..f8eca6b --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,46 @@ +name: Deploy Frontend to Vercel + +on: + push: + branches: + - main + paths: + - 'frontend/**' + - '.github/workflows/deploy.yml' + pull_request: + branches: + - main + workflow_dispatch: + +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install Vercel CLI + run: npm install --global vercel@latest + + - name: Pull Vercel Environment Information + working-directory: ./frontend + run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} + + - name: Build Project Artifacts + working-directory: ./frontend + run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} + + - name: Deploy Project Artifacts to Vercel + working-directory: ./frontend + run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 0000000..795de56 --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,78 @@ +name: PR Checks + +on: + pull_request: + branches: + - main + - develop + +jobs: + lint-and-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install frontend dependencies + working-directory: ./frontend + run: npm ci + + - name: Run linter + working-directory: ./frontend + run: npm run lint --if-present || echo "No lint script found" + + - name: Type check + working-directory: ./frontend + run: npm run type-check --if-present || npx tsc --noEmit + + - name: Run tests + working-directory: ./frontend + run: npm test --if-present || echo "No test script found" + + - name: Build project + working-directory: ./frontend + run: npm run build + + - name: Check for security vulnerabilities + working-directory: ./frontend + run: npm audit --audit-level=moderate + + code-quality: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: Install frontend dependencies + working-directory: ./frontend + run: npm ci + + - name: Check code formatting + working-directory: ./frontend + run: npx prettier --check "src/**/*.{ts,tsx}" || echo "No prettier config found" + + copilot-review: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - uses: actions/checkout@v4 + + - name: GitHub Copilot PR Review + uses: github/gh-copilot-review@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5882b8d..620b47b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -35,6 +35,7 @@ "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", "@vitejs/plugin-react": "^5.0.3", + "baseline-browser-mapping": "^2.9.18", "eslint": "^9.36.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", @@ -10528,17 +10529,6 @@ "license": "Apache-2.0", "optional": true }, - "node_modules/bare-url": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.1.tgz", - "integrity": "sha512-v2yl0TnaZTdEnelkKtXZGnotiV6qATBlnNuUMrHl6v9Lmmrh9mw9RYyImPU7/4RahumSwQS1k2oKXcRfXcbjJw==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base-x": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", @@ -10587,9 +10577,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.21", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.21.tgz", - "integrity": "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==", + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", + "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -11036,21 +11026,6 @@ "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", "license": "MIT" }, - "node_modules/bufferutil": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", - "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -18881,21 +18856,6 @@ "node": "^18 || ^20 || >= 21" } }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 858eb20..0cbaa37 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,7 @@ "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", "@vitejs/plugin-react": "^5.0.3", + "baseline-browser-mapping": "^2.9.18", "eslint": "^9.36.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", diff --git a/frontend/public/fonts/Advercase.otf b/frontend/public/fonts/Advercase.otf new file mode 100644 index 0000000000000000000000000000000000000000..99b0fb1997cef3f648cf3b262f91e4e0dd7f5670 GIT binary patch literal 27920 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 KUQdI&{{Ig`-rrCF literal 0 HcmV?d00001 diff --git a/frontend/src/App.css b/frontend/src/App.css index 08f506a..db953e5 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,142 +1 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -@theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -:root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} - -/* QR Scanner scanning animation */ -@keyframes scan { - 0% { - top: 0; - opacity: 0; - } - 25% { - opacity: 1; - } - 75% { - opacity: 1; - } - 100% { - top: 100%; - opacity: 0; - } -} - -.animate-scan { - animation: scan 2s ease-in-out infinite; -} \ No newline at end of file +/* Cleared to avoid conflicts with index.css design system */ \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0c946cb..197c3dd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,13 +1,17 @@ import { useMemo } from 'react'; -import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets'; import { clusterApiUrl } from '@solana/web3.js'; import { SolsticeProvider } from './contexts/SolsticeContext'; -import { Dashboard } from './components/Dashboard'; -import { Header } from './components/Header'; -import Home from './pages/Home'; +import { Layout } from './components/Layout'; +import { OnboardingFlow } from './components/OnboardingFlow'; +import { ProtectedRoute } from './components/ProtectedRoute'; +import { IdentityStatusPage } from './pages/IdentityStatusPage'; +import { QRScannerPage } from './pages/QRScannerPage'; +import { ChallengeScannerPage } from './pages/ChallengeScannerPage'; +import { ProofsPage } from './pages/ProofsPage'; import './App.css'; import '@solana/wallet-adapter-react-ui/styles.css'; @@ -31,18 +35,45 @@ function App() { - } /> - -
-
- -
- - } - /> + {/* Onboarding Route - First time users */} + } /> + + {/* Main App Routes - Protected, requires onboarding */} + }> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + diff --git a/frontend/src/components/ChallengeScanner.tsx b/frontend/src/components/ChallengeScanner.tsx index bf701ce..c0c89fc 100644 --- a/frontend/src/components/ChallengeScanner.tsx +++ b/frontend/src/components/ChallengeScanner.tsx @@ -27,7 +27,7 @@ export function ChallengeScanner() { const [error, setError] = useState(null); const [cameraError, setCameraError] = useState(null); const [pasteError, setPasteError] = useState(null); - + const fileInputRef = useRef(null); const videoRef = useRef(null); const canvasRef = useRef(null); @@ -48,7 +48,7 @@ export function ChallengeScanner() { canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx?.drawImage(img, 0, 0); - + const imageData = ctx?.getImageData(0, 0, canvas.width, canvas.height); if (imageData) { const code = jsQR(imageData.data, imageData.width, imageData.height); @@ -74,21 +74,21 @@ export function ChallengeScanner() { setError(null); const clipboardItems = await navigator.clipboard.read(); - + for (const item of clipboardItems) { if (item.types.includes('image/png') || item.types.includes('image/jpeg')) { const blob = await item.getType(item.types.find(t => t.startsWith('image/'))!); - + const img = new Image(); const url = URL.createObjectURL(blob); - + img.onload = async () => { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx?.drawImage(img, 0, 0); - + const imageData = ctx?.getImageData(0, 0, canvas.width, canvas.height); if (imageData) { const code = jsQR(imageData.data, imageData.width, imageData.height); @@ -100,12 +100,12 @@ export function ChallengeScanner() { } URL.revokeObjectURL(url); }; - + img.src = url; return; } } - + setPasteError('No image found in clipboard. Please copy an image first.'); } catch (error: any) { console.error('Error pasting image:', error); @@ -125,7 +125,7 @@ export function ChallengeScanner() { if (videoRef.current) { videoRef.current.srcObject = stream; streamRef.current = stream; - + videoRef.current.onloadedmetadata = () => { videoRef.current?.play(); scanQRFromVideo(); @@ -134,7 +134,7 @@ export function ChallengeScanner() { } catch (error: any) { console.error('Camera error:', error); setCameraError( - error.name === 'NotAllowedError' + error.name === 'NotAllowedError' ? 'Camera permission denied. Please allow camera access and try again.' : 'Failed to access camera.' ); @@ -171,7 +171,7 @@ export function ChallengeScanner() { if (imageData) { const code = jsQR(imageData.data, imageData.width, imageData.height); if (code) { - console.log('✅ Challenge QR code detected from camera!'); + console.log(' Challenge QR code detected from camera!'); stopCamera(); handleChallengeQRData(code.data); return; @@ -192,7 +192,7 @@ export function ChallengeScanner() { try { setError(null); console.log('Processing challenge QR code...'); - + // Parse challenge from QR code let parsed; try { @@ -256,21 +256,21 @@ export function ChallengeScanner() { if (challenge.callbackUrl) { console.log('📤 Submitting proof to:', challenge.callbackUrl); console.log('📦 Proof response:', proofResponse); - + // Extract challenge ID from callback URL for verification const urlMatch = challenge.callbackUrl.match(/challenges\/([^/]+)\//); const expectedChallengeId = urlMatch ? urlMatch[1] : null; console.log('🎯 Expected challenge ID from URL:', expectedChallengeId); - + if (expectedChallengeId && proofResponse.challengeId !== expectedChallengeId) { console.warn('⚠️ Challenge ID mismatch detected!'); console.warn(' From QR:', proofResponse.challengeId); console.warn(' From URL:', expectedChallengeId); // Use the challenge ID from the URL proofResponse.challengeId = expectedChallengeId; - console.log('✅ Corrected challenge ID to match URL'); + console.log(' Corrected challenge ID to match URL'); } - + const response = await fetch(challenge.callbackUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -279,22 +279,22 @@ export function ChallengeScanner() { if (!response.ok) { const errorData = await response.json().catch(() => ({ error: 'Unknown error' })); - console.error('❌ Backend error:', errorData); + console.error(' Backend error:', errorData); throw new Error(errorData.error || 'Failed to submit proof to app'); } - - console.log('✅ Proof accepted by backend'); + + console.log(' Proof accepted by backend'); } setSuccess(true); - console.log('✅ Proof submitted successfully!'); - + console.log(' Proof submitted successfully!'); + // Refresh identity status after successful proof submission if (wallet.publicKey) { await fetchIdentity(wallet.publicKey.toString()); console.log('🔄 Identity status refreshed'); } - + // Reset after 3 seconds setTimeout(() => { setChallenge(null); @@ -358,28 +358,28 @@ export function ChallengeScanner() { return (
-
-

Verification Challenge

- +
+

Verification Challenge

+
- -

{challenge.appName}

+ +

{challenge.appName}

- -

{getProofTypeDescription(challenge.proofType, challenge.params)}

+ +

{getProofTypeDescription(challenge.proofType, challenge.params)}

- -

{challenge.challengeId.substring(0, 16)}...

+ +

{challenge.challengeId.substring(0, 16)}...

{!isExpired && (
- +

{minutes}:{seconds.toString().padStart(2, '0')}

@@ -397,13 +397,12 @@ export function ChallengeScanner() { @@ -435,8 +434,8 @@ export function ChallengeScanner() { return (
-

Scan Verification Challenge

-

+

Scan Verification Challenge

+

Scan a challenge QR code from a third-party app that requires verification

@@ -458,7 +457,7 @@ export function ChallengeScanner() { <> - - - -
- - {/* Tab Content */} -
- {activeTab === 'scan' && } - {activeTab === 'challenge' && } - {activeTab === 'verify' && } - {activeTab === 'status' && } -
-
-
- ); -} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 9d13868..3878902 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -3,20 +3,20 @@ import { Shield } from 'lucide-react'; export function Header() { return ( -
+
-
- +
+
-

Solstice Protocol

-

Zero-Knowledge Identity Verification

+

Solstice Protocol

+

Zero-Knowledge Identity Verification

- - + +
diff --git a/frontend/src/components/IdentityStatus.tsx b/frontend/src/components/IdentityStatus.tsx index 4f16ccd..ccb22ac 100644 --- a/frontend/src/components/IdentityStatus.tsx +++ b/frontend/src/components/IdentityStatus.tsx @@ -20,21 +20,21 @@ export function IdentityStatus({ identity, loading, expanded = false }: Identity if (loading) { return ( -
-
-
+
+
+
); } if (!identity) { return ( -
+
- +
-

No Identity Registered

-

Scan your Aadhaar QR code to get started

+

No Identity Registered

+

Scan your Aadhaar QR code to get started

@@ -44,7 +44,7 @@ export function IdentityStatus({ identity, loading, expanded = false }: Identity return ( <> {/* Main Status Card */} -
+
{identity.isVerified ? ( @@ -52,17 +52,17 @@ export function IdentityStatus({ identity, loading, expanded = false }: Identity )}
-

+

{identity.isVerified ? 'Verified Identity' : 'Pending Verification'}

-

+

{identity.isVerified ? 'Ready for authentication' : 'Complete attribute verification'}

- + {identity.verificationTimestamp && ( -

+

Verified: {new Date(identity.verificationTimestamp * 1000).toLocaleDateString()}

)} @@ -87,27 +87,27 @@ export function IdentityStatus({ identity, loading, expanded = false }: Identity {/* Expanded Details */} {expanded && ( -
-

Identity Details

- +
+

Identity Details

+
- {identity.verificationTimestamp && ( - )}
-
-

Verification Breakdown

+
+

Verification Breakdown

@@ -122,16 +122,16 @@ export function IdentityStatus({ identity, loading, expanded = false }: Identity function AttributeCard({ title, verified, description }: { title: string; verified: boolean; description: string }) { return ( -
+
{verified ? ( ) : ( - + )} -

{title}

+

{title}

-

{description}

+

{description}

); } @@ -139,17 +139,16 @@ function AttributeCard({ title, verified, description }: { title: string; verifi function DetailItem({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) { return (
-

{label}

-

{value}

+

{label}

+

{value}

); } function VerificationBadge({ label, verified }: { label: string; verified: boolean }) { return ( - + {label}: {verified ? '' : ''} ); diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000..76e121e --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react'; +import { Outlet } from 'react-router-dom'; +import { Sidebar, MobileMenuButton } from './Sidebar'; +import { Header } from './Header'; + +export function Layout() { + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + + return ( +
+ {/* Mobile Menu Button - positioned absolutely/fixed by the component itself */} + setIsSidebarOpen(true)} /> + + {/* Layout Container */} +
+ {/* Sidebar Navigation */} + setIsSidebarOpen(false)} + /> + + {/* Main Content Area */} +
+ + {/* Background Elements (Subtle) */} +
+
+
+
+ + {/* Header - Sticky within the main content area */} +
+
+
+ + {/* Page Content (Nested Routes) */} +
+ +
+ + {/* Footer - Internal App Footer */} +
+
+
+

Solstice Protocol App

+
+ Privacy + Terms +
+
+
+
+
+
+
+ ); +} diff --git a/frontend/src/components/OnboardingFlow.old.tsx b/frontend/src/components/OnboardingFlow.old.tsx new file mode 100644 index 0000000..67b42a5 --- /dev/null +++ b/frontend/src/components/OnboardingFlow.old.tsx @@ -0,0 +1,527 @@ +import { useState, useEffect, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useWallet } from '@solana/wallet-adapter-react'; +import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; +import { useSolstice } from '../contexts/SolsticeContext'; +import { CheckCircle, Loader, Shield, QrCode, ChevronRight, AlertCircle, Camera, Upload, X } from 'lucide-react'; +import jsQR from 'jsqr'; +import { generateAllProofs, storeProofs } from '../lib/proofGenerator'; +import { parseAadhaarQR } from '../lib/aadhaarParser'; +import { + isOnboardingComplete, + recordOnboardingCompletion, + linkAadhaarToWallet, + verifyAadhaarWalletLink +} from '../lib/onboarding'; + +type OnboardingStep = 'wallet' | 'verify' | 'complete'; + +export function OnboardingFlow() { + const navigate = useNavigate(); + const wallet = useWallet(); + const { identity, fetchIdentity, registerIdentity, parseQRCode } = useSolstice(); + const [currentStep, setCurrentStep] = useState('wallet'); + const [scanning, setScanning] = useState(false); + const [processing, setProcessing] = useState(false); + const [error, setError] = useState(null); + const [aadhaarData, setAadhaarData] = useState(null); + const [scanMethod, setScanMethod] = useState<'camera' | 'upload' | null>(null); + const videoRef = useRef(null); + const canvasRef = useRef(null); + const fileInputRef = useRef(null); + const streamRef = useRef(null); + const animationFrameRef = useRef(null); + + // Check if user has already completed onboarding + useEffect(() => { + const checkOnboarding = async () => { + if (wallet.publicKey) { + const onboardedLocally = isOnboardingComplete(wallet.publicKey.toString()); + await fetchIdentity(wallet.publicKey.toString()); + } + }; + + checkOnboarding(); + }, [wallet.publicKey, fetchIdentity]); + + // Auto-advance steps + useEffect(() => { + if (wallet.connected && wallet.publicKey && currentStep === 'wallet') { + const onboarded = isOnboardingComplete(wallet.publicKey.toString()); + if (onboarded && identity) { + navigate('/status'); + } else { + setCurrentStep('verify'); + } + } + }, [wallet.connected, wallet.publicKey, currentStep, identity, navigate]); + + const handleQRScan = async (qrData: string) => { + if (processing) return; + + setProcessing(true); + setError(null); + setScanMethod(null); + setScanning(false); + + try { + console.log('📱 Processing Aadhaar QR code...'); + + const parsedData = await parseAadhaarQR(qrData); + + if (!parsedData) { + throw new Error('Invalid Aadhaar QR code'); + } + + setAadhaarData(parsedData); + console.log(' Aadhaar data parsed'); + + const nationality = 'IN'; + const aadhaarHash = qrData.substring(0, 12); + const nonce = qrData.substring(qrData.length - 32); + + const proofInputs = { + dateOfBirth: parsedData.dateOfBirth.split('/').reverse().join(''), + nationality, + aadhaarNumber: aadhaarHash, + nonce + }; + + console.log('⚙️ Generating ZK proofs...'); + const proofResults = await generateAllProofs(proofInputs); + + if (proofResults.errors.length > 0) { + throw new Error(`Proof generation errors: ${proofResults.errors.join(', ')}`); + } + + const proofsToStore = { + age: proofResults.ageProof || undefined, + nationality: proofResults.nationalityProof || undefined, + uniqueness: proofResults.uniquenessProof || undefined + }; + + console.log(' All proofs generated'); + + await storeProofs(wallet.publicKey!.toString(), proofsToStore); + console.log('💾 Proofs stored locally'); + + console.log(' Generating commitment...'); + const { commitment } = await parseQRCode(qrData); + + console.log('⛓️ Registering identity on-chain...'); + const merkleRoot = 'mock-merkle-root'; + const success = await registerIdentity(commitment, merkleRoot); + + if (success) { + console.log(' Registration complete!'); + + recordOnboardingCompletion(wallet.publicKey!.toString()); + linkAadhaarToWallet(aadhaarHash, wallet.publicKey!.toString()); + + setCurrentStep('complete'); + } else { + throw new Error('Failed to register identity'); + } + } catch (err: any) { + console.error('Error during onboarding:', err); + setError(err.message || 'Failed to process QR code'); + setScanning(false); + } finally { + setProcessing(false); + } + }; + + const startCamera = async () => { + try { + setError(null); + const stream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: 'environment' } + }); + + if (videoRef.current) { + videoRef.current.srcObject = stream; + streamRef.current = stream; + await videoRef.current.play(); + scanFromCamera(); + } + } catch (err: any) { + setError('Camera access denied. Please allow camera access or upload an image.'); + console.error('Camera error:', err); + } + }; + + const scanFromCamera = () => { + if (!videoRef.current || !canvasRef.current) return; + + const canvas = canvasRef.current; + const video = videoRef.current; + const context = canvas.getContext('2d'); + + if (!context) return; + + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + const scan = () => { + if (video.readyState === video.HAVE_ENOUGH_DATA) { + context.drawImage(video, 0, 0, canvas.width, canvas.height); + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const code = jsQR(imageData.data, imageData.width, imageData.height); + + if (code && code.data) { + stopCamera(); + handleQRScan(code.data); + return; + } + } + + animationFrameRef.current = requestAnimationFrame(scan); + }; + + scan(); + }; + + const stopCamera = () => { + if (streamRef.current) { + streamRef.current.getTracks().forEach(track => track.stop()); + streamRef.current = null; + } + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = null; + } + }; + + const handleFileUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setError(null); + + try { + const reader = new FileReader(); + reader.onload = async (event) => { + const img = new Image(); + img.onload = async () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const context = canvas.getContext('2d'); + + if (!context) { + setError('Failed to process image'); + return; + } + + context.drawImage(img, 0, 0); + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const code = jsQR(imageData.data, imageData.width, imageData.height); + + if (code && code.data) { + await handleQRScan(code.data); + } else { + setError('No QR code found in image. Please try again with a clearer image.'); + } + }; + img.src = event.target?.result as string; + }; + reader.readAsDataURL(file); + } catch (err: any) { + setError('Failed to read file. Please try again.'); + } + }; + + useEffect(() => { + return () => { + stopCamera(); + }; + }, []); + + useEffect(() => { + if (scanMethod === 'camera') { + startCamera(); + } + return () => { + if (scanMethod === 'camera') { + stopCamera(); + } + }; + }, [scanMethod]); + + const handleComplete = () => { + navigate('/status'); + }; + + return ( +
+
+ {/* Progress Indicator */} +
+
+ {(['wallet', 'verify', 'complete'] as const).map((step, index) => ( +
+
+ {index + 1} +
+ {index < 2 && ( +
+ )} +
+ ))} +
+
+ Connect Wallet + Verify Identity + Complete +
+
+ + {/* Main Content Card (Styled like eth-delhi) */} +
+ {currentStep === 'wallet' && ( +
+
+
+ +
+

+ Welcome to Solstice +

+

+ Connect your Solana wallet to begin your identity verification journey +

+
+ +
+ {!wallet.connected ? ( + + ) : ( +
+ + Wallet Connected +
+ )} +
+
+ )} + + {currentStep === 'verify' && ( +
+
+
+ +
+

+ Verify Your Identity +

+

+ Scan your Aadhaar QR code to create your privacy-preserving identity +

+
+ + {error && ( +
+ +

{error}

+
+ )} + + {processing ? ( +
+ +

Processing your identity...

+

This may take a moment

+
+ ) : scanning ? ( +
+ {!scanMethod ? ( +
+

+ Choose Scan Method +

+ + + + + + + + +
+ ) : scanMethod === 'camera' ? ( +
+
+
+
+
+ +
+ + +
+
+ ) : ( +
+ +

Processing image...

+
+ )} +
+ ) : ( +
+ +
+ )} +
+ )} + + {currentStep === 'complete' && ( +
+
+
+ +
+

+ Identity Verified! +

+

+ Your privacy-preserving identity has been successfully created +

+
+ +
+
+ + Zero-knowledge proofs generated +
+
+ + Identity registered on Solana +
+
+ + Wallet permanently linked +
+
+ + +
+ )} +
+ + {/* Footer */} +
+

Your privacy is protected. All data is processed securely.

+
+
+
+ ); +} diff --git a/frontend/src/components/OnboardingFlow.tsx b/frontend/src/components/OnboardingFlow.tsx new file mode 100644 index 0000000..71f58af --- /dev/null +++ b/frontend/src/components/OnboardingFlow.tsx @@ -0,0 +1,444 @@ +import { useState, useRef, useEffect } from 'react'; +import { useWallet } from '@solana/wallet-adapter-react'; +import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; +import { useNavigate } from 'react-router-dom'; +import jsQR from 'jsqr'; +import { useSolstice } from '../contexts/SolsticeContext'; +import { generateAllProofs, storeProofs } from '../lib/proofGenerator'; +import { parseAadhaarQR } from '../lib/aadhaarParser'; +import { generateIdentityCommitment } from '../lib/crypto'; +import { + isOnboardingComplete, + recordOnboardingCompletion, + linkAadhaarToWallet +} from '../lib/onboarding'; + +type OnboardingStep = 'welcome' | 'wallet' | 'scan-method' | 'camera' | 'upload' | 'processing' | 'complete'; + +export function OnboardingFlow() { + const wallet = useWallet(); + const navigate = useNavigate(); + const { identity, fetchIdentity, registerIdentity } = useSolstice(); + const [currentStep, setCurrentStep] = useState('welcome'); + const [error, setError] = useState(null); + const videoRef = useRef(null); + const canvasRef = useRef(null); + const fileInputRef = useRef(null); + const streamRef = useRef(null); + + // Progress dots based on major steps + const getStepIndex = (): number => { + switch (currentStep) { + case 'welcome': return 0; + case 'wallet': return 1; + case 'scan-method': + case 'camera': + case 'upload': return 2; + case 'processing': return 3; + case 'complete': return 4; + default: return 0; + } + }; + + const stopCamera = () => { + if (streamRef.current) { + streamRef.current.getTracks().forEach(track => track.stop()); + streamRef.current = null; + } + }; + + const startCamera = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: 'environment' } + }); + if (videoRef.current) { + videoRef.current.srcObject = stream; + videoRef.current.play(); + streamRef.current = stream; + scanQRCode(); + } + } catch (err: any) { + setError('Unable to access camera. Please check permissions.'); + } + }; + + const scanQRCode = () => { + const video = videoRef.current; + const canvas = canvasRef.current; + + if (!video || !canvas) return; + + const context = canvas.getContext('2d'); + if (!context) return; + + const scan = () => { + if (video.readyState === video.HAVE_ENOUGH_DATA && currentStep === 'camera') { + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + context.drawImage(video, 0, 0, canvas.width, canvas.height); + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const code = jsQR(imageData.data, imageData.width, imageData.height); + + if (code && code.data) { + handleQRScan(code.data); + return; + } + } + requestAnimationFrame(scan); + }; + scan(); + }; + + const handleQRScan = async (qrData: string) => { + try { + stopCamera(); + setCurrentStep('processing'); + setError(null); + + if (!wallet.publicKey) { + setError('Please connect your wallet first'); + setCurrentStep('wallet'); + return; + } + + console.log('📱 Processing Aadhaar QR code...'); + + const parsedData = await parseAadhaarQR(qrData); + + if (!parsedData) { + throw new Error('Invalid Aadhaar QR code'); + } + + console.log('✅ Aadhaar data parsed'); + + const nationality = 'IN'; + const aadhaarHash = qrData.substring(0, 12); + const nonce = qrData.substring(qrData.length - 32); + + const proofInputs = { + dateOfBirth: parsedData.dateOfBirth.split('/').reverse().join(''), + nationality, + aadhaarNumber: aadhaarHash, + nonce + }; + + console.log('⚙️ Generating ZK proofs...'); + const proofResults = await generateAllProofs(proofInputs); + + if (proofResults.errors.length > 0) { + throw new Error(`Proof generation errors: ${proofResults.errors.join(', ')}`); + } + + const proofsToStore = { + age: proofResults.ageProof || undefined, + nationality: proofResults.nationalityProof || undefined, + uniqueness: proofResults.uniquenessProof || undefined + }; + + console.log('✅ All proofs generated'); + + await storeProofs(wallet.publicKey.toString(), proofsToStore); + console.log('💾 Proofs stored locally'); + + console.log('🔐 Generating commitment...'); + const commitment = await generateIdentityCommitment({ + name: parsedData.name, + dateOfBirth: parsedData.dateOfBirth, + gender: parsedData.gender, + address: parsedData.address + }); + + console.log('⛓️ Registering identity on-chain...'); + const merkleRoot = 'mock-merkle-root'; + const success = await registerIdentity(commitment, merkleRoot); + + if (success) { + console.log('✅ Registration complete!'); + + recordOnboardingCompletion(wallet.publicKey.toString()); + linkAadhaarToWallet(aadhaarHash, wallet.publicKey.toString()); + + setCurrentStep('complete'); + } else { + throw new Error('Failed to register identity'); + } + } catch (err: any) { + console.error('Error during onboarding:', err); + setError(err.message || 'Failed to process QR code'); + setCurrentStep('scan-method'); + } + }; + + const handleFileUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setCurrentStep('upload'); + setError(null); + + try { + const reader = new FileReader(); + reader.onload = async (event) => { + const img = new Image(); + img.onload = async () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const context = canvas.getContext('2d'); + + if (!context) { + setError('Failed to process image'); + setCurrentStep('scan-method'); + return; + } + + context.drawImage(img, 0, 0); + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const code = jsQR(imageData.data, imageData.width, imageData.height); + + if (code && code.data) { + await handleQRScan(code.data); + } else { + setError('No QR code found in image'); + setCurrentStep('scan-method'); + } + }; + img.src = event.target?.result as string; + }; + reader.readAsDataURL(file); + } catch (err: any) { + setError('Failed to read file'); + setCurrentStep('scan-method'); + } + }; + + useEffect(() => { + if (currentStep === 'camera') { + startCamera(); + } + return () => { + stopCamera(); + }; + }, [currentStep]); + + useEffect(() => { + if (wallet.connected && currentStep === 'wallet') { + setTimeout(() => setCurrentStep('scan-method'), 800); + } + }, [wallet.connected, currentStep]); + + // Check if user has already completed onboarding + useEffect(() => { + const checkOnboarding = async () => { + if (wallet.publicKey) { + const onboardedLocally = isOnboardingComplete(wallet.publicKey.toString()); + if (onboardedLocally) { + navigate('/status'); + } + await fetchIdentity(wallet.publicKey.toString()); + } + }; + + checkOnboarding(); + }, [wallet.publicKey, fetchIdentity, navigate]); + + const ProgressDots = () => ( +
+ {[0, 1, 2, 3, 4].map((index) => ( +
+ ))} +
+ ); + + return ( +
+
+ + + {/* Welcome Step */} + {currentStep === 'welcome' && ( +
+

+ Welcome to Solstice +

+

+ Privacy-preserving identity verification on Solana +

+ +
+ )} + + {/* Wallet Connection Step */} + {currentStep === 'wallet' && ( +
+

+ Connect your wallet +

+

+ Link your Solana wallet to your identity +

+
+ {!wallet.connected ? ( + + ) : ( +
+ + + Wallet connected + +
+ )} +
+
+ )} + + {/* Scan Method Selection */} + {currentStep === 'scan-method' && ( +
+

+ Scan your Aadhaar QR code +

+

+ Choose your preferred scanning method +

+ + {error && ( +
+ {error} +
+ )} + +
+ + + +
+
+ )} + + {/* Camera Scanning */} + {currentStep === 'camera' && ( +
+
+

+ Scanning +

+

Position QR code in frame

+
+ +
+
+ + +
+ )} + + {/* Processing */} + {(currentStep === 'processing' || currentStep === 'upload') && ( +
+
+

+ Processing your identity +

+

+ Creating zero-knowledge proofs... +

+
+ )} + + {/* Complete */} + {currentStep === 'complete' && ( +
+
+ ✓ +
+

+ Identity verified +

+

+ Your privacy-preserving identity is ready +

+ +
+
+ + Zero-knowledge proofs generated +
+
+ + Identity registered on Solana +
+
+ + Wallet permanently linked +
+
+ + +
+ )} +
+
+ ); +} diff --git a/frontend/src/components/OnboardingFlow.tsx.backup b/frontend/src/components/OnboardingFlow.tsx.backup new file mode 100644 index 0000000..67b42a5 --- /dev/null +++ b/frontend/src/components/OnboardingFlow.tsx.backup @@ -0,0 +1,527 @@ +import { useState, useEffect, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useWallet } from '@solana/wallet-adapter-react'; +import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; +import { useSolstice } from '../contexts/SolsticeContext'; +import { CheckCircle, Loader, Shield, QrCode, ChevronRight, AlertCircle, Camera, Upload, X } from 'lucide-react'; +import jsQR from 'jsqr'; +import { generateAllProofs, storeProofs } from '../lib/proofGenerator'; +import { parseAadhaarQR } from '../lib/aadhaarParser'; +import { + isOnboardingComplete, + recordOnboardingCompletion, + linkAadhaarToWallet, + verifyAadhaarWalletLink +} from '../lib/onboarding'; + +type OnboardingStep = 'wallet' | 'verify' | 'complete'; + +export function OnboardingFlow() { + const navigate = useNavigate(); + const wallet = useWallet(); + const { identity, fetchIdentity, registerIdentity, parseQRCode } = useSolstice(); + const [currentStep, setCurrentStep] = useState('wallet'); + const [scanning, setScanning] = useState(false); + const [processing, setProcessing] = useState(false); + const [error, setError] = useState(null); + const [aadhaarData, setAadhaarData] = useState(null); + const [scanMethod, setScanMethod] = useState<'camera' | 'upload' | null>(null); + const videoRef = useRef(null); + const canvasRef = useRef(null); + const fileInputRef = useRef(null); + const streamRef = useRef(null); + const animationFrameRef = useRef(null); + + // Check if user has already completed onboarding + useEffect(() => { + const checkOnboarding = async () => { + if (wallet.publicKey) { + const onboardedLocally = isOnboardingComplete(wallet.publicKey.toString()); + await fetchIdentity(wallet.publicKey.toString()); + } + }; + + checkOnboarding(); + }, [wallet.publicKey, fetchIdentity]); + + // Auto-advance steps + useEffect(() => { + if (wallet.connected && wallet.publicKey && currentStep === 'wallet') { + const onboarded = isOnboardingComplete(wallet.publicKey.toString()); + if (onboarded && identity) { + navigate('/status'); + } else { + setCurrentStep('verify'); + } + } + }, [wallet.connected, wallet.publicKey, currentStep, identity, navigate]); + + const handleQRScan = async (qrData: string) => { + if (processing) return; + + setProcessing(true); + setError(null); + setScanMethod(null); + setScanning(false); + + try { + console.log('📱 Processing Aadhaar QR code...'); + + const parsedData = await parseAadhaarQR(qrData); + + if (!parsedData) { + throw new Error('Invalid Aadhaar QR code'); + } + + setAadhaarData(parsedData); + console.log(' Aadhaar data parsed'); + + const nationality = 'IN'; + const aadhaarHash = qrData.substring(0, 12); + const nonce = qrData.substring(qrData.length - 32); + + const proofInputs = { + dateOfBirth: parsedData.dateOfBirth.split('/').reverse().join(''), + nationality, + aadhaarNumber: aadhaarHash, + nonce + }; + + console.log('⚙️ Generating ZK proofs...'); + const proofResults = await generateAllProofs(proofInputs); + + if (proofResults.errors.length > 0) { + throw new Error(`Proof generation errors: ${proofResults.errors.join(', ')}`); + } + + const proofsToStore = { + age: proofResults.ageProof || undefined, + nationality: proofResults.nationalityProof || undefined, + uniqueness: proofResults.uniquenessProof || undefined + }; + + console.log(' All proofs generated'); + + await storeProofs(wallet.publicKey!.toString(), proofsToStore); + console.log('💾 Proofs stored locally'); + + console.log(' Generating commitment...'); + const { commitment } = await parseQRCode(qrData); + + console.log('⛓️ Registering identity on-chain...'); + const merkleRoot = 'mock-merkle-root'; + const success = await registerIdentity(commitment, merkleRoot); + + if (success) { + console.log(' Registration complete!'); + + recordOnboardingCompletion(wallet.publicKey!.toString()); + linkAadhaarToWallet(aadhaarHash, wallet.publicKey!.toString()); + + setCurrentStep('complete'); + } else { + throw new Error('Failed to register identity'); + } + } catch (err: any) { + console.error('Error during onboarding:', err); + setError(err.message || 'Failed to process QR code'); + setScanning(false); + } finally { + setProcessing(false); + } + }; + + const startCamera = async () => { + try { + setError(null); + const stream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: 'environment' } + }); + + if (videoRef.current) { + videoRef.current.srcObject = stream; + streamRef.current = stream; + await videoRef.current.play(); + scanFromCamera(); + } + } catch (err: any) { + setError('Camera access denied. Please allow camera access or upload an image.'); + console.error('Camera error:', err); + } + }; + + const scanFromCamera = () => { + if (!videoRef.current || !canvasRef.current) return; + + const canvas = canvasRef.current; + const video = videoRef.current; + const context = canvas.getContext('2d'); + + if (!context) return; + + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + const scan = () => { + if (video.readyState === video.HAVE_ENOUGH_DATA) { + context.drawImage(video, 0, 0, canvas.width, canvas.height); + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const code = jsQR(imageData.data, imageData.width, imageData.height); + + if (code && code.data) { + stopCamera(); + handleQRScan(code.data); + return; + } + } + + animationFrameRef.current = requestAnimationFrame(scan); + }; + + scan(); + }; + + const stopCamera = () => { + if (streamRef.current) { + streamRef.current.getTracks().forEach(track => track.stop()); + streamRef.current = null; + } + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = null; + } + }; + + const handleFileUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setError(null); + + try { + const reader = new FileReader(); + reader.onload = async (event) => { + const img = new Image(); + img.onload = async () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const context = canvas.getContext('2d'); + + if (!context) { + setError('Failed to process image'); + return; + } + + context.drawImage(img, 0, 0); + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const code = jsQR(imageData.data, imageData.width, imageData.height); + + if (code && code.data) { + await handleQRScan(code.data); + } else { + setError('No QR code found in image. Please try again with a clearer image.'); + } + }; + img.src = event.target?.result as string; + }; + reader.readAsDataURL(file); + } catch (err: any) { + setError('Failed to read file. Please try again.'); + } + }; + + useEffect(() => { + return () => { + stopCamera(); + }; + }, []); + + useEffect(() => { + if (scanMethod === 'camera') { + startCamera(); + } + return () => { + if (scanMethod === 'camera') { + stopCamera(); + } + }; + }, [scanMethod]); + + const handleComplete = () => { + navigate('/status'); + }; + + return ( +
+
+ {/* Progress Indicator */} +
+
+ {(['wallet', 'verify', 'complete'] as const).map((step, index) => ( +
+
+ {index + 1} +
+ {index < 2 && ( +
+ )} +
+ ))} +
+
+ Connect Wallet + Verify Identity + Complete +
+
+ + {/* Main Content Card (Styled like eth-delhi) */} +
+ {currentStep === 'wallet' && ( +
+
+
+ +
+

+ Welcome to Solstice +

+

+ Connect your Solana wallet to begin your identity verification journey +

+
+ +
+ {!wallet.connected ? ( + + ) : ( +
+ + Wallet Connected +
+ )} +
+
+ )} + + {currentStep === 'verify' && ( +
+
+
+ +
+

+ Verify Your Identity +

+

+ Scan your Aadhaar QR code to create your privacy-preserving identity +

+
+ + {error && ( +
+ +

{error}

+
+ )} + + {processing ? ( +
+ +

Processing your identity...

+

This may take a moment

+
+ ) : scanning ? ( +
+ {!scanMethod ? ( +
+

+ Choose Scan Method +

+ + + + + + + + +
+ ) : scanMethod === 'camera' ? ( +
+
+
+
+
+ +
+ + +
+
+ ) : ( +
+ +

Processing image...

+
+ )} +
+ ) : ( +
+ +
+ )} +
+ )} + + {currentStep === 'complete' && ( +
+
+
+ +
+

+ Identity Verified! +

+

+ Your privacy-preserving identity has been successfully created +

+
+ +
+
+ + Zero-knowledge proofs generated +
+
+ + Identity registered on Solana +
+
+ + Wallet permanently linked +
+
+ + +
+ )} +
+ + {/* Footer */} +
+

Your privacy is protected. All data is processed securely.

+
+
+
+ ); +} diff --git a/frontend/src/components/ProofsDashboard.tsx b/frontend/src/components/ProofsDashboard.tsx index edfaa4a..636bf9c 100644 --- a/frontend/src/components/ProofsDashboard.tsx +++ b/frontend/src/components/ProofsDashboard.tsx @@ -21,11 +21,11 @@ export function ProofsDashboard() { const loadProofs = async () => { try { setLoading(true); - + // Load proofs from IndexedDB const storedProofs = await getStoredProofs(); setProofs(storedProofs); - + } catch (error) { console.error('Error loading proofs:', error); } finally { @@ -36,29 +36,29 @@ export function ProofsDashboard() { const getStoredProofs = async (): Promise => { return new Promise((resolve) => { const request = indexedDB.open('SolsticeProofs', 1); - + request.onupgradeneeded = (event: any) => { const db = event.target.result; if (!db.objectStoreNames.contains('proofs')) { db.createObjectStore('proofs', { keyPath: 'type' }); } }; - + request.onsuccess = (event: any) => { const db = event.target.result; const transaction = db.transaction(['proofs'], 'readonly'); const store = transaction.objectStore('proofs'); const getAllRequest = store.getAll(); - + getAllRequest.onsuccess = () => { resolve(getAllRequest.result || []); }; - + getAllRequest.onerror = () => { resolve([]); }; }; - + request.onerror = () => { resolve([]); }; @@ -128,7 +128,7 @@ export function ProofsDashboard() { if (loading) { return (
- +
); } @@ -136,9 +136,9 @@ export function ProofsDashboard() { if (proofs.length === 0) { return (
- -

No Proofs Generated Yet

-

+ +

No Proofs Generated Yet

+

Upload your Aadhaar QR code in the "Scan QR Code" tab to automatically generate your identity proofs.

@@ -148,8 +148,8 @@ export function ProofsDashboard() { return (
-

My Identity Proofs

-

+

My Identity Proofs

+

Your zero-knowledge proofs allow you to verify attributes without revealing personal data. Share these proofs with services that need identity verification.

@@ -159,14 +159,14 @@ export function ProofsDashboard() { {proofs.map((proof) => (
{/* Header */}
{getProofIcon(proof.type)}
-

+

{getProofTitle(proof.type)}

@@ -187,12 +187,12 @@ export function ProofsDashboard() {
{/* Description */} -

+

{getProofDescription(proof.type)}

{/* Metadata */} -
+
Generated: {new Date(proof.generatedAt).toLocaleDateString()}
@@ -207,14 +207,14 @@ export function ProofsDashboard() {
{/* Scanning indicator */} -
+
Scanning...
{/* Instructions */} -
-

+

+

📸 Position the QR code within the frame

-

+

Scanning automatically (~60 times per second)

{cameraError && ( -
+

{cameraError}

@@ -460,45 +460,46 @@ export function QRScanner() { )} {step === 'registered' && ( -
+
-

Identity Registered!

+

Identity Registered!

Your identity commitment has been registered on Solana.

- + {generatingProofs && ( -
- -

Generating ZK Proofs...

-

+

+ +

Generating ZK Proofs...

+

This may take a few seconds. All computation happens in your browser for privacy.

)} - + {!generatingProofs && ( -
+

- ZK proofs generated and stored locally
+ ZK proofs generated and stored locally
You can now verify attributes on any dApp instantly!

)} - + - -

+ +

Your identity is now registered. One person, one identity.

)} +
); } diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..85fddf3 --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -0,0 +1,148 @@ +import { useState, useEffect } from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { X, QrCode, Shield, CheckCircle, User, Zap } from 'lucide-react'; + +interface SidebarProps { + isOpen: boolean; + onClose: () => void; +} + +export function Sidebar({ isOpen, onClose }: SidebarProps) { + const location = useLocation(); + const [isMobile, setIsMobile] = useState(false); + + // Check if current screen is mobile + useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth < 1024); + }; + + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + + // Close sidebar when route changes on mobile + useEffect(() => { + if (isMobile) { + onClose(); + } + }, [location.pathname, isMobile, onClose]); + + const navItems = [ + { id: 'status', label: 'Identity Status', path: '/status', icon: User }, + { id: 'challenge', label: 'Scan Challenge', path: '/challenge', icon: Zap }, + { id: 'proofs', label: 'My Proofs', path: '/proofs', icon: CheckCircle }, + ]; + + const isActive = (path: string) => { + // Exact match or sub-path match if we had nested routes + return location.pathname === path; + }; + + return ( + <> + {/* Mobile Overlay */} + {isMobile && isOpen && ( +