add ZKPassport age verification (livestream code)

This commit is contained in:
Václav Pavlín 2025-09-09 09:36:15 +02:00
parent c91164dbde
commit fefe7608ad
No known key found for this signature in database
GPG Key ID: B378FB31BB6D89A5
4 changed files with 1350 additions and 918 deletions

2121
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -50,8 +50,11 @@
"@reown/appkit-adapter-wagmi": "^1.7.17",
"@reown/appkit-wallet-button": "^1.7.17",
"@tanstack/react-query": "^5.84.1",
"@types/qrcode.react": "^1.0.5",
"@waku/sdk": "^0.0.35-67a7287.0",
"buffer": "^6.0.3",
"@zkpassport/sdk": "^0.8.3",
"bitcoinjs-message": "^2.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@ -62,6 +65,7 @@
"next-themes": "^0.3.0",
"ordiscan": "^1.3.0",
"re-resizable": "6.11.2",
"qrcode.react": "^4.2.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",

83
src/lib/zkPassport.ts Normal file
View File

@ -0,0 +1,83 @@
import { EU_COUNTRIES, ZKPassport } from '@zkpassport/sdk';
export const verifyAge = async (setProgress: (status:string) => void, setUrl: (url:string) => void): Promise<boolean> => {
const zkPassport = new ZKPassport();
const queryBuilder = await zkPassport.request({
name: "OpChan",
logo: "https://zkpassport.id/logo.png",
purpose: "Prove you are 18+ years old",
scope: "adult",
});
const {
url,
onResult,
onGeneratingProof,
onError,
onProofGenerated,
onReject,
onRequestReceived
} = queryBuilder.gte("age", 18).done();
setUrl(url);
return new Promise((resolve, reject) => {
try {
console.log("Starting age verification with zkPassport");
onRequestReceived(() => {
setProgress("Request received, preparing for age verification");
console.log("Request received, preparing for age verification");
});
onGeneratingProof(() => {
setProgress("Generating cryptographic proof of age");
console.log("Generating cryptographic proof of age");
});
onProofGenerated(() => {
setProgress("Age proof generated successfully");
console.log("Age proof generated successfully");
});
onReject(() => {
setProgress("Age verification request was rejected");
console.log("Age verification request was rejected by the user");
resolve(false);
});
onError((error) => {
setProgress(`Age verification error: ${error}`);
console.error("Age verification error", error);
resolve(false);
});
onResult(({ verified, uniqueIdentifier, result }) => {
console.log("Age verification callback", verified, uniqueIdentifier, result);
try {
console.log("Age verification result", verified, result);
if (verified) {
const isOver18 = result.age?.gte?.result;
setProgress("Age verification completed successfully");
resolve(isOver18 || false);
console.log("User is 18+ years old", isOver18);
} else {
setProgress("Age verification failed");
resolve(false);
}
} catch (error) {
console.error("Age verification result processing error", error);
setProgress(`Age verification result processing error: ${error}`);
resolve(false);
} finally {
setUrl('');
setProgress('');
}
});
} catch (error) {
console.error("Age verification exception", error);
setProgress(`Age verification exception: ${error}`);
reject(error);
}
});
}

View File

@ -7,6 +7,8 @@ import { DelegationFullStatus } from '@/lib/delegation';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { verifyAge } from '@/lib/zkPassport';
import { QRCodeCanvas } from 'qrcode.react';
import {
Select,
SelectContent,
@ -63,6 +65,9 @@ export default function ProfilePage() {
EDisplayPreference.WALLET_ADDRESS
);
const [walletWizardOpen, setWalletWizardOpen] = useState(false);
const [url, setUrl] = useState<string>('');
const [progress, setProgress] = useState<string>('');
const [isVerifying, setIsVerifying] = useState<boolean>(false);
// Initialize and update local state when user data changes
useEffect(() => {
@ -587,6 +592,61 @@ export default function ProfilePage() {
</div>
</main>
{/* Age Verification Section */}
<div className="max-w-md mx-auto mt-8 p-6 bg-cyber-muted/20 border border-cyber-muted/30 rounded-lg">
<h2 className="text-xl font-bold text-white mb-4">Age Verification</h2>
<p className="text-cyber-neutral mb-4">
Verify your age to access restricted content.
</p>
<Button
onClick={async () => {
setIsVerifying(true);
try {
const result = await verifyAge(setProgress, setUrl);
console.log('Age verification result:', result);
} catch (error) {
console.error('Age verification failed:', error);
} finally {
setIsVerifying(false);
}
}}
disabled={isVerifying}
className="w-full bg-cyber-accent hover:bg-cyber-accent/80 text-black font-mono"
>
{isVerifying ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Verifying...
</>
) : (
'Verify Age'
)}
</Button>
{progress && (
<p className="mt-4 text-sm text-cyber-neutral">{progress}</p>
)}
{url && (
<div className="mt-4 space-y-4">
<div className="text-center">
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-cyber-accent hover:underline font-medium"
>
Open verification in new tab
</a>
</div>
<div className="flex justify-center p-4 bg-white rounded-lg shadow-lg inline-block">
<QRCodeCanvas value={url} size={200} level="H" includeMargin={true} />
</div>
<p className="text-xs text-cyber-neutral text-center">
Scan this QR code to open the verification page on your mobile device
</p>
</div>
)}
</div>
<footer className="page-footer">
<p>OpChan - A decentralized forum built on Waku & Bitcoin Ordinals</p>
</footer>