mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 20:37:56 +00:00
feat: Add citizenship NFT workflow and wallet enhancements
- Add BeCitizen page with citizenship application workflow - Add NftList component to display Tiki role NFTs - Implement citizenship crypto and workflow utilities - Add wallet score tracking and calculation - Update WalletDashboard to display NFTs and scores - Update AccountBalance and AppLayout for better UX - Enhance PolkadotContext with Tiki pallet integration - Clean up temporary debugging scripts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Wallet, TrendingUp, ArrowUpRight, ArrowDownRight, RefreshCw, Award, Plus, Coins, Send } from 'lucide-react';
|
||||
import { Wallet, TrendingUp, ArrowUpRight, ArrowDownRight, RefreshCw, Award, Plus, Coins, Send, Shield, Users } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ASSET_IDS, getAssetSymbol } from '@/lib/wallet';
|
||||
import { AddTokenModal } from './AddTokenModal';
|
||||
import { TransferModal } from './TransferModal';
|
||||
import { getAllScores, type UserScores } from '@/lib/scores';
|
||||
|
||||
interface TokenBalance {
|
||||
assetId: number;
|
||||
@@ -31,7 +32,14 @@ export const AccountBalance: React.FC = () => {
|
||||
const [usdtBalance, setUsdtBalance] = useState<string>('0');
|
||||
const [hezUsdPrice, setHezUsdPrice] = useState<number>(0);
|
||||
const [pezUsdPrice, setPezUsdPrice] = useState<number>(0);
|
||||
const [trustScore, setTrustScore] = useState<string>('-');
|
||||
const [scores, setScores] = useState<UserScores>({
|
||||
trustScore: 0,
|
||||
referralScore: 0,
|
||||
stakingScore: 0,
|
||||
tikiScore: 0,
|
||||
totalScore: 0
|
||||
});
|
||||
const [loadingScores, setLoadingScores] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [otherTokens, setOtherTokens] = useState<TokenBalance[]>([]);
|
||||
const [isAddTokenModalOpen, setIsAddTokenModalOpen] = useState(false);
|
||||
@@ -316,23 +324,38 @@ export const AccountBalance: React.FC = () => {
|
||||
fetchBalance();
|
||||
fetchTokenPrices(); // Fetch token USD prices from pools
|
||||
|
||||
// Fetch Trust Score
|
||||
const fetchTrustScore = async () => {
|
||||
// Fetch All Scores from blockchain
|
||||
const fetchAllScores = async () => {
|
||||
if (!api || !isApiReady || !selectedAccount?.address) {
|
||||
setTrustScore('-');
|
||||
setScores({
|
||||
trustScore: 0,
|
||||
referralScore: 0,
|
||||
stakingScore: 0,
|
||||
tikiScore: 0,
|
||||
totalScore: 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingScores(true);
|
||||
try {
|
||||
const score = await api.query.trust.trustScores(selectedAccount.address);
|
||||
setTrustScore(score.toString());
|
||||
const userScores = await getAllScores(api, selectedAccount.address);
|
||||
setScores(userScores);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch trust score:', err);
|
||||
setTrustScore('-');
|
||||
console.error('Failed to fetch scores:', err);
|
||||
setScores({
|
||||
trustScore: 0,
|
||||
referralScore: 0,
|
||||
stakingScore: 0,
|
||||
tikiScore: 0,
|
||||
totalScore: 0
|
||||
});
|
||||
} finally {
|
||||
setLoadingScores(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTrustScore();
|
||||
fetchAllScores();
|
||||
|
||||
// Subscribe to HEZ balance updates
|
||||
let unsubscribeHez: () => void;
|
||||
@@ -535,30 +558,81 @@ export const AccountBalance: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Account Info */}
|
||||
{/* Account Info & Scores */}
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Account</span>
|
||||
<span className="text-white font-mono">
|
||||
{selectedAccount.meta.name || 'Unnamed'}
|
||||
</span>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg font-medium text-gray-300">
|
||||
Account Information
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{/* Account Details */}
|
||||
<div className="space-y-2 pb-4 border-b border-gray-800">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Account</span>
|
||||
<span className="text-white font-mono">
|
||||
{selectedAccount.meta.name || 'Unnamed'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Address</span>
|
||||
<span className="text-white font-mono text-xs">
|
||||
{selectedAccount.address.slice(0, 8)}...{selectedAccount.address.slice(-8)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Address</span>
|
||||
<span className="text-white font-mono text-xs">
|
||||
{selectedAccount.address.slice(0, 8)}...{selectedAccount.address.slice(-8)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400 flex items-center gap-1">
|
||||
<Award className="w-3 h-3 text-purple-400" />
|
||||
Trust Score
|
||||
</span>
|
||||
<span className="text-lg font-bold bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
{trustScore}
|
||||
</span>
|
||||
|
||||
{/* Scores from Blockchain */}
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-3">Scores from Blockchain</div>
|
||||
{loadingScores ? (
|
||||
<div className="text-sm text-gray-400">Loading scores...</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{/* Score Grid */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<Shield className="h-3 w-3 text-purple-400" />
|
||||
<span className="text-xs text-gray-400">Trust</span>
|
||||
</div>
|
||||
<span className="text-base font-bold text-purple-400">{scores.trustScore}</span>
|
||||
</div>
|
||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<Users className="h-3 w-3 text-cyan-400" />
|
||||
<span className="text-xs text-gray-400">Referral</span>
|
||||
</div>
|
||||
<span className="text-base font-bold text-cyan-400">{scores.referralScore}</span>
|
||||
</div>
|
||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<TrendingUp className="h-3 w-3 text-green-400" />
|
||||
<span className="text-xs text-gray-400">Staking</span>
|
||||
</div>
|
||||
<span className="text-base font-bold text-green-400">{scores.stakingScore}</span>
|
||||
</div>
|
||||
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<Award className="h-3 w-3 text-pink-400" />
|
||||
<span className="text-xs text-gray-400">Tiki</span>
|
||||
</div>
|
||||
<span className="text-base font-bold text-pink-400">{scores.tikiScore}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total Score */}
|
||||
<div className="pt-3 border-t border-gray-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-400">Total Score</span>
|
||||
<span className="text-xl font-bold bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
{scores.totalScore}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { TreasuryOverview } from './treasury/TreasuryOverview';
|
||||
import { FundingProposal } from './treasury/FundingProposal';
|
||||
import { SpendingHistory } from './treasury/SpendingHistory';
|
||||
import { MultiSigApproval } from './treasury/MultiSigApproval';
|
||||
import { Github, FileText, ExternalLink, Shield, Award, User, FileEdit, Users2, MessageSquare, ShieldCheck, Wifi, WifiOff, Wallet, DollarSign, PiggyBank, History, Key, TrendingUp, ArrowRightLeft, Lock, LogIn, LayoutDashboard, Settings, UserCog, Repeat } from 'lucide-react';
|
||||
import { Github, FileText, ExternalLink, Shield, Award, User, FileEdit, Users2, MessageSquare, ShieldCheck, Wifi, WifiOff, Wallet, DollarSign, PiggyBank, History, Key, TrendingUp, ArrowRightLeft, Lock, LogIn, LayoutDashboard, Settings, UserCog, Repeat, Users } from 'lucide-react';
|
||||
import GovernanceInterface from './GovernanceInterface';
|
||||
import RewardDistribution from './RewardDistribution';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
@@ -100,6 +100,14 @@ const AppLayout: React.FC = () => {
|
||||
Wallet
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/be-citizen')}
|
||||
className="text-cyan-300 hover:text-cyan-100 transition-colors flex items-center gap-1 text-sm font-semibold"
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
Be Citizen
|
||||
</button>
|
||||
|
||||
{/* Governance Dropdown */}
|
||||
<div className="relative group">
|
||||
<button className="text-gray-300 hover:text-white transition-colors flex items-center gap-1 text-sm">
|
||||
@@ -212,13 +220,22 @@ const AppLayout: React.FC = () => {
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => navigate('/login')}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center gap-2 text-sm"
|
||||
>
|
||||
<LogIn className="w-4 h-4" />
|
||||
Login
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
onClick={() => navigate('/be-citizen')}
|
||||
className="text-cyan-300 hover:text-cyan-100 transition-colors flex items-center gap-1 text-sm font-semibold"
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
Be Citizen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/login')}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center gap-2 text-sm"
|
||||
>
|
||||
<LogIn className="w-4 h-4" />
|
||||
Login
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<a
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Loader2, Award, Crown, Shield, Users } from 'lucide-react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { getUserTikis } from '@/lib/citizenship-workflow';
|
||||
import type { TikiInfo } from '@/lib/citizenship-workflow';
|
||||
|
||||
// Icon map for different Tiki roles
|
||||
const getTikiIcon = (role: string) => {
|
||||
const roleLower = role.toLowerCase();
|
||||
|
||||
if (roleLower.includes('hemwelatî') || roleLower.includes('welati') || roleLower.includes('citizen')) {
|
||||
return <Shield className="w-6 h-6 text-cyan-500" />;
|
||||
}
|
||||
if (roleLower.includes('leader') || roleLower.includes('chief')) {
|
||||
return <Crown className="w-6 h-6 text-yellow-500" />;
|
||||
}
|
||||
if (roleLower.includes('elder') || roleLower.includes('wise')) {
|
||||
return <Award className="w-6 h-6 text-purple-500" />;
|
||||
}
|
||||
return <Users className="w-6 h-6 text-green-500" />;
|
||||
};
|
||||
|
||||
// Color scheme for different roles
|
||||
const getRoleBadgeColor = (role: string) => {
|
||||
const roleLower = role.toLowerCase();
|
||||
|
||||
if (roleLower.includes('hemwelatî') || roleLower.includes('welati') || roleLower.includes('citizen')) {
|
||||
return 'bg-cyan-500/10 text-cyan-500 border-cyan-500/30';
|
||||
}
|
||||
if (roleLower.includes('leader') || roleLower.includes('chief')) {
|
||||
return 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30';
|
||||
}
|
||||
if (roleLower.includes('elder') || roleLower.includes('wise')) {
|
||||
return 'bg-purple-500/10 text-purple-500 border-purple-500/30';
|
||||
}
|
||||
return 'bg-green-500/10 text-green-500 border-green-500/30';
|
||||
};
|
||||
|
||||
export const NftList: React.FC = () => {
|
||||
const { api, isApiReady, selectedAccount } = usePolkadot();
|
||||
const [tikis, setTikis] = useState<TikiInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTikis = async () => {
|
||||
if (!api || !isApiReady || !selectedAccount) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const userTikis = await getUserTikis(api, selectedAccount.address);
|
||||
setTikis(userTikis);
|
||||
} catch (err) {
|
||||
console.error('Error fetching Tikis:', err);
|
||||
setError('Failed to load NFTs');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTikis();
|
||||
}, [api, isApiReady, selectedAccount]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Your NFTs (Tikis)</CardTitle>
|
||||
<CardDescription>Your Tiki collection</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-cyan-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Your NFTs (Tikis)</CardTitle>
|
||||
<CardDescription>Your Tiki collection</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center py-12">
|
||||
<p className="text-red-500">{error}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (tikis.length === 0) {
|
||||
return (
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white">Your NFTs (Tikis)</CardTitle>
|
||||
<CardDescription>Your Tiki collection</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-center py-12">
|
||||
<Award className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-gray-500 mb-2">No NFTs yet</p>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Complete your citizenship application to receive your Welati Tiki NFT
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-white flex items-center gap-2">
|
||||
<Award className="w-5 h-5" />
|
||||
Your NFTs (Tikiler)
|
||||
</CardTitle>
|
||||
<CardDescription>Your Tiki collection ({tikis.length} total)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{tikis.map((tiki, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-gray-800/50 border border-gray-700 rounded-lg p-4 hover:border-cyan-500/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Icon */}
|
||||
<div className="flex-shrink-0">
|
||||
{getTikiIcon(tiki.role)}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<h3 className="font-semibold text-white text-sm">
|
||||
Tiki #{tiki.id}
|
||||
</h3>
|
||||
<Badge className={getRoleBadgeColor(tiki.role)}>
|
||||
{tiki.role === 'Hemwelatî' ? 'Welati' : tiki.role}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Metadata if available */}
|
||||
{tiki.metadata && typeof tiki.metadata === 'object' && (
|
||||
<div className="space-y-1 mt-2">
|
||||
{Object.entries(tiki.metadata).map(([key, value]) => (
|
||||
<div key={key} className="text-xs text-gray-400">
|
||||
<span className="font-medium">{key}:</span>{' '}
|
||||
<span>{String(value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ExistingCitizenAuth } from './ExistingCitizenAuth';
|
||||
import { NewCitizenApplication } from './NewCitizenApplication';
|
||||
|
||||
interface CitizenshipModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onClose }) => {
|
||||
const [activeTab, setActiveTab] = useState<'existing' | 'new'>('existing');
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-2xl">
|
||||
🏛️ Digital Kurdistan Citizenship
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Join the Digital Kurdistan State as a citizen or authenticate your existing citizenship
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as 'existing' | 'new')} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="existing">I am Already a Citizen</TabsTrigger>
|
||||
<TabsTrigger value="new">I Want to Become a Citizen</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="existing" className="mt-6">
|
||||
<ExistingCitizenAuth onClose={onClose} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="new" className="mt-6">
|
||||
<NewCitizenApplication onClose={onClose} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,237 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Loader2, CheckCircle, AlertTriangle, Shield } from 'lucide-react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { verifyNftOwnership } from '@/lib/citizenship-workflow';
|
||||
import { generateAuthChallenge, signChallenge, verifySignature, saveCitizenSession } from '@/lib/citizenship-crypto';
|
||||
import type { AuthChallenge } from '@/lib/citizenship-crypto';
|
||||
|
||||
interface ExistingCitizenAuthProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClose }) => {
|
||||
const { api, isApiReady, selectedAccount, connectWallet } = usePolkadot();
|
||||
|
||||
const [tikiNumber, setTikiNumber] = useState('');
|
||||
const [step, setStep] = useState<'input' | 'verifying' | 'signing' | 'success' | 'error'>('input');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [challenge, setChallenge] = useState<AuthChallenge | null>(null);
|
||||
|
||||
const handleVerifyNFT = async () => {
|
||||
if (!api || !isApiReady || !selectedAccount) {
|
||||
setError('Please connect your wallet first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tikiNumber.trim()) {
|
||||
setError('Please enter your Welati Tiki NFT number');
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setStep('verifying');
|
||||
|
||||
try {
|
||||
// Verify NFT ownership
|
||||
const ownsNFT = await verifyNftOwnership(api, tikiNumber, selectedAccount.address);
|
||||
|
||||
if (!ownsNFT) {
|
||||
setError(`NFT #${tikiNumber} not found in your wallet or not a Welati Tiki`);
|
||||
setStep('error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate challenge for signature
|
||||
const authChallenge = generateAuthChallenge(tikiNumber);
|
||||
setChallenge(authChallenge);
|
||||
setStep('signing');
|
||||
} catch (err) {
|
||||
console.error('Verification error:', err);
|
||||
setError('Failed to verify NFT ownership');
|
||||
setStep('error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignChallenge = async () => {
|
||||
if (!selectedAccount || !challenge) {
|
||||
setError('Missing authentication data');
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Sign the challenge
|
||||
const signature = await signChallenge(selectedAccount, challenge);
|
||||
|
||||
// Verify signature (self-verification for demonstration)
|
||||
const isValid = await verifySignature(signature, challenge, selectedAccount.address);
|
||||
|
||||
if (!isValid) {
|
||||
setError('Signature verification failed');
|
||||
setStep('error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save session
|
||||
const session = {
|
||||
tikiNumber,
|
||||
walletAddress: selectedAccount.address,
|
||||
sessionToken: signature, // In production, use proper JWT
|
||||
lastAuthenticated: Date.now(),
|
||||
expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours
|
||||
};
|
||||
|
||||
await saveCitizenSession(session);
|
||||
|
||||
setStep('success');
|
||||
|
||||
// Redirect to citizen dashboard after 2 seconds
|
||||
setTimeout(() => {
|
||||
// TODO: Navigate to citizen dashboard
|
||||
onClose();
|
||||
window.location.href = '/dashboard'; // Or use router.push('/dashboard')
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Signature error:', err);
|
||||
setError('Failed to sign authentication challenge');
|
||||
setStep('error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnectWallet = async () => {
|
||||
try {
|
||||
await connectWallet();
|
||||
} catch (err) {
|
||||
setError('Failed to connect wallet');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5 text-cyan-500" />
|
||||
Authenticate as Citizen
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your Welati Tiki NFT number to authenticate
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Step 1: Enter NFT Number */}
|
||||
{step === 'input' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tikiNumber">Welati Tiki NFT Number</Label>
|
||||
<Input
|
||||
id="tikiNumber"
|
||||
placeholder="e.g., 12345"
|
||||
value={tikiNumber}
|
||||
onChange={(e) => setTikiNumber(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleVerifyNFT()}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This is your unique citizen ID number received after KYC approval
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!selectedAccount ? (
|
||||
<Button onClick={handleConnectWallet} className="w-full">
|
||||
Connect Wallet First
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleVerifyNFT} className="w-full">
|
||||
Verify NFT Ownership
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 2: Verifying */}
|
||||
{step === 'verifying' && (
|
||||
<div className="flex flex-col items-center justify-center py-8 space-y-4">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-cyan-500" />
|
||||
<p className="text-sm text-muted-foreground">Verifying NFT ownership on blockchain...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Sign Challenge */}
|
||||
{step === 'signing' && (
|
||||
<>
|
||||
<Alert>
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
<AlertDescription>
|
||||
NFT ownership verified! Now sign to prove you control this wallet.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="bg-muted p-4 rounded-lg space-y-2">
|
||||
<p className="text-sm font-medium">Authentication Challenge:</p>
|
||||
<p className="text-xs text-muted-foreground font-mono break-all">
|
||||
{challenge?.nonce}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleSignChallenge} className="w-full">
|
||||
Sign Message to Authenticate
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 4: Success */}
|
||||
{step === 'success' && (
|
||||
<div className="flex flex-col items-center justify-center py-8 space-y-4">
|
||||
<CheckCircle className="h-16 w-16 text-green-500" />
|
||||
<h3 className="text-lg font-semibold">Authentication Successful!</h3>
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
Welcome back, Citizen #{tikiNumber}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Redirecting to citizen dashboard...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{step === 'error' && (
|
||||
<Button onClick={() => { setStep('input'); setError(null); }} variant="outline" className="w-full">
|
||||
Try Again
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Security Info */}
|
||||
<Card className="bg-cyan-500/10 border-cyan-500/30">
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-2 text-sm">
|
||||
<h4 className="font-semibold flex items-center gap-2">
|
||||
<Shield className="h-4 w-4" />
|
||||
Security Information
|
||||
</h4>
|
||||
<ul className="space-y-1 text-xs text-muted-foreground">
|
||||
<li>• Your NFT number is cryptographically verified on-chain</li>
|
||||
<li>• Signature proves you control the wallet without revealing private keys</li>
|
||||
<li>• Session expires after 24 hours for your security</li>
|
||||
<li>• No personal data is transmitted or stored on-chain</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,538 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Loader2, AlertTriangle, CheckCircle, User, Users as UsersIcon, MapPin, Briefcase, Mail, Clock } from 'lucide-react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import type { CitizenshipData, Region, MaritalStatus } from '@/lib/citizenship-workflow';
|
||||
import { FOUNDER_ADDRESS, submitKycApplication, subscribeToKycApproval, getKycStatus } from '@/lib/citizenship-workflow';
|
||||
import { generateCommitmentHash, generateNullifierHash, encryptData, saveLocalCitizenshipData, uploadToIPFS } from '@/lib/citizenship-crypto';
|
||||
|
||||
interface NewCitizenApplicationProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type FormData = Omit<CitizenshipData, 'walletAddress' | 'timestamp'>;
|
||||
|
||||
export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ onClose }) => {
|
||||
const { api, isApiReady, selectedAccount, connectWallet } = usePolkadot();
|
||||
const { register, handleSubmit, watch, setValue, formState: { errors } } = useForm<FormData>();
|
||||
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [waitingForApproval, setWaitingForApproval] = useState(false);
|
||||
const [kycApproved, setKycApproved] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [checkingStatus, setCheckingStatus] = useState(false);
|
||||
|
||||
const maritalStatus = watch('maritalStatus');
|
||||
const childrenCount = watch('childrenCount');
|
||||
|
||||
// Check KYC status on mount
|
||||
useEffect(() => {
|
||||
const checkKycStatus = async () => {
|
||||
if (!api || !isApiReady || !selectedAccount) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCheckingStatus(true);
|
||||
try {
|
||||
const status = await getKycStatus(api, selectedAccount.address);
|
||||
console.log('Current KYC Status:', status);
|
||||
|
||||
if (status === 'Approved') {
|
||||
console.log('KYC already approved! Redirecting to dashboard...');
|
||||
setKycApproved(true);
|
||||
|
||||
// Redirect to dashboard after 2 seconds
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
window.location.href = '/dashboard';
|
||||
}, 2000);
|
||||
} else if (status === 'Pending') {
|
||||
// If pending, show the waiting screen
|
||||
setWaitingForApproval(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error checking KYC status:', err);
|
||||
} finally {
|
||||
setCheckingStatus(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkKycStatus();
|
||||
}, [api, isApiReady, selectedAccount, onClose]);
|
||||
|
||||
// Subscribe to KYC approval events
|
||||
useEffect(() => {
|
||||
if (!api || !isApiReady || !selectedAccount || !waitingForApproval) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Setting up KYC approval listener for:', selectedAccount.address);
|
||||
|
||||
const unsubscribe = subscribeToKycApproval(
|
||||
api,
|
||||
selectedAccount.address,
|
||||
() => {
|
||||
console.log('KYC Approved! Redirecting to dashboard...');
|
||||
setKycApproved(true);
|
||||
setWaitingForApproval(false);
|
||||
|
||||
// Redirect to citizen dashboard after 2 seconds
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
window.location.href = '/dashboard';
|
||||
}, 2000);
|
||||
},
|
||||
(error) => {
|
||||
console.error('KYC approval subscription error:', error);
|
||||
setError(`Failed to monitor approval status: ${error}`);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
};
|
||||
}, [api, isApiReady, selectedAccount, waitingForApproval, onClose]);
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
if (!api || !isApiReady || !selectedAccount) {
|
||||
setError('Please connect your wallet first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!agreed) {
|
||||
setError('Please agree to the terms');
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setSubmitting(true);
|
||||
|
||||
try {
|
||||
// Check KYC status before submitting
|
||||
const currentStatus = await getKycStatus(api, selectedAccount.address);
|
||||
|
||||
if (currentStatus === 'Approved') {
|
||||
setError('Your KYC has already been approved! Redirecting to dashboard...');
|
||||
setKycApproved(true);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
window.location.href = '/dashboard';
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStatus === 'Pending') {
|
||||
setError('You already have a pending KYC application. Please wait for admin approval.');
|
||||
setWaitingForApproval(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare complete citizenship data
|
||||
const citizenshipData: CitizenshipData = {
|
||||
...data,
|
||||
walletAddress: selectedAccount.address,
|
||||
timestamp: Date.now(),
|
||||
referralCode: data.referralCode || FOUNDER_ADDRESS // Auto-assign to founder if empty
|
||||
};
|
||||
|
||||
// Generate commitment and nullifier hashes
|
||||
const commitmentHash = await generateCommitmentHash(citizenshipData);
|
||||
const nullifierHash = await generateNullifierHash(selectedAccount.address, citizenshipData.timestamp);
|
||||
|
||||
console.log('Commitment Hash:', commitmentHash);
|
||||
console.log('Nullifier Hash:', nullifierHash);
|
||||
|
||||
// Encrypt data
|
||||
const encryptedData = await encryptData(citizenshipData, selectedAccount.address);
|
||||
|
||||
// Save to local storage (backup)
|
||||
await saveLocalCitizenshipData(citizenshipData, selectedAccount.address);
|
||||
|
||||
// Upload to IPFS
|
||||
const ipfsCid = await uploadToIPFS(encryptedData);
|
||||
|
||||
console.log('IPFS CID:', ipfsCid);
|
||||
console.log('IPFS CID type:', typeof ipfsCid);
|
||||
console.log('IPFS CID value:', JSON.stringify(ipfsCid));
|
||||
|
||||
// Ensure ipfsCid is a string
|
||||
const cidString = String(ipfsCid);
|
||||
if (!cidString || cidString === 'undefined' || cidString === '[object Object]') {
|
||||
throw new Error(`Invalid IPFS CID: ${cidString}`);
|
||||
}
|
||||
|
||||
// Submit to blockchain
|
||||
console.log('Submitting KYC application to blockchain...');
|
||||
const result = await submitKycApplication(
|
||||
api,
|
||||
selectedAccount,
|
||||
citizenshipData.fullName,
|
||||
citizenshipData.email,
|
||||
cidString,
|
||||
`Citizenship application for ${citizenshipData.fullName}`
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.error || 'Failed to submit KYC application to blockchain');
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ KYC application submitted to blockchain');
|
||||
console.log('Block hash:', result.blockHash);
|
||||
|
||||
// Move to waiting for approval state
|
||||
setSubmitted(true);
|
||||
setSubmitting(false);
|
||||
setWaitingForApproval(true);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Submission error:', err);
|
||||
setError('Failed to submit citizenship application');
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedAccount) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Connect Wallet Required</CardTitle>
|
||||
<CardDescription>
|
||||
You need to connect your wallet to apply for citizenship
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={connectWallet} className="w-full">
|
||||
Connect Wallet
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// KYC Approved - Success state
|
||||
if (kycApproved) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="pt-6 flex flex-col items-center justify-center py-8 space-y-4">
|
||||
<CheckCircle className="h-16 w-16 text-green-500 animate-pulse" />
|
||||
<h3 className="text-lg font-semibold text-center text-green-500">KYC Approved!</h3>
|
||||
<p className="text-sm text-muted-foreground text-center max-w-md">
|
||||
Congratulations! Your citizenship application has been approved. Redirecting to citizen dashboard...
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Waiting for approval - Loading state
|
||||
if (waitingForApproval) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="pt-6 flex flex-col items-center justify-center py-8 space-y-6">
|
||||
{/* Animated Loader with Halos */}
|
||||
<div className="relative flex items-center justify-center">
|
||||
{/* Outer halo */}
|
||||
<div className="absolute w-32 h-32 border-4 border-cyan-500/20 rounded-full animate-ping"></div>
|
||||
{/* Middle halo */}
|
||||
<div className="absolute w-24 h-24 border-4 border-purple-500/30 rounded-full animate-pulse"></div>
|
||||
{/* Inner spinning sun */}
|
||||
<div className="relative w-16 h-16 flex items-center justify-center">
|
||||
<Loader2 className="w-16 h-16 text-cyan-500 animate-spin" />
|
||||
<Clock className="absolute w-8 h-8 text-yellow-400 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center space-y-2">
|
||||
<h3 className="text-lg font-semibold">Waiting for Admin Approval</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-md">
|
||||
Your application has been submitted to the blockchain and is waiting for admin approval.
|
||||
This page will automatically update when your citizenship is approved.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Status steps */}
|
||||
<div className="w-full max-w-md space-y-3 pt-4">
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0" />
|
||||
<span className="text-muted-foreground">Application encrypted and stored on IPFS</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0" />
|
||||
<span className="text-muted-foreground">Transaction submitted to blockchain</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<Loader2 className="h-5 w-5 text-cyan-500 animate-spin flex-shrink-0" />
|
||||
<span className="font-medium">Waiting for admin to approve KYC...</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm opacity-50">
|
||||
<Clock className="h-5 w-5 text-gray-400 flex-shrink-0" />
|
||||
<span className="text-muted-foreground">Receive Welati Tiki NFT</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<Alert className="bg-cyan-500/10 border-cyan-500/30">
|
||||
<AlertDescription className="text-xs">
|
||||
<strong>Note:</strong> Do not close this page. The system is monitoring the blockchain
|
||||
for approval events in real-time. You will be automatically redirected once approved.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Initial submission success (before blockchain confirmation)
|
||||
if (submitted && !waitingForApproval) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="pt-6 flex flex-col items-center justify-center py-8 space-y-4">
|
||||
<Loader2 className="h-16 w-16 text-cyan-500 animate-spin" />
|
||||
<h3 className="text-lg font-semibold text-center">Processing Application...</h3>
|
||||
<p className="text-sm text-muted-foreground text-center max-w-md">
|
||||
Encrypting your data and submitting to the blockchain. Please wait...
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
{/* Personal Identity Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="h-5 w-5" />
|
||||
Nasnameya Kesane (Personal Identity)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fullName">Navê Te (Your Full Name) *</Label>
|
||||
<Input {...register('fullName', { required: true })} placeholder="e.g., Berzê Ronahî" />
|
||||
{errors.fullName && <p className="text-xs text-red-500">Required</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fatherName">Navê Bavê Te (Father's Name) *</Label>
|
||||
<Input {...register('fatherName', { required: true })} placeholder="e.g., Şêrko" />
|
||||
{errors.fatherName && <p className="text-xs text-red-500">Required</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="grandfatherName">Navê Bavkalê Te (Grandfather's Name) *</Label>
|
||||
<Input {...register('grandfatherName', { required: true })} placeholder="e.g., Welat" />
|
||||
{errors.grandfatherName && <p className="text-xs text-red-500">Required</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="motherName">Navê Dayika Te (Mother's Name) *</Label>
|
||||
<Input {...register('motherName', { required: true })} placeholder="e.g., Gula" />
|
||||
{errors.motherName && <p className="text-xs text-red-500">Required</p>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Tribal Affiliation */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Eşîra Te (Tribal Affiliation)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tribe">Eşîra Te (Your Tribe) *</Label>
|
||||
<Input {...register('tribe', { required: true })} placeholder="e.g., Barzanî, Soran, Hewramî..." />
|
||||
{errors.tribe && <p className="text-xs text-red-500">Required</p>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Family Status */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<UsersIcon className="h-5 w-5" />
|
||||
Rewşa Malbatê (Family Status)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Zewicî / Nezewicî (Married / Unmarried) *</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) => setValue('maritalStatus', value as MaritalStatus)}
|
||||
defaultValue="nezewici"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="zewici" id="married" />
|
||||
<Label htmlFor="married">Zewicî (Married)</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="nezewici" id="unmarried" />
|
||||
<Label htmlFor="unmarried">Nezewicî (Unmarried)</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{maritalStatus === 'zewici' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="childrenCount">Hejmara Zarokan (Number of Children)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
{...register('childrenCount', { valueAsNumber: true })}
|
||||
placeholder="0"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{childrenCount && childrenCount > 0 && (
|
||||
<div className="space-y-3">
|
||||
<Label>Navên Zarokan (Children's Names)</Label>
|
||||
{Array.from({ length: childrenCount }).map((_, i) => (
|
||||
<div key={i} className="grid grid-cols-2 gap-2">
|
||||
<Input
|
||||
{...register(`children.${i}.name` as const)}
|
||||
placeholder={`Zaroka ${i + 1} - Nav`}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
{...register(`children.${i}.birthYear` as const, { valueAsNumber: true })}
|
||||
placeholder="Sala Dayikbûnê"
|
||||
min="1900"
|
||||
max={new Date().getFullYear()}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Geographic Origin */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MapPin className="h-5 w-5" />
|
||||
Herêma Te (Your Region)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="region">Ji Kuderê yî? (Where are you from?) *</Label>
|
||||
<Select onValueChange={(value) => setValue('region', value as Region)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Herêmeke hilbijêre (Select a region)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="bakur">Bakur (North - Turkey/Türkiye)</SelectItem>
|
||||
<SelectItem value="basur">Başûr (South - Iraq)</SelectItem>
|
||||
<SelectItem value="rojava">Rojava (West - Syria)</SelectItem>
|
||||
<SelectItem value="rojhelat">Rojhilat (East - Iran)</SelectItem>
|
||||
<SelectItem value="kurdistan_a_sor">Kurdistan a Sor (Red Kurdistan - Armenia/Azerbaijan)</SelectItem>
|
||||
<SelectItem value="diaspora">Diaspora (Living Abroad)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.region && <p className="text-xs text-red-500">Required</p>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Contact & Profession */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Briefcase className="h-5 w-5" />
|
||||
Têkilî û Pîşe (Contact & Profession)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="flex items-center gap-2">
|
||||
<Mail className="h-4 w-4" />
|
||||
E-mail *
|
||||
</Label>
|
||||
<Input
|
||||
type="email"
|
||||
{...register('email', { required: true, pattern: /^\S+@\S+$/i })}
|
||||
placeholder="example@email.com"
|
||||
/>
|
||||
{errors.email && <p className="text-xs text-red-500">Valid email required</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="profession">Pîşeya Te (Your Profession) *</Label>
|
||||
<Input {...register('profession', { required: true })} placeholder="e.g., Mamosta, Bijîşk, Xebatkar..." />
|
||||
{errors.profession && <p className="text-xs text-red-500">Required</p>}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Referral */}
|
||||
<Card className="bg-purple-500/10 border-purple-500/30">
|
||||
<CardHeader>
|
||||
<CardTitle>Koda Referral (Referral Code - Optional)</CardTitle>
|
||||
<CardDescription>
|
||||
If you were invited by another citizen, enter their referral code
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Input {...register('referralCode')} placeholder="Optional - Leave empty to be auto-assigned to Founder" />
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
If empty, you will be automatically linked to the Founder (Satoshi Qazi Muhammed)
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Terms Agreement */}
|
||||
<Card>
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="flex items-start space-x-2">
|
||||
<Checkbox id="terms" checked={agreed} onCheckedChange={(checked) => setAgreed(checked as boolean)} />
|
||||
<Label htmlFor="terms" className="text-sm leading-relaxed cursor-pointer">
|
||||
Ez pejirandim ku daneyên min bi awayekî ewle (ZK-proof) tên hilanîn û li ser blockchain-ê hash-a wan tê tomarkirin.
|
||||
<br />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
(I agree that my data is securely stored with ZK-proof and only its hash is recorded on the blockchain)
|
||||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button type="submit" disabled={submitting || !agreed} className="w-full" size="lg">
|
||||
{submitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Şandina Daxwazê...
|
||||
</>
|
||||
) : (
|
||||
'Şandina Daxwazê (Submit Application)'
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Wallet, Chrome, ExternalLink, Copy, Check, LogOut, Award } from 'lucide-react';
|
||||
import { Wallet, Chrome, ExternalLink, Copy, Check, LogOut, Award, Users, TrendingUp, Shield } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { formatAddress } from '@/lib/wallet';
|
||||
import { getAllScores, type UserScores } from '@/lib/scores';
|
||||
|
||||
interface WalletModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -29,7 +30,14 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
} = usePolkadot();
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [trustScore, setTrustScore] = useState<string>('-');
|
||||
const [scores, setScores] = useState<UserScores>({
|
||||
trustScore: 0,
|
||||
referralScore: 0,
|
||||
stakingScore: 0,
|
||||
tikiScore: 0,
|
||||
totalScore: 0
|
||||
});
|
||||
const [loadingScores, setLoadingScores] = useState(false);
|
||||
|
||||
const handleCopyAddress = () => {
|
||||
if (selectedAccount?.address) {
|
||||
@@ -53,39 +61,39 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Fetch trust score from blockchain
|
||||
// Fetch all scores from blockchain
|
||||
useEffect(() => {
|
||||
const fetchTrustScore = async () => {
|
||||
console.log('🔍 Fetching trust score...', {
|
||||
hasApi: !!api,
|
||||
isApiReady,
|
||||
hasAccount: !!selectedAccount,
|
||||
address: selectedAccount?.address
|
||||
});
|
||||
|
||||
const fetchAllScores = async () => {
|
||||
if (!api || !isApiReady || !selectedAccount?.address) {
|
||||
console.log('⚠️ Cannot fetch trust score - missing requirements');
|
||||
setTrustScore('-');
|
||||
setScores({
|
||||
trustScore: 0,
|
||||
referralScore: 0,
|
||||
stakingScore: 0,
|
||||
tikiScore: 0,
|
||||
totalScore: 0
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingScores(true);
|
||||
try {
|
||||
console.log('📡 Querying api.query.trust.trustScores...');
|
||||
const score = await api.query.trust.trustScores(selectedAccount.address);
|
||||
const scoreStr = score.toString();
|
||||
setTrustScore(scoreStr);
|
||||
console.log('✅ Trust score fetched successfully:', scoreStr);
|
||||
const userScores = await getAllScores(api, selectedAccount.address);
|
||||
setScores(userScores);
|
||||
} catch (err) {
|
||||
console.error('❌ Failed to fetch trust score:', err);
|
||||
console.error('Error details:', {
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
stack: err instanceof Error ? err.stack : undefined
|
||||
console.error('Failed to fetch scores:', err);
|
||||
setScores({
|
||||
trustScore: 0,
|
||||
referralScore: 0,
|
||||
stakingScore: 0,
|
||||
tikiScore: 0,
|
||||
totalScore: 0
|
||||
});
|
||||
setTrustScore('-');
|
||||
} finally {
|
||||
setLoadingScores(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTrustScore();
|
||||
fetchAllScores();
|
||||
}, [api, isApiReady, selectedAccount]);
|
||||
|
||||
return (
|
||||
@@ -164,12 +172,48 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-1">Trust Score</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Award className="h-4 w-4 text-purple-400" />
|
||||
<span className="text-lg font-bold bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
{trustScore}
|
||||
</span>
|
||||
<div className="text-xs text-gray-400 mb-2">Scores from Blockchain</div>
|
||||
{loadingScores ? (
|
||||
<div className="text-sm text-gray-400">Loading scores...</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-gray-900/50 rounded p-2">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<Shield className="h-3 w-3 text-purple-400" />
|
||||
<span className="text-xs text-gray-400">Trust</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-purple-400">{scores.trustScore}</span>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 rounded p-2">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<Users className="h-3 w-3 text-cyan-400" />
|
||||
<span className="text-xs text-gray-400">Referral</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-cyan-400">{scores.referralScore}</span>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 rounded p-2">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<TrendingUp className="h-3 w-3 text-green-400" />
|
||||
<span className="text-xs text-gray-400">Staking</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-green-400">{scores.stakingScore}</span>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 rounded p-2">
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<Award className="h-3 w-3 text-pink-400" />
|
||||
<span className="text-xs text-gray-400">Tiki</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-pink-400">{scores.tikiScore}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 pt-2 border-t border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-400">Total Score</span>
|
||||
<span className="text-lg font-bold bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
{loadingScores ? '...' : scores.totalScore}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user