-
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..1f5cd73
--- /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, 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 && (
+
+ )}
+
+ {/* Sidebar */}
+
+ >
+ );
+}
+
+export function MobileMenuButton({ onClick }: { onClick: () => void }) {
+ return (
+
+ );
+}
diff --git a/frontend/src/components/ui/text-animate.tsx b/frontend/src/components/ui/text-animate.tsx
new file mode 100644
index 0000000..5ceeacb
--- /dev/null
+++ b/frontend/src/components/ui/text-animate.tsx
@@ -0,0 +1,52 @@
+import { useEffect, useState } from 'react';
+
+interface TextAnimateProps {
+ text?: string;
+ children?: string;
+ animation?: 'slideUp' | 'fadeIn';
+ by?: 'word' | 'char';
+ className?: string;
+ style?: React.CSSProperties;
+}
+
+export function TextAnimate({
+ text,
+ children,
+ animation = 'fadeIn',
+ by = 'word',
+ className = '',
+ style
+}: TextAnimateProps) {
+ const [isVisible, setIsVisible] = useState(false);
+
+ const content = text || children || '';
+
+ useEffect(() => {
+ setIsVisible(false);
+ const timer = setTimeout(() => setIsVisible(true), 50);
+ return () => clearTimeout(timer);
+ }, [content]);
+
+ const elements = by === 'word' ? content.split(' ') : content.split('');
+
+ return (
+
+ {elements.map((element, index) => (
+
+ {element}
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index a461c50..e4e7f16 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1 +1,117 @@
-@import "tailwindcss";
\ No newline at end of file
+@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap');
+
+@font-face {
+ font-family: 'Advercase';
+ src: url('/fonts/Advercase.otf') format('opentype');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@import "tailwindcss";
+
+@theme {
+ /* --- Typography --- */
+ --font-serif: "Playfair Display", serif;
+ --font-sans: "Playfair Display", serif;
+ /* Force Playfair as default */
+
+ /* --- User Provided Palette --- */
+
+ /* Shadow Grey */
+ --color-shadow-grey-50: #f0f0f5;
+ --color-shadow-grey-100: #e0e0eb;
+ --color-shadow-grey-200: #c2c2d6;
+ --color-shadow-grey-300: #a3a3c2;
+ --color-shadow-grey-400: #8585ad;
+ --color-shadow-grey-500: #666699;
+ --color-shadow-grey-600: #52527a;
+ --color-shadow-grey-700: #3d3d5c;
+ --color-shadow-grey-800: #29293d;
+ --color-shadow-grey-900: #14141f;
+ --color-shadow-grey-950: #0e0e15;
+
+ /* Vintage Grape */
+ --color-vintage-grape-50: #f1f0f5;
+ --color-vintage-grape-100: #e4e0eb;
+ --color-vintage-grape-200: #c9c1d7;
+ --color-vintage-grape-300: #aea2c3;
+ --color-vintage-grape-400: #9384ae;
+ --color-vintage-grape-500: #77659a;
+ --color-vintage-grape-600: #60517b;
+ --color-vintage-grape-700: #483c5d;
+ --color-vintage-grape-800: #30283e;
+ --color-vintage-grape-900: #18141f;
+ --color-vintage-grape-950: #110e16;
+
+ /* Stone Brown */
+ --color-stone-brown-50: #f4f3f1;
+ --color-stone-brown-100: #e8e6e3;
+ --color-stone-brown-200: #d1cdc7;
+ --color-stone-brown-300: #bab4ab;
+ --color-stone-brown-400: #a39b8f;
+ --color-stone-brown-500: #8c8273;
+ --color-stone-brown-600: #70685c;
+ --color-stone-brown-700: #544e45;
+ --color-stone-brown-800: #38342e;
+ --color-stone-brown-900: #1c1a17;
+ --color-stone-brown-950: #141210;
+
+ /* Grey Olive */
+ --color-grey-olive-50: #f4f3f1;
+ --color-grey-olive-100: #e9e7e2;
+ --color-grey-olive-200: #d3cec5;
+ --color-grey-olive-300: #bcb6a9;
+ --color-grey-olive-400: #a69e8c;
+ --color-grey-olive-500: #90866f;
+ --color-grey-olive-600: #736b59;
+ --color-grey-olive-700: #565043;
+ --color-grey-olive-800: #3a352c;
+ --color-grey-olive-900: #1d1b16;
+ --color-grey-olive-950: #141310;
+
+ /* Khaki Beige */
+ --color-khaki-beige-50: #f4f3f0;
+ --color-khaki-beige-100: #eae8e1;
+ --color-khaki-beige-200: #d5d1c3;
+ --color-khaki-beige-300: #c0baa5;
+ --color-khaki-beige-400: #aaa388;
+ --color-khaki-beige-500: #958c6a;
+ --color-khaki-beige-600: #777055;
+ --color-khaki-beige-700: #5a543f;
+ --color-khaki-beige-800: #3c382a;
+ --color-khaki-beige-900: #1e1c15;
+ --color-khaki-beige-950: #15140f;
+
+ /* --- Semantic Mappings --- */
+
+ /* Backgrounds */
+ --color-primary: var(--color-shadow-grey-950);
+ --color-secondary: var(--color-shadow-grey-900);
+ --color-tertiary: var(--color-shadow-grey-800);
+
+ /* Text */
+ --color-text-primary: var(--color-stone-brown-50);
+ --color-text-secondary: var(--color-stone-brown-300);
+ --color-text-muted: var(--color-stone-brown-500);
+
+ /* Borders & Accents */
+ --color-border-custom: var(--color-shadow-grey-800);
+
+ /* Feature Accents */
+ --color-accent-grape: var(--color-vintage-grape-400);
+ --color-accent-olive: var(--color-grey-olive-400);
+}
+
+/* Global Reset */
+body {
+ font-family: var(--font-serif);
+ background-color: var(--color-primary);
+ color: var(--color-text-primary);
+ -webkit-font-smoothing: antialiased;
+}
+
+/* Selection */
+::selection {
+ background: var(--color-vintage-grape-700);
+ color: var(--color-stone-brown-50);
+}
\ No newline at end of file
diff --git a/frontend/src/lib/anchor.ts b/frontend/src/lib/anchor.ts
index fd46743..25f5783 100644
--- a/frontend/src/lib/anchor.ts
+++ b/frontend/src/lib/anchor.ts
@@ -158,9 +158,9 @@ export async function registerIdentity(
}
console.log('Updating existing identity with new data...');
- // Anchor generates methods dynamically - cast to any to bypass TypeScript
+ // Anchor converts snake_case to camelCase
const tx = await (program.methods as any)
- .update_identity(commitmentBytes, merkleRootBytes)
+ .updateIdentity(commitmentBytes, merkleRootBytes)
.accounts({
identity: identityPda,
user: userPublicKey,
diff --git a/frontend/src/lib/crypto.ts b/frontend/src/lib/crypto.ts
new file mode 100644
index 0000000..13b7b26
--- /dev/null
+++ b/frontend/src/lib/crypto.ts
@@ -0,0 +1,24 @@
+/**
+ * Generate identity commitment from Aadhaar data
+ * This creates a cryptographic commitment without revealing personal information
+ */
+export async function generateIdentityCommitment(aadhaarData: {
+ name: string;
+ dateOfBirth: string;
+ gender: string;
+ address: string;
+}): Promise
{
+ // Combine sensitive data fields (same format as backend)
+ const dataString = `${aadhaarData.name}|${aadhaarData.dateOfBirth}|${aadhaarData.gender}|${aadhaarData.address}`;
+
+ // Create SHA-256 hash as commitment
+ const encoder = new TextEncoder();
+ const data = encoder.encode(dataString);
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
+
+ // Convert to hex string
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const commitment = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
+
+ return commitment;
+}
diff --git a/frontend/src/lib/onboarding.ts b/frontend/src/lib/onboarding.ts
new file mode 100644
index 0000000..a7f182c
--- /dev/null
+++ b/frontend/src/lib/onboarding.ts
@@ -0,0 +1,108 @@
+/**
+ * Onboarding utilities for Solstice Protocol
+ *
+ * Manages the one-time onboarding process that links
+ * an Aadhaar identity to a Solana wallet.
+ */
+
+/**
+ * Check if a wallet has completed onboarding
+ */
+export function isOnboardingComplete(walletAddress: string | null): boolean {
+ if (!walletAddress) return false;
+
+ const onboardingKey = `solstice_onboarded_${walletAddress}`;
+ return localStorage.getItem(onboardingKey) === 'true';
+}
+
+/**
+ * Mark onboarding as complete for a wallet
+ */
+export function setOnboardingComplete(walletAddress: string): void {
+ const onboardingKey = `solstice_onboarded_${walletAddress}`;
+ localStorage.setItem(onboardingKey, 'true');
+}
+
+/**
+ * Clear onboarding status (for testing/reset)
+ */
+export function clearOnboardingStatus(walletAddress: string): void {
+ const onboardingKey = `solstice_onboarded_${walletAddress}`;
+ localStorage.removeItem(onboardingKey);
+}
+
+/**
+ * Link an Aadhaar number to a wallet address
+ */
+export function linkAadhaarToWallet(aadhaarNumber: string, walletAddress: string): void {
+ const linkKey = `solstice_linked_${aadhaarNumber}`;
+ localStorage.setItem(linkKey, walletAddress);
+}
+
+/**
+ * Get wallet address linked to an Aadhaar number
+ */
+export function getLinkedWallet(aadhaarNumber: string): string | null {
+ const linkKey = `solstice_linked_${aadhaarNumber}`;
+ return localStorage.getItem(linkKey);
+}
+
+/**
+ * Check if an Aadhaar is already linked to a wallet
+ */
+export function isAadhaarLinked(aadhaarNumber: string): boolean {
+ return getLinkedWallet(aadhaarNumber) !== null;
+}
+
+/**
+ * Check if the current wallet matches the linked Aadhaar
+ */
+export function verifyAadhaarWalletLink(
+ aadhaarNumber: string,
+ currentWallet: string
+): { valid: boolean; linkedWallet: string | null; message: string } {
+ const linkedWallet = getLinkedWallet(aadhaarNumber);
+
+ if (!linkedWallet) {
+ return {
+ valid: true,
+ linkedWallet: null,
+ message: 'No existing link found. You can proceed with linking.'
+ };
+ }
+
+ if (linkedWallet === currentWallet) {
+ return {
+ valid: true,
+ linkedWallet,
+ message: 'This Aadhaar is already linked to your current wallet.'
+ };
+ }
+
+ return {
+ valid: false,
+ linkedWallet,
+ message: `This Aadhaar is already linked to another wallet (${linkedWallet.slice(0, 8)}...)`
+ };
+}
+
+/**
+ * Get all onboarding related data for a wallet
+ */
+export function getOnboardingData(walletAddress: string) {
+ return {
+ isComplete: isOnboardingComplete(walletAddress),
+ completedAt: localStorage.getItem(`solstice_onboarded_at_${walletAddress}`),
+ };
+}
+
+/**
+ * Store onboarding completion timestamp
+ */
+export function recordOnboardingCompletion(walletAddress: string): void {
+ setOnboardingComplete(walletAddress);
+ localStorage.setItem(
+ `solstice_onboarded_at_${walletAddress}`,
+ new Date().toISOString()
+ );
+}
diff --git a/frontend/src/pages/ChallengeScannerPage.tsx b/frontend/src/pages/ChallengeScannerPage.tsx
new file mode 100644
index 0000000..ae65b97
--- /dev/null
+++ b/frontend/src/pages/ChallengeScannerPage.tsx
@@ -0,0 +1,5 @@
+import { ChallengeScanner } from '../components/ChallengeScanner';
+
+export function ChallengeScannerPage() {
+ return ;
+}
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
deleted file mode 100644
index 240573b..0000000
--- a/frontend/src/pages/Home.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-import { useState, useEffect, useRef } from 'react';
-import { useNavigate } from 'react-router-dom';
-import {
- Shield, Zap, Fingerprint, Check, ArrowRight, Sparkles,
- Terminal, Database, Cpu, Network, Key, Activity,
- Layers, Code, Globe, Users, Rocket, Lock, Eye,
- ChevronDown
-} from 'lucide-react';
-import '../fonts.css';
-
-function Home() {
- const navigate = useNavigate();
- const [isVisible, setIsVisible] = useState(false);
- const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
- const [currentSection, setCurrentSection] = useState(0);
- const [stats, setStats] = useState({
- proofs: 0,
- users: 0,
- savings: 0
- });
- const [hasAnimated, setHasAnimated] = useState(false);
- const metricsRef = useRef(null);
-
- // Track mouse for parallax effect
- useEffect(() => {
- const handleMouseMove = (e: MouseEvent) => {
- setMousePosition({
- x: (e.clientX / window.innerWidth - 0.5) * 20,
- y: (e.clientY / window.innerHeight - 0.5) * 20
- });
- };
- window.addEventListener('mousemove', handleMouseMove);
- return () => window.removeEventListener('mousemove', handleMouseMove);
- }, []);
- const [isScrolling, setIsScrolling] = useState(false);
-
- const sections = ['hero', 'problem', 'solution', 'technology', 'usecases', 'metrics'];
-
- // Number animation
- const animateNumber = (start: number, end: number, duration: number, key: string) => {
- const startTime = Date.now();
- const tick = () => {
- const elapsed = Date.now() - startTime;
- const progress = Math.min(elapsed / duration, 1);
- const current = Math.floor(start + (end - start) * progress);
- setStats(prev => ({ ...prev, [key]: current }));
- if (progress < 1) {
- requestAnimationFrame(tick);
- }
- };
- tick();
- };
-
- // Metrics animation on scroll
- useEffect(() => {
- const observer = new IntersectionObserver(
- (entries) => {
- entries.forEach((entry) => {
- if (entry.isIntersecting && !hasAnimated) {
- setHasAnimated(true);
- animateNumber(0, 1400000000, 2500, 'proofs');
- animateNumber(0, 100000, 2000, 'users');
- animateNumber(0, 5000, 1800, 'savings');
- }
- });
- },
- { threshold: 0.5 }
- );
-
- if (metricsRef.current) {
- observer.observe(metricsRef.current);
- }
-
- return () => {
- if (metricsRef.current) {
- observer.unobserve(metricsRef.current);
- }
- };
- }, [hasAnimated]);
-
- useEffect(() => {
- setIsVisible(true);
- }, []);
-
- const scrollToSection = (sectionId: string) => {
- const element = document.getElementById(sectionId);
- if (element) {
- element.scrollIntoView({ behavior: 'smooth', block: 'start' });
- }
- };
-
- return (
-
- {/* Background Effects */}
-
-
- {/* Header */}
-
-
- SOLSTICE
-
-
-
-
- {/* Main Content */}
-
- {/* Hero */}
-
-
- Zero-Knowledge
-
- Identity Proofs
-
-
- on Solana
-
-
-
-
- Prove your identity without revealing your data. Age, nationality, uniqueness — all verified through cryptography, not trust.
-
-
- {/* CTA Buttons */}
-
-
-
-
-
-
- {/* Features */}
-
-
-
-
-
-
Privacy First
-
- Your personal data stays private. Only cryptographic proofs are shared.
-
-
-
-
-
-
-
-
Lightning Fast
-
- Generate proofs in seconds, verify in milliseconds on Solana.
-
-
-
-
-
-
-
-
Secure by Design
-
- Built on Groth16 SNARKs and government-verified credentials.
-
-
-
-
-
- {/* Footer */}
-
-
-
-
- );
-}
-
-export default Home;
\ No newline at end of file
diff --git a/frontend/src/pages/IdentityStatusPage.tsx b/frontend/src/pages/IdentityStatusPage.tsx
new file mode 100644
index 0000000..3779c93
--- /dev/null
+++ b/frontend/src/pages/IdentityStatusPage.tsx
@@ -0,0 +1,20 @@
+import { useSolstice } from '../contexts/SolsticeContext';
+import { IdentityStatus } from '../components/IdentityStatus';
+
+export function IdentityStatusPage() {
+ const { identity, loading } = useSolstice();
+
+ return (
+
+
+
Identity Status
+
+ View your registered identity details and verification status.
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/ProofsPage.tsx b/frontend/src/pages/ProofsPage.tsx
new file mode 100644
index 0000000..7ea8164
--- /dev/null
+++ b/frontend/src/pages/ProofsPage.tsx
@@ -0,0 +1,5 @@
+import { ProofsDashboard } from '../components/ProofsDashboard';
+
+export function ProofsPage() {
+ return ;
+}
diff --git a/frontend/src/pages/QRScannerPage.tsx b/frontend/src/pages/QRScannerPage.tsx
new file mode 100644
index 0000000..26dd8cb
--- /dev/null
+++ b/frontend/src/pages/QRScannerPage.tsx
@@ -0,0 +1,5 @@
+import { QRScanner } from '../components/QRScanner';
+
+export function QRScannerPage() {
+ return ;
+}