diff --git a/package.json b/package.json index 0e8482a..4a861c3 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,7 @@ "prepare": "husky" }, "dependencies": { - "@aztec/bb.js": "3.0.0-nightly.20251104", "@creit.tech/stellar-wallets-kit": "^1.5.0", - "@noir-lang/noir_js": "1.0.0-beta.19", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-separator": "^1.1.8", diff --git a/public/zk/noir_not_expired.json b/public/zk/noir_not_expired.json deleted file mode 100644 index 193b111..0000000 --- a/public/zk/noir_not_expired.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "noir_version": "1.0.0-beta.15+83245db91dcf63420ef4bcbbd85b98f397fee663", - "hash": "13520019959916939672", - "abi": { - "parameters": [ - { - "name": "expiry_ts", - "type": { "kind": "integer", "sign": "unsigned", "width": 64 }, - "visibility": "private" - }, - { - "name": "now_ts", - "type": { "kind": "integer", "sign": "unsigned", "width": 64 }, - "visibility": "private" - } - ], - "return_type": null, - "error_types": {} - }, - "bytecode": "H4sIAAAAAAAA/7VTOwrCQBDNZINIYmVplVI7xQsERLASSxsJYhbZwqghCpa5geIFPIXocdJZ2tgbcTYsa/7gg/B2Z+a9mVkIKF9oyKs5c0nENbxD9BHkDywpBhjjelX5BY+ZyF1n7IW9S+c2GV6DYDpr9x+jw31zGoSv81OqzQFAfi1kJU2lmFbci58JryACW5KDJjX61/Ipvkf0BbXEDESp9qhQcldxpjyoec2lXAvZYR5d+GxPbeb6dEk9e7tb+4y6fh0rOOuCQ4EXiAfjeqOaPv7VROjC2ZAbImsJOki5qxJn1UKGbyMhxz2byOK8fI83MBC6BmcEAAA=", - "debug_symbols": "dZDdCsMgDIXfJddedD8do68yRrE2LUJQSXUwSt99UebWXezmxOT4JcYVRhzS3Fs3+QW62woDWyI79+SNjtY7qa6bgpr2kRGlBDtfqKAZXYTOJSIFD02pXFqCdiVGzeI2CtCNEqXhZAnzaVNfuvmPtpW9XD9wK/RdMm0s/7wXjtAdFJyKnou2oltuz1YPhO+tpuTMbsn4DNWp3xDYGxwTYx5QPBn5Ag==", - "file_map": { - "50": { - "source": "fn main(expiry_ts: u64, now_ts: u64) {\n assert(expiry_ts > now_ts);\n}\n", - "path": "/mnt/c/Users/Josué Brenes/Downloads/Argentina/hackathon/zks/noir-not-expired/src/main.nr" - } - }, - "expression_width": { "Bounded": { "width": 4 } } -} diff --git a/public/zk/noir_valid_status.json b/public/zk/noir_valid_status.json deleted file mode 100644 index 57987bb..0000000 --- a/public/zk/noir_valid_status.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "noir_version": "1.0.0-beta.15+83245db91dcf63420ef4bcbbd85b98f397fee663", - "hash": "3434633426592842425", - "abi": { - "parameters": [{ "name": "valid", "type": { "kind": "field" }, "visibility": "private" }], - "return_type": null, - "error_types": {} - }, - "bytecode": "H4sIAAAAAAAA/43NIQqAMBgF4P8HD2LUpngEEUxitBgEi0ERm3FHEC/gKUSPs2a0rI+xDQbbYF958HjwEKRI5TxMi0gEm+5iCILmNhubjeZX+rTVTUjXJ8VX7+96lJSdP7j/fDgTEmjotAAAAA==", - "debug_symbols": "dY/BCoMwDIbfJeceHJsXX2UMqTVKIKQltoMhvvui6OYOO6XJ3+9P/hl67MrYkgxxguY+Q6fETGPLMfhMUWw6Lw6Ots2KaCM46UYlrygZGinMDp6ey/ZpSl62mr2aWjlA6a2a4UCM62txX7r6j17rnb3VH7g2+mGdD6Q/90IFzWVZzZR8x7hnGIqEU6T8SodyhE4aA/ZFcbXbNFvwBg==", - "file_map": { - "50": { - "source": "fn main(valid: Field) {\n assert(valid == 1);\n}\n", - "path": "/mnt/c/Users/Josué Brenes/Downloads/Argentina/hackathon/zks/noir-valid-status/src/main.nr" - } - }, - "expression_width": { "Bounded": { "width": 4 } } -} diff --git a/public/zk/noir_workshop.json b/public/zk/noir_workshop.json deleted file mode 100644 index cf1945f..0000000 --- a/public/zk/noir_workshop.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "noir_version": "1.0.0-beta.15+83245db91dcf63420ef4bcbbd85b98f397fee663", - "hash": "5222927446623847482", - "abi": { - "parameters": [ - { - "name": "age", - "type": { "kind": "integer", "sign": "unsigned", "width": 8 }, - "visibility": "private" - } - ], - "return_type": null, - "error_types": {} - }, - "bytecode": "H4sIAAAAAAAA/51SsQrCMBBNUpXSOjk6ddRN8RNEcBJHFylig2SwaqmCYz9B/AG/QvRzurnp4lxbzEGMNTY9KK93fe/uXRqM3lHhuJwxn6RY5TlOH4NjFqbAJeg7gOdw7HijIO6e2pfx4BxFk2mrdxvur+tDP34eHxJXHbiRV3VQIe2HV/BIIIEFs4IpSQ1pkM5CKu49STDW6EtQuYPS/SGip7+TNDhNjh4L6DxkO+oyP6QLGrib7Spk1A/h5AEtoYNR3BUBvV1Oj2o5NUt4t+WBHCs5OvwjJxKquFjRt57zDXrCNRD9wh4vOJ5XTfcDAAA=", - "debug_symbols": "dZDBDoMwCIbfhXMPc26H+SrLYmpF04TQBtsli/Hdh41u7rALFH4+KMzQY5fH1vMQJmjuM3TiifzYUnA2+cCanRcDe9gmQdQUHHSlohXkBA1nIgNPS7kUTdFy8cmKqicDyL16bTh4wvW1mC99+o/W1cbWtw98VfqhkXVefv4LFTRafy62Lvaidlnbi7cd4bbVkNkdlkyvuCv7GaIEh30WXAcUTUe+AQ==", - "file_map": { - "50": { - "source": "fn main(age: u8) {\r\n assert(age > 18);\r\n}\r\n", - "path": "/mnt/c/Users/Josué Brenes/Downloads/Argentina/hackathon/zks/noir-acta/src/main.nr" - } - }, - "expression_width": { "Bounded": { "width": 4 } } -} diff --git a/src/@types/credentials.ts b/src/@types/credentials.ts index 197260c..00246e3 100644 --- a/src/@types/credentials.ts +++ b/src/@types/credentials.ts @@ -36,45 +36,8 @@ export type CredentialVerifyProps = { status?: string | null; since?: string | null; revealed?: Record | null; - zkValid?: boolean | null; - zkStatement?: ZkStatement | null; - hasVerified?: boolean; }; -export type ZkTypeEqStatement = { - kind: 'typeEq'; - selectedKeys: string[]; - isValid: boolean; - typeHash: string; - expectedHash: string; - valid: string; -}; - -export type ZkIsAdultStatement = { - kind: 'isAdult'; - selectedKeys: string[]; - isAdult: boolean; -}; - -export type ZkNotExpiredStatement = { - kind: 'notExpired'; - selectedKeys: string[]; - notExpired: boolean; -}; - -export type ZkIsValidStatement = { - kind: 'isValid'; - selectedKeys: string[]; - isValid: boolean; -}; - -export type ZkStatement = - | 'none' - | ZkTypeEqStatement - | ZkIsAdultStatement - | ZkNotExpiredStatement - | ZkIsValidStatement; - export type CredentialCardProps = { name: string; category: string; diff --git a/src/@types/guided-tour.ts b/src/@types/guided-tour.ts new file mode 100644 index 0000000..db059eb --- /dev/null +++ b/src/@types/guided-tour.ts @@ -0,0 +1,6 @@ +export interface TourStep { + icon: React.ReactNode; + title: string; + description: string; + content?: React.ReactNode; +} diff --git a/src/components/modules/credentials/hooks/useCredentialVerify.ts b/src/components/modules/credentials/hooks/useCredentialVerify.ts index 9d914d2..0d5f041 100644 --- a/src/components/modules/credentials/hooks/useCredentialVerify.ts +++ b/src/components/modules/credentials/hooks/useCredentialVerify.ts @@ -1,10 +1,8 @@ 'use client'; import { useEffect, useState } from 'react'; -import type { ZkStatement } from '@/@types/credentials'; import { useNetwork } from '@/providers/network.provider'; import { useWalletContext } from '@/providers/wallet.provider'; -import { verifyZkProof } from '@/lib/zk'; import { useActaApiKey } from '@/components/modules/vault/hooks/use-acta-api-key'; import { actaFetchJson } from '@/lib/actaApi'; @@ -20,14 +18,10 @@ export function useCredentialVerify(vcId: string) { const { apiKey } = useActaApiKey(); const [verify, setVerify] = useState(null); const [revealed, setRevealed] = useState | null>(null); - const [zkValid, setZkValid] = useState(null); - const [zkStatement, setZkStatement] = useState(null); - const [hasVerified, setHasVerified] = useState(false); - const [reverifyLoading, setReverifyLoading] = useState(false); const [shareParam, setShareParam] = useState(null); - const [hasZkProofInShare, setHasZkProofInShare] = useState(false); const [shareType, setShareType] = useState(null); const [shareLoading, setShareLoading] = useState(true); + useEffect(() => { const read = async () => { if (typeof window === 'undefined') { @@ -92,9 +86,6 @@ export function useCredentialVerify(vcId: string) { if (shareParam && typeof shareParam === 'object') { const sp = shareParam as { revealedFields?: Record; - statement?: unknown; - proof?: string; - ok?: boolean; type?: unknown; }; setRevealed(sp.revealedFields || null); @@ -106,24 +97,8 @@ export function useCredentialVerify(vcId: string) { } else { setShareType(null); } - const st = sp.statement; - const hasSt = - typeof st === 'object' && - st && - 'kind' in st && - (st as { kind?: string }).kind !== 'none'; - const hasProof = typeof sp.proof === 'string' && sp.proof.length > 0; - const hasZk = Boolean(hasSt && hasProof); - setHasZkProofInShare(hasZk); - if (hasZk) { - setZkStatement(st as ZkStatement); - } else { - setZkStatement(null); - } - // No auto-verification: status must be shown only after user clicks } - // API-based verification requires API key + owner wallet address. if (vcId && walletAddress && apiKey.trim()) { try { const v = await actaFetchJson<{ status: string; since?: string }>({ @@ -148,44 +123,9 @@ export function useCredentialVerify(vcId: string) { run(); }, [vcId, network, walletAddress, shareParam, apiKey]); - const reverify = async () => { - if (!shareParam || typeof shareParam !== 'object') return; - const sp = shareParam as { - statement?: { - kind?: string; - typeHash?: string; - expectedHash?: string; - valid?: string; - }; - publicSignals?: string[]; - proof?: string; - ok?: boolean; - }; - try { - setReverifyLoading(true); - if (typeof sp.ok === 'boolean') { - setZkValid(sp.ok === true); - setHasVerified(true); - return; - } - const ok = await verifyZkProof(sp as unknown as typeof sp); - setZkValid(ok); - setHasVerified(true); - } catch { - } finally { - setReverifyLoading(false); - } - }; - return { verify, revealed, - zkValid, - zkStatement, - reverify, - reverifyLoading, - hasVerified, - hasZkProofInShare, shareType, shareLoading, }; diff --git a/src/components/modules/credentials/hooks/useShareCredential.ts b/src/components/modules/credentials/hooks/useShareCredential.ts index cdfe8b1..c9094d8 100644 --- a/src/components/modules/credentials/hooks/useShareCredential.ts +++ b/src/components/modules/credentials/hooks/useShareCredential.ts @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useMemo, useState } from 'react'; -import type { Credential, ZkStatement } from '@/@types/credentials'; +import type { Credential } from '@/@types/credentials'; export function useShareCredential(credential: Credential | null) { const fields = useMemo(() => { @@ -60,23 +60,6 @@ export function useShareCredential(credential: Credential | null) { const [selected, setSelected] = useState>({}); const [copied, setCopied] = useState(false); - const [predicate, setPredicate] = useState<{ - kind: 'none' | 'isAdult' | 'notExpired' | 'isValid'; - }>({ kind: 'none' }); - const [proof, setProof] = useState<{ - statement: ZkStatement; - publicSignals: string[]; - proof: string | null; - ok?: boolean; - } | null>(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - if (predicate.kind === 'none') { - setProof(null); - } - }, [predicate.kind]); const onSelectAll = () => { if (!credential) return; @@ -114,17 +97,6 @@ export function useShareCredential(credential: Credential | null) { const payload: Record = { revealedFields }; if (credential?.id) payload.vc_id = credential.id; if (credential?.type) payload.type = credential.type; - if ( - proof && - proof.statement !== ('none' as ZkStatement) && - typeof proof.proof === 'string' && - proof.proof - ) { - payload.statement = proof.statement as unknown; - payload.publicSignals = proof.publicSignals as unknown; - payload.proof = proof.proof as unknown; - if (typeof proof.ok === 'boolean') payload.ok = proof.ok as unknown; - } const json = JSON.stringify(payload); @@ -157,19 +129,7 @@ export function useShareCredential(credential: Credential | null) { setShareParam(''); } })(); - }, [revealedFields, credential, proof]); - - const isExpired = useMemo(() => { - try { - const exp = credential?.expirationDate || null; - if (!exp) return false; - const t = typeof exp === 'string' ? Date.parse(exp) : Number(exp); - if (!Number.isFinite(t)) return false; - return Date.now() >= t; - } catch { - return false; - } - }, [credential]); + }, [revealedFields, credential]); const onToggle = (key: string) => { setSelected((prev) => ({ ...prev, [key]: !prev[key] })); @@ -185,74 +145,6 @@ export function useShareCredential(credential: Credential | null) { } catch {} }; - async function onGenerateProof() { - setLoading(true); - setError(null); - setProof(null); - try { - const kind = predicate.kind; - if (kind === 'none') { - setProof(null); - } else { - if (!credential) { - setProof({ statement: 'none', publicSignals: [], proof: null }); - return; - } - if (kind === 'isAdult' && !('birthDate' in credential)) { - throw new Error('birth_date_missing'); - } - if (kind === 'notExpired') { - const exp = credential.expirationDate || null; - const t = typeof exp === 'string' ? Date.parse(exp || '') : Number(exp); - if (!exp) { - throw new Error('expiration_date_missing'); - } - if (!Number.isFinite(t)) { - throw new Error('expiration_date_invalid'); - } - if (Date.now() >= t) { - setError('Credential is expired. Cannot generate proof.'); - return; - } - } - const { generateZkProof } = await import('@/lib/zk'); - const res = await generateZkProof({ - credential: credential as unknown as Record, - revealFields: selected, - predicate, - }); - setProof({ - statement: res.statement as ZkStatement, - publicSignals: res.publicSignals as string[], - proof: res.proof, - ok: (res as unknown as { ok?: boolean }).ok === true, - }); - } - } catch (e: unknown) { - const msg = - typeof e === 'object' && e && 'message' in e - ? String((e as { message?: unknown }).message || '') - : ''; - const m = msg.toLowerCase(); - let display = 'Proof could not be generated.'; - if (predicate.kind === 'notExpired') { - display = - m.includes('satisfy') || m.includes('constraint') - ? 'Credential is expired. Cannot generate proof.' - : 'Proof error on expiration test.'; - } else if (predicate.kind === 'isValid') { - display = 'Credential status is invalid. Cannot generate proof.'; - } else if (predicate.kind === 'isAdult') { - display = m.includes('missing') - ? 'Birth date required to generate age proof.' - : 'Age below threshold. Cannot generate proof.'; - } - setError(display); - } finally { - setLoading(false); - } - } - return { fields, selected, @@ -263,11 +155,5 @@ export function useShareCredential(credential: Credential | null) { onUnselectAll, onToggle, onCopy, - predicate, - setPredicate, - loading, - error, - onGenerateProof, - isExpired, }; } diff --git a/src/components/modules/credentials/ui/CredentialVerify.tsx b/src/components/modules/credentials/ui/CredentialVerify.tsx index 5a2414b..e82ff3a 100644 --- a/src/components/modules/credentials/ui/CredentialVerify.tsx +++ b/src/components/modules/credentials/ui/CredentialVerify.tsx @@ -5,18 +5,7 @@ import { useCredentialVerify } from '@/components/modules/credentials/hooks/useC import ImpactaCertificate from '@/components/modules/credentials/ui/impacta-bootcamp/Certificate'; export function CredentialVerify({ vcId }: { vcId: string }) { - const { - verify, - revealed, - zkValid, - zkStatement, - reverify, - reverifyLoading, - hasVerified, - hasZkProofInShare, - shareType, - shareLoading, - } = useCredentialVerify(vcId); + const { verify, revealed, shareType, shareLoading } = useCredentialVerify(vcId); const isImpacta = typeof shareType === 'string' && shareType.includes('ImpactaCertificateCredential'); @@ -89,19 +78,7 @@ export function CredentialVerify({ vcId }: { vcId: string }) { status={verify?.status} since={verify?.since ?? null} revealed={revealed || null} - zkValid={zkValid ?? null} - zkStatement={zkStatement || null} - hasVerified={hasVerified} /> - {hasZkProofInShare && ( - - )} ); } diff --git a/src/components/modules/credentials/ui/CredentialVerifyCard.tsx b/src/components/modules/credentials/ui/CredentialVerifyCard.tsx index 0127b36..d7b4b85 100644 --- a/src/components/modules/credentials/ui/CredentialVerifyCard.tsx +++ b/src/components/modules/credentials/ui/CredentialVerifyCard.tsx @@ -5,28 +5,8 @@ import Image from 'next/image'; import { useVerifyCard } from '@/components/modules/credentials/hooks/useVerifyCard'; import type { CredentialVerifyProps } from '@/@types/credentials'; -export function CredentialVerifyCard({ - vcId, - status, - since, - revealed, - zkValid, - zkStatement, - hasVerified, -}: CredentialVerifyProps) { +export function CredentialVerifyCard({ vcId, status, since, revealed }: CredentialVerifyProps) { const { displayStatus, formatRevealed, copy } = useVerifyCard(status); - const kind = - zkStatement && typeof zkStatement === 'object' - ? (zkStatement as { kind?: string }).kind - : undefined; - const testName = - kind === 'isAdult' - ? 'Age ≥ 18' - : kind === 'notExpired' - ? 'Not expired' - : kind === 'isValid' - ? 'Status is valid' - : undefined; const StatusIcon = displayStatus === 'Revoked' ? XCircle @@ -84,41 +64,6 @@ export function CredentialVerifyCard({
{vcId || '-'}
- {hasVerified && ( - <> -
- ZK Proof - - {zkValid == null ? 'Not provided' : zkValid ? 'Passed' : 'Failed'} - -
- {testName && ( -
- Test - - {testName} - -
- )} -
- Verification uses the provided zero-knowledge proof; no private data is revealed. -
- - )} {revealed && ( diff --git a/src/components/modules/credentials/ui/ShareCredentialModal.tsx b/src/components/modules/credentials/ui/ShareCredentialModal.tsx index dfe8dc6..2c1219d 100644 --- a/src/components/modules/credentials/ui/ShareCredentialModal.tsx +++ b/src/components/modules/credentials/ui/ShareCredentialModal.tsx @@ -16,24 +16,8 @@ export default function ShareCredentialModal({ credential: Credential | null; onClose: () => void; }) { - const { - fields, - selected, - copied, - shareParam, - onSelectAll, - onUnselectAll, - onToggle, - onCopy, - predicate, - setPredicate, - loading, - error, - onGenerateProof, - isExpired, - } = useShareCredential(credential); - const hasDob = !!credential?.birthDate; - const hasExp = !!credential?.expirationDate; + const { fields, selected, copied, shareParam, onSelectAll, onUnselectAll, onToggle, onCopy } = + useShareCredential(credential); const [qrDataUrl, setQrDataUrl] = useState(''); const isImpactaCertificate = !!credential?.type.includes('ImpactaCertificateCredential'); @@ -211,7 +195,7 @@ export default function ShareCredentialModal({
{fields - .filter((f) => (hasExp ? true : f.key !== 'expirationDate')) + .filter((f) => (credential?.expirationDate ? true : f.key !== 'expirationDate')) .map((f) => (
- -
-
-
- - - -
-

Zero-Knowledge Proof

-
- -

- Verify your credential without revealing private data -

- -
-
- - -
- - - - {error && ( -
- {error} -
- )} - {predicate.kind === 'notExpired' && isExpired && ( -
- This credential is expired -
- )} - {predicate.kind === 'isAdult' && !hasDob && ( -
- Birth date required to enable age proof -
- )} -
-
diff --git a/src/components/modules/dashboard/ui/GuidedTour.tsx b/src/components/modules/dashboard/ui/GuidedTour.tsx new file mode 100644 index 0000000..e101252 --- /dev/null +++ b/src/components/modules/dashboard/ui/GuidedTour.tsx @@ -0,0 +1,321 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { + X, + ChevronLeft, + ChevronRight, + Layout, + Lock, + ShieldCheck, + FilePlus, + Share2, + KeyRound, + Book, +} from 'lucide-react'; +import Image from 'next/image'; +import Link from 'next/link'; +import type { TourStep } from '@/@types/guided-tour'; + +function StepCard({ + label, + detail, + href, + onClick, +}: { + label: string; + detail: string; + href?: string; + onClick?: () => void; +}) { + const inner = ( +
+

{label}

+

{detail}

+
+ ); + + if (href) + return ( + + {inner} + + ); + if (onClick) + return ( + + ); + return inner; +} + +function StepBullet({ bold, text }: { bold: string; text: string }) { + return ( +

+ {bold}: {text} +

+ ); +} + +function StepAction({ label, href }: { label: string; href: string }) { + return ( +
+ + + {label} + +
+ ); +} + +function IconWrapper({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function buildSteps(onClose: () => void): TourStep[] { + return [ + { + icon: ACTA, + title: 'Welcome to ACTA', + description: + 'ACTA is a decentralized credential management platform built on Stellar. This guide will walk you through how to use each section of the app.', + content: ( +
+ + +
+ ), + }, + { + icon: , + title: 'Navigating the App', + description: + 'Use the sidebar on the left to switch between pages. The header at the top lets you toggle between Testnet and Mainnet.', + content: ( +
+ + + +
+ ), + }, + { + icon: , + title: 'Your Vault', + description: + 'Your vault is your on-chain credential storage. Create it once, then all credentials issued to you are stored here.', + content: ( +
+ + + + +
+ ), + }, + { + icon: , + title: 'Authorize Issuers', + description: + 'Before anyone can issue credentials to your vault, you need to authorize their wallet address.', + content: ( +
+ + + +
+ ), + }, + { + icon: , + title: 'Issue Credentials', + description: + 'Create and issue credentials using built-in or custom templates. Fill in the details and issue to any authorized vault.', + content: ( +
+ + +
+ ), + }, + { + icon: , + title: 'Share & Verify', + description: + 'Share credentials with anyone via a unique link. Recipients can verify authenticity on-chain.', + content: ( +
+ + + +
+ ), + }, + { + icon: , + title: 'API Keys', + description: + 'Request an API key to access ACTA services programmatically. You can generate one from the API Keys page.', + content: ( +
+ +
+ ), + }, + { + icon: , + title: 'Tutorials & Resources', + description: + 'Watch step-by-step video tutorials to learn how each feature works. Track your progress as you complete them.', + content: ( +
+ +
+ ), + }, + ]; +} + +export default function GuidedTour({ open, onClose }: { open: boolean; onClose: () => void }) { + const [current, setCurrent] = useState(0); + const steps = buildSteps(onClose); + const total = steps.length; + const step = steps[current]; + + if (!open) return null; + + const progress = ((current + 1) / total) * 100; + + const handleClose = () => { + setCurrent(0); + onClose(); + }; + + return ( +
+
+ +
+
+
+
+ +
+
+
+ {steps.map((_, i) => ( +
+ + +
+ +
+ {step.icon} +

{step.title}

+

+ {step.description} +

+
+ + {step.content &&
{step.content}
} + +
+ + + + {current + 1} / {total} + + + {current < total - 1 ? ( + + ) : ( + + )} +
+
+
+
+ ); +} diff --git a/src/layouts/client/Client.tsx b/src/layouts/client/Client.tsx index 76944e5..5c16ecd 100644 --- a/src/layouts/client/Client.tsx +++ b/src/layouts/client/Client.tsx @@ -3,9 +3,10 @@ import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar'; import { AppSidebar } from '@/layouts/sidebar/Sidebar'; import { HeaderHome } from '@/layouts/header/Header'; import { SettingsOverlayHost } from '@/components/modules/settings/ui/SettingsOverlayHost'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { usePathname } from 'next/navigation'; import TutorialModal from '@/components/modules/tutorial/ui/TutorialModal'; +import GuidedTour from '@/components/modules/dashboard/ui/GuidedTour'; import { useWalletContext } from '@/providers/wallet.provider'; import { useNetwork } from '@/providers/network.provider'; import MobileBottomNav from '@/components/ui/mobile-bottom-nav'; @@ -18,33 +19,59 @@ export default function DashboardLayoutClient({ useNetwork(); const pathname = usePathname(); const isMobile = useIsMobile(); - const [tutorialClosed, setTutorialClosed] = useState(false); const [tutorialOpen, setTutorialOpen] = useState(false); + const [guidedTourOpen, setGuidedTourOpen] = useState(false); + const isFirstVisit = useRef(false); useEffect(() => { try { - const key = 'tutorial_shown_global'; - const seen = localStorage.getItem(key) === 'true'; - setTimeout(() => setTutorialOpen(!seen), 0); + const tourSeen = localStorage.getItem('guided_tour_shown') === 'true'; + const tutorialSeen = localStorage.getItem('tutorial_shown_global') === 'true'; + + if (!tourSeen) { + isFirstVisit.current = true; + setTimeout(() => setGuidedTourOpen(true), 0); + } else if (!tutorialSeen) { + setTimeout(() => setTutorialOpen(true), 0); + } } catch {} }, []); + useEffect(() => { + const handler = () => setGuidedTourOpen(true); + window.addEventListener('open-guided-tour', handler as EventListener); + return () => window.removeEventListener('open-guided-tour', handler as EventListener); + }, []); + return ( {!isMobile && } { try { - const key = 'tutorial_shown_global'; - localStorage.setItem(key, 'true'); + localStorage.setItem('tutorial_shown_global', 'true'); } catch {} - setTutorialClosed(true); setTutorialOpen(false); }} /> + { + setGuidedTourOpen(false); + try { + localStorage.setItem('guided_tour_shown', 'true'); + } catch {} + if (isFirstVisit.current) { + isFirstVisit.current = false; + try { + localStorage.setItem('tutorial_shown_global', 'true'); + } catch {} + } + }} + />
{pathname?.startsWith('/dashboard') && !(isMobile && pathname?.startsWith('/dashboard/tutorials')) && } diff --git a/src/layouts/sidebar/Sidebar.tsx b/src/layouts/sidebar/Sidebar.tsx index 830b893..6a4d6d8 100644 --- a/src/layouts/sidebar/Sidebar.tsx +++ b/src/layouts/sidebar/Sidebar.tsx @@ -1,7 +1,7 @@ import { useRouter } from 'next/navigation'; import Image from 'next/image'; import { Sidebar, SidebarBody, SidebarLink } from '@/components/ui/aceternity-sidebar'; -import { Home, User, Book, ShieldCheck, FilePlus, Lock, KeyRound } from 'lucide-react'; +import { Home, User, Book, ShieldCheck, FilePlus, Lock, KeyRound, Compass } from 'lucide-react'; export function AppSidebar() { const router = useRouter(); @@ -62,6 +62,16 @@ export function AppSidebar() {
+ , + onClick: () => { + window.dispatchEvent(new CustomEvent('open-guided-tour')); + }, + }} + /> >> 0; - for (let i = 0; i < enc.length; i++) { - h ^= enc[i]; - h = Math.imul(h, 16777619); - h >>>= 0; - } - return String(h); -} - -function isValidStatus(status: unknown): boolean { - const v = String(status || '').toLowerCase(); - return v === 'valid'; -} - -function notExpired(expiration: unknown): boolean { - if (!expiration) return true; - const t = typeof expiration === 'string' ? Date.parse(expiration) : Number(expiration); - if (!Number.isFinite(t)) return true; - return Date.now() < t; -} - -function bytesToBase64(bytes: Uint8Array): string { - let binary = ''; - const len = bytes.length; - for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]); - return btoa(binary); -} - -function base64ToBytes(b64: string): Uint8Array { - let s = String(b64 || ''); - s = s.replace(/\s+/g, ''); - s = s.replace(/-/g, '+').replace(/_/g, '/'); - const pad = s.length % 4; - if (pad) s = s + '='.repeat(4 - pad); - const bin = atob(s); - const out = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); - return out; -} - -async function loadNoir(acirUrl: string) { - const { Noir } = await import('@noir-lang/noir_js'); - const { UltraHonkBackend } = await import('@aztec/bb.js'); - const resp = await fetch(acirUrl); - if (!resp.ok) throw new Error('acir_not_found'); - const acir = await resp.json(); - const noir = new Noir(acir); - const backend = new UltraHonkBackend(acir.bytecode); - return { noir, backend }; -} - -export async function generateZkProof({ - credential, - revealFields, - predicate, -}: { - credential: Record; - revealFields: Record; - predicate: Predicate; -}) { - const kind = predicate.kind; - const selectedKeys = Object.keys(revealFields || {}).filter((k) => !!revealFields[k]); - if (kind === 'none') return { statement: 'none', publicSignals: [], proof: null }; - - if (kind === 'typeEq') { - const typeStr = String(credential.type || ''); - const expected = String(predicate.value || typeStr); - const typeHash = strToField(typeStr); - const expectedHash = strToField(expected); - const validFlag = - isValidStatus(credential.status) && notExpired(credential.expirationDate) ? '1' : '0'; - const { noir, backend } = await loadNoir('/zk/noir_valid_vc.json'); - const execRes = await noir.execute({ - type_hash: typeHash, - expected_hash: expectedHash, - valid: validFlag, - }); - const proofData = await backend.generateProof(execRes.witness); - const verified = await backend.verifyProof(proofData); - // Do not use returnValue; rely only on the verified result - const proof = JSON.stringify({ - publicInputs: proofData.publicInputs, - proof: bytesToBase64(proofData.proof), - }); - return { - statement: { kind, selectedKeys, typeHash, expectedHash, valid: validFlag }, - publicSignals: proofData.publicInputs, - proof, - ok: verified, - }; - } - - if (kind === 'isAdult') { - const dob = String((credential as Record)['birthDate'] || ''); - if (!dob) throw new Error('birth_date_missing'); - const t = Date.parse(dob); - if (!Number.isFinite(t)) throw new Error('birth_date_invalid'); - const now = Date.now(); - const ageYears = Math.max(0, Math.floor((now - t) / (365.25 * 24 * 60 * 60 * 1000))); - const { noir, backend } = await loadNoir('/zk/noir_workshop.json'); - const execRes = await noir.execute({ age: ageYears }); - const proofData = await backend.generateProof(execRes.witness); - const verified = await backend.verifyProof(proofData); - const proof = JSON.stringify({ - publicInputs: proofData.publicInputs, - proof: bytesToBase64(proofData.proof), - }); - return { - statement: { kind, selectedKeys, age: ageYears }, - publicSignals: proofData.publicInputs, - proof, - ok: verified, - }; - } - - if (kind === 'notExpired') { - const exp = (credential as Record)['expirationDate']; - if (!exp) throw new Error('expiration_date_missing'); - const t = typeof exp === 'string' ? Date.parse(exp) : Number(exp); - if (!Number.isFinite(t)) throw new Error('expiration_date_invalid'); - const now = Date.now(); - const { noir, backend } = await loadNoir('/zk/noir_not_expired.json'); - const execRes = await noir.execute({ expiry_ts: Math.floor(t), now_ts: Math.floor(now) }); - const proofData = await backend.generateProof(execRes.witness); - const verified = await backend.verifyProof(proofData); - const proof = JSON.stringify({ - publicInputs: proofData.publicInputs, - proof: bytesToBase64(proofData.proof), - }); - return { - statement: { kind, selectedKeys, expiry_ts: Math.floor(t), now_ts: Math.floor(now) }, - publicSignals: proofData.publicInputs, - proof, - ok: verified, - }; - } - - if (kind === 'isValid') { - const status = (credential as Record)['status']; - const flag = String(isValidStatus(status) ? '1' : '0'); - const { noir, backend } = await loadNoir('/zk/noir_valid_status.json'); - const execRes = await noir.execute({ valid: flag }); - const proofData = await backend.generateProof(execRes.witness); - const verified = await backend.verifyProof(proofData); - const proof = JSON.stringify({ - publicInputs: proofData.publicInputs, - proof: bytesToBase64(proofData.proof), - }); - return { - statement: { kind, selectedKeys, valid: flag }, - publicSignals: proofData.publicInputs, - proof, - ok: verified, - }; - } - - return { statement: 'none', publicSignals: [], proof: null }; -} - -export async function verifyZkProof( - payload: { - statement?: { kind?: string; typeHash?: string; expectedHash?: string; valid?: string }; - publicSignals?: string[]; - proof?: string; - ok?: boolean; - } | null -): Promise { - if (!payload || !payload.statement || !payload.proof) return false; - if (typeof payload.ok === 'boolean' && payload.ok === true) return true; - const st = payload.statement; - let backend, p; - if (st.kind === 'typeEq') { - ({ backend } = await loadNoir('/zk/noir_valid_vc.json')); - } else if (st.kind === 'isAdult') { - ({ backend } = await loadNoir('/zk/noir_workshop.json')); - } else if (st.kind === 'notExpired') { - ({ backend } = await loadNoir('/zk/noir_not_expired.json')); - } else if (st.kind === 'isValid') { - ({ backend } = await loadNoir('/zk/noir_valid_status.json')); - } else { - return false; - } - p = JSON.parse(String(payload.proof || '{}')) as { - publicInputs?: string[]; - proof?: string; - }; - if (!p || !p.proof || !p.publicInputs) return false; - const proofData: { publicInputs: string[]; proof: Uint8Array } = { - publicInputs: p.publicInputs, - proof: base64ToBytes(p.proof), - }; - const ok = await backend.verifyProof(proofData); - return !!ok; -}