mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-24 01:37:55 +00:00
refactor(core): Apply various updates and fixes across components
This commit is contained in:
+5
-2
@@ -24,6 +24,7 @@ import { WebSocketProvider } from '@/contexts/WebSocketContext';
|
||||
import { IdentityProvider } from '@/contexts/IdentityContext';
|
||||
import { AuthProvider } from '@/contexts/AuthContext';
|
||||
import { DashboardProvider } from '@/contexts/DashboardContext';
|
||||
import { ReferralProvider } from '@/contexts/ReferralContext';
|
||||
import { ProtectedRoute } from '@/components/ProtectedRoute';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
@@ -42,7 +43,8 @@ function App() {
|
||||
<WebSocketProvider>
|
||||
<IdentityProvider>
|
||||
<DashboardProvider>
|
||||
<Router>
|
||||
<ReferralProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
@@ -100,7 +102,8 @@ function App() {
|
||||
} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</Router>
|
||||
</ReferralProvider>
|
||||
</DashboardProvider>
|
||||
</IdentityProvider>
|
||||
</WebSocketProvider>
|
||||
|
||||
@@ -10,13 +10,13 @@ import type { TikiInfo } from '@pezkuwi/lib/citizenship-workflow';
|
||||
const getTikiIcon = (role: string) => {
|
||||
const roleLower = role.toLowerCase();
|
||||
|
||||
if (roleLower.includes('hemwelatî') || roleLower.includes('welati') || roleLower.includes('citizen')) {
|
||||
if (roleLower.includes('welati') || roleLower.includes('citizen')) {
|
||||
return <Shield className="w-6 h-6 text-cyan-500" />;
|
||||
}
|
||||
if (roleLower.includes('leader') || roleLower.includes('chief')) {
|
||||
if (roleLower.includes('serok') || roleLower.includes('leader') || roleLower.includes('chief')) {
|
||||
return <Crown className="w-6 h-6 text-yellow-500" />;
|
||||
}
|
||||
if (roleLower.includes('elder') || roleLower.includes('wise')) {
|
||||
if (roleLower.includes('axa') || roleLower.includes('hekem') || 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" />;
|
||||
@@ -26,13 +26,13 @@ const getTikiIcon = (role: string) => {
|
||||
const getRoleBadgeColor = (role: string) => {
|
||||
const roleLower = role.toLowerCase();
|
||||
|
||||
if (roleLower.includes('hemwelatî') || roleLower.includes('welati') || roleLower.includes('citizen')) {
|
||||
if (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')) {
|
||||
if (roleLower.includes('serok') || 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')) {
|
||||
if (roleLower.includes('axa') || roleLower.includes('hekem') || 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';
|
||||
@@ -149,7 +149,7 @@ export const NftList: React.FC = () => {
|
||||
Tiki #{tiki.id}
|
||||
</h3>
|
||||
<Badge className={getRoleBadgeColor(tiki.role)}>
|
||||
{tiki.role === 'Hemwelatî' ? 'Welati' : tiki.role}
|
||||
{tiki.role}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,23 +1,85 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { Loader2, Wallet } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
requireAdmin?: boolean;
|
||||
}
|
||||
|
||||
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
children,
|
||||
requireAdmin = false
|
||||
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
children,
|
||||
requireAdmin = false
|
||||
}) => {
|
||||
const { user, loading, isAdmin } = useAuth();
|
||||
const { selectedAccount, connectWallet } = usePolkadot();
|
||||
const [walletRestoreChecked, setWalletRestoreChecked] = useState(false);
|
||||
const [forceUpdate, setForceUpdate] = useState(0);
|
||||
|
||||
if (loading) {
|
||||
// Listen for wallet changes
|
||||
useEffect(() => {
|
||||
const handleWalletChange = () => {
|
||||
setForceUpdate(prev => prev + 1);
|
||||
};
|
||||
|
||||
window.addEventListener('walletChanged', handleWalletChange);
|
||||
return () => window.removeEventListener('walletChanged', handleWalletChange);
|
||||
}, []);
|
||||
|
||||
// Wait for wallet restoration (max 3 seconds)
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setWalletRestoreChecked(true);
|
||||
}, 3000);
|
||||
|
||||
// If wallet restored earlier, clear timeout
|
||||
if (selectedAccount) {
|
||||
setWalletRestoreChecked(true);
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [selectedAccount, forceUpdate]);
|
||||
|
||||
// Show loading while:
|
||||
// 1. Auth is loading, OR
|
||||
// 2. Wallet restoration not checked yet
|
||||
if (loading || !walletRestoreChecked) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-green-500" />
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-green-500 mx-auto mb-4" />
|
||||
<p className="text-gray-400">
|
||||
{!walletRestoreChecked ? 'Restoring wallet connection...' : 'Loading...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// For admin routes, require wallet connection
|
||||
if (requireAdmin && !selectedAccount) {
|
||||
const handleConnect = async () => {
|
||||
await connectWallet();
|
||||
// Event is automatically dispatched by handleSetSelectedAccount wrapper
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||
<div className="text-center max-w-md">
|
||||
<Wallet className="w-16 h-16 text-green-500 mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold text-white mb-2">Connect Your Wallet</h2>
|
||||
<p className="text-gray-400 mb-6">
|
||||
Admin panel requires wallet authentication. Please connect your wallet to continue.
|
||||
</p>
|
||||
<Button onClick={handleConnect} size="lg" className="bg-green-600 hover:bg-green-700">
|
||||
<Wallet className="mr-2 h-5 w-5" />
|
||||
Connect Wallet
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,7 +89,20 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||
}
|
||||
|
||||
if (requireAdmin && !isAdmin) {
|
||||
return <Navigate to="/" replace />;
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||
<div className="text-center max-w-md">
|
||||
<div className="text-red-500 text-6xl mb-4">⛔</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">Access Denied</h2>
|
||||
<p className="text-gray-400 mb-4">
|
||||
Your wallet ({selectedAccount?.address.slice(0, 8)}...) does not have admin privileges.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Only founder and commission members can access the admin panel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
||||
@@ -863,8 +863,8 @@ const TokenSwap = () => {
|
||||
type="number"
|
||||
value={fromAmount}
|
||||
onChange={(e) => setFromAmount(e.target.value)}
|
||||
placeholder="0.0"
|
||||
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-600"
|
||||
placeholder="Amount"
|
||||
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-500 placeholder:opacity-50"
|
||||
disabled={!selectedAccount}
|
||||
/>
|
||||
<Select
|
||||
@@ -934,8 +934,8 @@ const TokenSwap = () => {
|
||||
type="text"
|
||||
value={toAmount}
|
||||
readOnly
|
||||
placeholder="0.0"
|
||||
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-600"
|
||||
placeholder="Amount"
|
||||
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
<Select
|
||||
value={toToken}
|
||||
|
||||
@@ -269,8 +269,8 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
id="recipient"
|
||||
value={recipient}
|
||||
onChange={(e) => setRecipient(e.target.value)}
|
||||
placeholder="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
|
||||
className="bg-gray-800 border-gray-700 text-white mt-2"
|
||||
placeholder="Recipient address"
|
||||
className="bg-gray-800 border-gray-700 text-white mt-2 placeholder:text-gray-500 placeholder:opacity-50"
|
||||
disabled={isTransferring}
|
||||
/>
|
||||
</div>
|
||||
@@ -283,8 +283,8 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
||||
step={selectedToken === 'HEZ' || selectedToken === 'PEZ' ? '0.0001' : '0.000001'}
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
placeholder="0.0000"
|
||||
className="bg-gray-800 border-gray-700 text-white mt-2"
|
||||
placeholder="Amount"
|
||||
className="bg-gray-800 border-gray-700 text-white mt-2 placeholder:text-gray-500 placeholder:opacity-50"
|
||||
disabled={isTransferring}
|
||||
/>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
|
||||
@@ -214,8 +214,8 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
type="number"
|
||||
value={depositAmount}
|
||||
onChange={(e) => setDepositAmount(e.target.value)}
|
||||
placeholder="0.00"
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500"
|
||||
placeholder="Amount"
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500 placeholder:text-gray-500 placeholder:opacity-50"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
@@ -279,9 +279,9 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
type="number"
|
||||
value={withdrawAmount}
|
||||
onChange={(e) => setWithdrawAmount(e.target.value)}
|
||||
placeholder="0.00"
|
||||
placeholder="Amount"
|
||||
max={wusdtBalance}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500"
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500 placeholder:text-gray-500 placeholder:opacity-50"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
@@ -300,8 +300,8 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
||||
type="text"
|
||||
value={withdrawAddress}
|
||||
onChange={(e) => setWithdrawAddress(e.target.value)}
|
||||
placeholder="Enter bank account or crypto address"
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500"
|
||||
placeholder="Bank account or crypto address"
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500 placeholder:text-gray-500 placeholder:opacity-50"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -391,10 +391,10 @@ export function CommissionSetupTab() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<textarea
|
||||
placeholder="Paste addresses, one per line Example: 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty 5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y"
|
||||
placeholder="Member addresses, one per line"
|
||||
value={newMemberAddress}
|
||||
onChange={(e) => setNewMemberAddress(e.target.value)}
|
||||
className="flex-1 font-mono text-sm p-3 bg-gray-800 border border-gray-700 rounded min-h-[120px]"
|
||||
className="flex-1 font-mono text-sm p-3 bg-gray-800 border border-gray-700 rounded min-h-[120px] placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleAddMember}
|
||||
|
||||
@@ -13,10 +13,11 @@ import { NewCitizenApplication } from './NewCitizenApplication';
|
||||
interface CitizenshipModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
referrerAddress?: string | null;
|
||||
}
|
||||
|
||||
export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onClose }) => {
|
||||
const [activeTab, setActiveTab] = useState<'existing' | 'new'>('existing');
|
||||
export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onClose, referrerAddress }) => {
|
||||
const [activeTab, setActiveTab] = useState<'existing' | 'new'>(referrerAddress ? 'new' : 'existing');
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
@@ -26,7 +27,9 @@ export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onCl
|
||||
🏛️ Digital Kurdistan Citizenship
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Join the Digital Kurdistan State as a citizen or authenticate your existing citizenship
|
||||
{referrerAddress
|
||||
? 'You have been invited to join Digital Kurdistan! Complete the application below.'
|
||||
: 'Join the Digital Kurdistan State as a citizen or authenticate your existing citizenship'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -41,7 +44,7 @@ export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onCl
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="new" className="mt-6">
|
||||
<NewCitizenApplication onClose={onClose} />
|
||||
<NewCitizenApplication onClose={onClose} referrerAddress={referrerAddress} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 '@pezkuwi/lib/citizenship-workflow';
|
||||
import { verifyCitizenNumber } from '@pezkuwi/lib/tiki';
|
||||
import { generateAuthChallenge, signChallenge, verifySignature, saveCitizenSession } from '@pezkuwi/lib/citizenship-workflow';
|
||||
import type { AuthChallenge } from '@pezkuwi/lib/citizenship-workflow';
|
||||
|
||||
@@ -17,7 +17,7 @@ interface ExistingCitizenAuthProps {
|
||||
export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClose }) => {
|
||||
const { api, isApiReady, selectedAccount, connectWallet } = usePolkadot();
|
||||
|
||||
const [tikiNumber, setTikiNumber] = useState('');
|
||||
const [citizenNumber, setCitizenNumber] = 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);
|
||||
@@ -28,8 +28,8 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tikiNumber.trim()) {
|
||||
setError('Please enter your Welati Tiki NFT number');
|
||||
if (!citizenNumber.trim()) {
|
||||
setError('Please enter your Citizen Number');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -37,22 +37,22 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
setStep('verifying');
|
||||
|
||||
try {
|
||||
// Verify NFT ownership
|
||||
const ownsNFT = await verifyNftOwnership(api, tikiNumber, selectedAccount.address);
|
||||
// Verify Citizen Number
|
||||
const isValid = await verifyCitizenNumber(api, citizenNumber, selectedAccount.address);
|
||||
|
||||
if (!ownsNFT) {
|
||||
setError(`NFT #${tikiNumber} not found in your wallet or not a Welati Tiki`);
|
||||
if (!isValid) {
|
||||
setError(`Invalid Citizen Number or it doesn't match your wallet`);
|
||||
setStep('error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate challenge for signature
|
||||
const authChallenge = generateAuthChallenge(tikiNumber);
|
||||
const authChallenge = generateAuthChallenge(citizenNumber);
|
||||
setChallenge(authChallenge);
|
||||
setStep('signing');
|
||||
} catch (err) {
|
||||
console.error('Verification error:', err);
|
||||
setError('Failed to verify NFT ownership');
|
||||
setError('Failed to verify Citizen Number');
|
||||
setStep('error');
|
||||
}
|
||||
};
|
||||
@@ -80,7 +80,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
|
||||
// Save session
|
||||
const session = {
|
||||
tikiNumber,
|
||||
tikiNumber: citizenNumber,
|
||||
walletAddress: selectedAccount.address,
|
||||
sessionToken: signature, // In production, use proper JWT
|
||||
lastAuthenticated: Date.now(),
|
||||
@@ -91,11 +91,10 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
|
||||
setStep('success');
|
||||
|
||||
// Redirect to citizen dashboard after 2 seconds
|
||||
// Redirect to citizens page after 2 seconds
|
||||
setTimeout(() => {
|
||||
// TODO: Navigate to citizen dashboard
|
||||
onClose();
|
||||
window.location.href = '/dashboard'; // Or use router.push('/dashboard')
|
||||
window.location.href = '/citizens';
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Signature error:', err);
|
||||
@@ -121,24 +120,24 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
Authenticate as Citizen
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your Welati Tiki NFT number to authenticate
|
||||
Enter your Citizen Number from your Dashboard to authenticate
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Step 1: Enter NFT Number */}
|
||||
{/* Step 1: Enter Citizen Number */}
|
||||
{step === 'input' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tikiNumber">Welati Tiki NFT Number</Label>
|
||||
<Label htmlFor="citizenNumber">Citizen Number</Label>
|
||||
<Input
|
||||
id="tikiNumber"
|
||||
placeholder="e.g., 12345"
|
||||
value={tikiNumber}
|
||||
onChange={(e) => setTikiNumber(e.target.value)}
|
||||
id="citizenNumber"
|
||||
placeholder="e.g., #42-0-123456"
|
||||
value={citizenNumber}
|
||||
onChange={(e) => setCitizenNumber(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
|
||||
Enter your full Citizen Number from your Dashboard (format: #CollectionID-ItemID-6digits)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -148,7 +147,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleVerifyNFT} className="w-full">
|
||||
Verify NFT Ownership
|
||||
Verify Citizen Number
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -158,7 +157,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
{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>
|
||||
<p className="text-sm text-muted-foreground">Verifying Citizen Number on blockchain...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -191,7 +190,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
||||
<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}
|
||||
Welcome back, Citizen #{citizenNumber}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Redirecting to citizen dashboard...
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 { Loader2, AlertTriangle, CheckCircle, User, Users as UsersIcon, MapPin, Briefcase, Mail, Clock, Check, X, AlertCircle } from 'lucide-react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import type { CitizenshipData, Region, MaritalStatus } from '@pezkuwi/lib/citizenship-workflow';
|
||||
import { FOUNDER_ADDRESS, submitKycApplication, subscribeToKycApproval, getKycStatus } from '@pezkuwi/lib/citizenship-workflow';
|
||||
@@ -16,11 +16,12 @@ import { generateCommitmentHash, generateNullifierHash, encryptData, saveLocalCi
|
||||
|
||||
interface NewCitizenApplicationProps {
|
||||
onClose: () => void;
|
||||
referrerAddress?: string | null;
|
||||
}
|
||||
|
||||
type FormData = Omit<CitizenshipData, 'walletAddress' | 'timestamp'>;
|
||||
|
||||
export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ onClose }) => {
|
||||
export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ onClose, referrerAddress }) => {
|
||||
const { api, isApiReady, selectedAccount, connectWallet } = usePolkadot();
|
||||
const { register, handleSubmit, watch, setValue, formState: { errors } } = useForm<FormData>();
|
||||
|
||||
@@ -31,10 +32,80 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [checkingStatus, setCheckingStatus] = useState(false);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [applicationHash, setApplicationHash] = useState<string>('');
|
||||
|
||||
const maritalStatus = watch('maritalStatus');
|
||||
const childrenCount = watch('childrenCount');
|
||||
|
||||
const handleApprove = async () => {
|
||||
if (!api || !selectedAccount) {
|
||||
setError('Please connect your wallet first');
|
||||
return;
|
||||
}
|
||||
|
||||
setConfirming(true);
|
||||
try {
|
||||
const { web3FromAddress } = await import('@polkadot/extension-dapp');
|
||||
const injector = await web3FromAddress(selectedAccount.address);
|
||||
|
||||
console.log('Confirming citizenship application (self-confirmation)...');
|
||||
|
||||
// Call confirm_citizenship() extrinsic - self-confirmation for Welati Tiki
|
||||
const tx = api.tx.identityKyc.confirmCitizenship();
|
||||
|
||||
await tx.signAndSend(selectedAccount.address, { signer: injector.signer }, ({ status, events, dispatchError }) => {
|
||||
if (dispatchError) {
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
console.error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`);
|
||||
setError(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`);
|
||||
} else {
|
||||
console.error(dispatchError.toString());
|
||||
setError(dispatchError.toString());
|
||||
}
|
||||
setConfirming(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.isInBlock || status.isFinalized) {
|
||||
console.log('✅ Citizenship confirmed successfully!');
|
||||
console.log('Block hash:', status.asInBlock || status.asFinalized);
|
||||
|
||||
// Check for CitizenshipConfirmed event
|
||||
events.forEach(({ event }) => {
|
||||
if (event.section === 'identityKyc' && event.method === 'CitizenshipConfirmed') {
|
||||
console.log('📢 CitizenshipConfirmed event detected');
|
||||
setKycApproved(true);
|
||||
setWaitingForApproval(false);
|
||||
|
||||
// Redirect to citizen dashboard after 2 seconds
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
window.location.href = '/dashboard';
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
setConfirming(false);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Approval error:', err);
|
||||
setError(err.message || 'Failed to approve application');
|
||||
setConfirming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = async () => {
|
||||
// Cancel/withdraw the application - simply close modal and go back
|
||||
// No blockchain interaction needed - application will remain Pending until confirmed or admin-rejected
|
||||
console.log('Canceling citizenship application (no blockchain interaction)');
|
||||
onClose();
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
// Check KYC status on mount
|
||||
useEffect(() => {
|
||||
const checkKycStatus = async () => {
|
||||
@@ -139,6 +210,13 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: Referral initiation must be done by the REFERRER before the referee does KYC
|
||||
// The referrer calls api.tx.referral.initiateReferral(refereeAddress) from InviteUserModal
|
||||
// Here we just use the referrerAddress in the citizenship data if provided
|
||||
if (referrerAddress) {
|
||||
console.log(`KYC application with referrer: ${referrerAddress}`);
|
||||
}
|
||||
|
||||
// Prepare complete citizenship data
|
||||
const citizenshipData: CitizenshipData = {
|
||||
...data,
|
||||
@@ -193,6 +271,11 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
console.log('✅ KYC application submitted to blockchain');
|
||||
console.log('Block hash:', result.blockHash);
|
||||
|
||||
// Save block hash for display
|
||||
if (result.blockHash) {
|
||||
setApplicationHash(result.blockHash.slice(0, 16) + '...');
|
||||
}
|
||||
|
||||
// Move to waiting for approval state
|
||||
setSubmitted(true);
|
||||
setSubmitting(false);
|
||||
@@ -238,59 +321,107 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
);
|
||||
}
|
||||
|
||||
// Waiting for approval - Loading state
|
||||
// Waiting for self-confirmation
|
||||
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" />
|
||||
{/* Icon */}
|
||||
<div className="relative">
|
||||
<div className="h-24 w-24 rounded-full border-4 border-primary/20 flex items-center justify-center">
|
||||
<CheckCircle className="h-10 w-10 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center space-y-2">
|
||||
<h3 className="text-lg font-semibold">Waiting for Admin Approval</h3>
|
||||
<h3 className="text-lg font-semibold">Confirm Your Citizenship Application</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.
|
||||
Your application has been submitted to the blockchain. Please review and confirm your identity to mint your Citizen NFT (Welati Tiki).
|
||||
</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 className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
|
||||
<Check className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Data Encrypted</p>
|
||||
<p className="text-xs text-muted-foreground">Your KYC data has been encrypted and stored on IPFS</p>
|
||||
</div>
|
||||
</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 className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
|
||||
<Check className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Blockchain Submitted</p>
|
||||
<p className="text-xs text-muted-foreground">Transaction hash: {applicationHash || 'Processing...'}</p>
|
||||
</div>
|
||||
</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 className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
|
||||
<AlertCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Awaiting Your Confirmation</p>
|
||||
<p className="text-xs text-muted-foreground">Confirm or reject your application below</p>
|
||||
</div>
|
||||
</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>
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-3 w-full max-w-md pt-4">
|
||||
<Button
|
||||
onClick={handleApprove}
|
||||
disabled={confirming}
|
||||
className="flex-1 bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{confirming ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Confirming...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
Approve
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleReject}
|
||||
disabled={confirming}
|
||||
variant="destructive"
|
||||
className="flex-1"
|
||||
>
|
||||
{confirming ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Rejecting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Reject
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive" className="w-full max-w-md">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button variant="outline" onClick={onClose} className="mt-2">
|
||||
Close
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -493,7 +624,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Input {...register('referralCode')} placeholder="Optional - Leave empty to be auto-assigned to Founder" />
|
||||
<Input {...register('referralCode')} placeholder="Referral code (optional)" className="placeholder:text-gray-500 placeholder:opacity-50" />
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
If empty, you will be automatically linked to the Founder (Satoshi Qazi Muhammed)
|
||||
</p>
|
||||
|
||||
@@ -172,13 +172,14 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="amountCrypto">Amount ({token})</Label>
|
||||
<Input
|
||||
id="amountCrypto"
|
||||
type="number"
|
||||
<Input
|
||||
id="amountCrypto"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={amountCrypto}
|
||||
value={amountCrypto}
|
||||
onChange={e => setAmountCrypto(e.target.value)}
|
||||
placeholder="10.00"
|
||||
placeholder="Amount"
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -202,13 +203,14 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="fiatAmount">Total Amount ({fiatCurrency})</Label>
|
||||
<Input
|
||||
id="fiatAmount"
|
||||
type="number"
|
||||
<Input
|
||||
id="fiatAmount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={fiatAmount}
|
||||
value={fiatAmount}
|
||||
onChange={e => setFiatAmount(e.target.value)}
|
||||
placeholder="1000.00"
|
||||
placeholder="Amount"
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -254,6 +256,7 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
value={paymentDetails[field] || ''}
|
||||
onChange={(e) => handlePaymentDetailChange(field, e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -270,7 +273,8 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
step="0.01"
|
||||
value={minOrderAmount}
|
||||
onChange={e => setMinOrderAmount(e.target.value)}
|
||||
placeholder={`Min ${token} per trade`}
|
||||
placeholder="Minimum amount (optional)"
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -281,7 +285,8 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
||||
step="0.01"
|
||||
value={maxOrderAmount}
|
||||
onChange={e => setMaxOrderAmount(e.target.value)}
|
||||
placeholder={`Max ${token} per trade`}
|
||||
placeholder="Maximum amount (optional)"
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -114,8 +114,8 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
||||
step="0.01"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
placeholder={`Enter amount (max ${offer.remaining_amount})`}
|
||||
className="bg-gray-800 border-gray-700 text-white"
|
||||
placeholder="Amount"
|
||||
className="bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
{offer.min_order_amount && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
|
||||
@@ -96,42 +96,75 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
// Check active sessions and sets the user
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setUser(session?.user ?? null);
|
||||
if (session?.user) {
|
||||
checkAdminStatus();
|
||||
}
|
||||
checkAdminStatus(); // Check admin status regardless of Supabase session
|
||||
setLoading(false);
|
||||
}).catch(() => {
|
||||
// If Supabase is not available, continue without auth
|
||||
// If Supabase is not available, still check wallet-based admin
|
||||
checkAdminStatus();
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
// Listen for changes on auth state
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setUser(session?.user ?? null);
|
||||
if (session?.user) {
|
||||
checkAdminStatus();
|
||||
}
|
||||
checkAdminStatus(); // Check admin status on auth change
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
// Listen for wallet changes (from PolkadotContext)
|
||||
const handleWalletChange = () => {
|
||||
checkAdminStatus();
|
||||
};
|
||||
window.addEventListener('walletChanged', handleWalletChange);
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
window.removeEventListener('walletChanged', handleWalletChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const checkAdminStatus = async () => {
|
||||
// Admin wallet whitelist (blockchain-based auth)
|
||||
const ADMIN_WALLETS = [
|
||||
'5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', // Founder (original)
|
||||
'5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3', // Founder delegate (initial KYC member)
|
||||
'5GgTgG9sRmPQAYU1RsTejZYnZRjwzKZKWD3awtuqjHioki45', // Founder (current dev wallet)
|
||||
];
|
||||
|
||||
try {
|
||||
// PRIMARY: Check wallet-based admin (blockchain auth)
|
||||
const connectedWallet = localStorage.getItem('selectedWallet');
|
||||
console.log('🔍 Admin check - Connected wallet:', connectedWallet);
|
||||
console.log('🔍 Admin check - Whitelist:', ADMIN_WALLETS);
|
||||
|
||||
if (connectedWallet && ADMIN_WALLETS.includes(connectedWallet)) {
|
||||
console.log('✅ Admin access granted (wallet-based)');
|
||||
setIsAdmin(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// SECONDARY: Check Supabase admin_roles (if wallet not in whitelist)
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return false;
|
||||
if (user) {
|
||||
const { data, error } = await supabase
|
||||
.from('admin_roles')
|
||||
.select('role')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('admin_roles')
|
||||
.select('role')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
if (!error && data && ['admin', 'super_admin'].includes(data.role)) {
|
||||
console.log('✅ Admin access granted (Supabase-based)');
|
||||
setIsAdmin(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const adminStatus = !error && data && ['admin', 'super_admin'].includes(data.role);
|
||||
setIsAdmin(adminStatus);
|
||||
return adminStatus;
|
||||
} catch {
|
||||
console.log('❌ Admin access denied');
|
||||
setIsAdmin(false);
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.error('Admin check error:', err);
|
||||
setIsAdmin(false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -33,6 +33,19 @@ export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
|
||||
const [selectedAccount, setSelectedAccount] = useState<InjectedAccountWithMeta | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Wrapper to trigger events when wallet changes
|
||||
const handleSetSelectedAccount = (account: InjectedAccountWithMeta | null) => {
|
||||
setSelectedAccount(account);
|
||||
if (account) {
|
||||
localStorage.setItem('selectedWallet', account.address);
|
||||
console.log('💾 Wallet saved:', account.address);
|
||||
window.dispatchEvent(new Event('walletChanged'));
|
||||
} else {
|
||||
localStorage.removeItem('selectedWallet');
|
||||
window.dispatchEvent(new Event('walletChanged'));
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize Polkadot API
|
||||
useEffect(() => {
|
||||
const initApi = async () => {
|
||||
@@ -76,35 +89,73 @@ export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
|
||||
};
|
||||
}, [endpoint]);
|
||||
|
||||
// Auto-restore wallet on page load
|
||||
useEffect(() => {
|
||||
const restoreWallet = async () => {
|
||||
const savedAddress = localStorage.getItem('selectedWallet');
|
||||
if (!savedAddress) return;
|
||||
|
||||
try {
|
||||
// Enable extension
|
||||
const extensions = await web3Enable('PezkuwiChain');
|
||||
if (extensions.length === 0) return;
|
||||
|
||||
// Get accounts
|
||||
const allAccounts = await web3Accounts();
|
||||
if (allAccounts.length === 0) return;
|
||||
|
||||
// Find saved account
|
||||
const savedAccount = allAccounts.find(acc => acc.address === savedAddress);
|
||||
if (savedAccount) {
|
||||
setAccounts(allAccounts);
|
||||
handleSetSelectedAccount(savedAccount);
|
||||
console.log('✅ Wallet restored:', savedAddress.slice(0, 8) + '...');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to restore wallet:', err);
|
||||
}
|
||||
};
|
||||
|
||||
restoreWallet();
|
||||
}, []);
|
||||
|
||||
// Connect wallet (Polkadot.js extension)
|
||||
const connectWallet = async () => {
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
|
||||
// Enable extension
|
||||
const extensions = await web3Enable('PezkuwiChain');
|
||||
|
||||
|
||||
if (extensions.length === 0) {
|
||||
setError('Please install Polkadot.js extension');
|
||||
window.open('https://polkadot.js.org/extension/', '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log('✅ Polkadot.js extension enabled');
|
||||
|
||||
|
||||
// Get accounts
|
||||
const allAccounts = await web3Accounts();
|
||||
|
||||
|
||||
if (allAccounts.length === 0) {
|
||||
setError('No accounts found. Please create an account in Polkadot.js extension');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setAccounts(allAccounts);
|
||||
setSelectedAccount(allAccounts[0]); // Auto-select first account
|
||||
|
||||
|
||||
// Try to restore previously selected account, otherwise use first
|
||||
const savedAddress = localStorage.getItem('selectedWallet');
|
||||
const accountToSelect = savedAddress
|
||||
? allAccounts.find(acc => acc.address === savedAddress) || allAccounts[0]
|
||||
: allAccounts[0];
|
||||
|
||||
// Use wrapper to trigger events
|
||||
handleSetSelectedAccount(accountToSelect);
|
||||
|
||||
console.log(`✅ Found ${allAccounts.length} account(s)`);
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Wallet connection failed:', err);
|
||||
setError('Failed to connect wallet');
|
||||
@@ -114,7 +165,7 @@ export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
|
||||
// Disconnect wallet
|
||||
const disconnectWallet = () => {
|
||||
setAccounts([]);
|
||||
setSelectedAccount(null);
|
||||
handleSetSelectedAccount(null);
|
||||
console.log('🔌 Wallet disconnected');
|
||||
};
|
||||
|
||||
@@ -124,7 +175,7 @@ export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
|
||||
isConnected: isApiReady, // Alias for backward compatibility
|
||||
accounts,
|
||||
selectedAccount,
|
||||
setSelectedAccount,
|
||||
setSelectedAccount: handleSetSelectedAccount,
|
||||
connectWallet,
|
||||
disconnectWallet,
|
||||
error,
|
||||
|
||||
@@ -38,9 +38,10 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||
const connectionAttempts = useRef(0);
|
||||
|
||||
const ENDPOINTS = [
|
||||
'wss://ws.pezkuwichain.io', // Production WebSocket
|
||||
'ws://localhost:9944', // Local development node
|
||||
'ws://127.0.0.1:9944', // Alternative local address
|
||||
'ws://localhost:8082', // Local Vite dev server
|
||||
'ws://127.0.0.1:9944', // Local development node (primary)
|
||||
'ws://localhost:9944', // Local development node (alternative)
|
||||
'wss://ws.pezkuwichain.io', // Production WebSocket (fallback)
|
||||
];
|
||||
|
||||
const connect = useCallback((endpointIndex: number = 0) => {
|
||||
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
import { SessionMonitor } from '@/components/security/SessionMonitor';
|
||||
import { PermissionEditor } from '@/components/security/PermissionEditor';
|
||||
import { SecurityAudit } from '@/components/security/SecurityAudit';
|
||||
import { KycApprovalTab } from '@/components/admin/KycApprovalTab';
|
||||
import { CommissionVotingTab } from '@/components/admin/CommissionVotingTab';
|
||||
import { CommissionSetupTab } from '@/components/admin/CommissionSetupTab';
|
||||
|
||||
export default function AdminPanel() {
|
||||
const navigate = useNavigate();
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
@@ -149,38 +153,58 @@ export default function AdminPanel() {
|
||||
</button>
|
||||
<h1 className="text-3xl font-bold mb-8">Admin Panel</h1>
|
||||
|
||||
<Tabs defaultValue="users" className="space-y-4">
|
||||
<TabsList className="grid w-full grid-cols-7">
|
||||
<TabsTrigger value="users">
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Users
|
||||
<Tabs defaultValue="setup" className="space-y-4">
|
||||
<TabsList className="grid w-full grid-cols-10 h-auto">
|
||||
<TabsTrigger value="setup" className="flex-col h-auto py-3">
|
||||
<Shield className="h-4 w-4 mb-1" />
|
||||
<span className="text-xs leading-tight">Commission<br/>Setup</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="roles">
|
||||
<Shield className="mr-2 h-4 w-4" />
|
||||
Roles
|
||||
<TabsTrigger value="kyc" className="flex-col h-auto py-3">
|
||||
<Users className="h-4 w-4 mb-1" />
|
||||
<span className="text-xs leading-tight">KYC<br/>Approvals</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="sessions">
|
||||
<Monitor className="mr-2 h-4 w-4" />
|
||||
Sessions
|
||||
<TabsTrigger value="voting" className="flex-col h-auto py-3">
|
||||
<Activity className="h-4 w-4 mb-1" />
|
||||
<span className="text-xs leading-tight">Commission<br/>Voting</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="permissions">
|
||||
<Lock className="mr-2 h-4 w-4" />
|
||||
Permissions
|
||||
<TabsTrigger value="users" className="flex-col h-auto py-3">
|
||||
<Users className="h-4 w-4 mb-1" />
|
||||
<span className="text-xs leading-tight">Users</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="security">
|
||||
<AlertTriangle className="mr-2 h-4 w-4" />
|
||||
Security
|
||||
<TabsTrigger value="roles" className="flex-col h-auto py-3">
|
||||
<Shield className="h-4 w-4 mb-1" />
|
||||
<span className="text-xs leading-tight">Roles</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="activity">
|
||||
<Activity className="mr-2 h-4 w-4" />
|
||||
Activity
|
||||
<TabsTrigger value="sessions" className="flex-col h-auto py-3">
|
||||
<Monitor className="h-4 w-4 mb-1" />
|
||||
<span className="text-xs leading-tight">Sessions</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="settings">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Settings
|
||||
<TabsTrigger value="permissions" className="flex-col h-auto py-3">
|
||||
<Lock className="h-4 w-4 mb-1" />
|
||||
<span className="text-xs leading-tight">Permissions</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="security" className="flex-col h-auto py-3">
|
||||
<AlertTriangle className="h-4 w-4 mb-1" />
|
||||
<span className="text-xs leading-tight">Security</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="activity" className="flex-col h-auto py-3">
|
||||
<Activity className="h-4 w-4 mb-1" />
|
||||
<span className="text-xs leading-tight">Activity</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="settings" className="flex-col h-auto py-3">
|
||||
<Settings className="h-4 w-4 mb-1" />
|
||||
<span className="text-xs leading-tight">Settings</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="kyc">
|
||||
<KycApprovalTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="voting">
|
||||
<CommissionVotingTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="users">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -323,6 +347,9 @@ export default function AdminPanel() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="setup">
|
||||
<CommissionSetupTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
+87
-58
@@ -1,79 +1,101 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { CitizenshipModal } from '@/components/citizenship/CitizenshipModal';
|
||||
import { Shield, Users, Award, Globe, ChevronRight, ArrowLeft, Home } from 'lucide-react';
|
||||
import { InviteUserModal } from '@/components/referral/InviteUserModal';
|
||||
import { Shield, Users, Award, Globe, ChevronRight, ArrowLeft, UserPlus } from 'lucide-react';
|
||||
|
||||
const BeCitizen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
|
||||
const [referrerAddress, setReferrerAddress] = useState<string | null>(null);
|
||||
|
||||
// Check for referral parameter on mount
|
||||
useEffect(() => {
|
||||
const ref = searchParams.get('ref');
|
||||
if (ref) {
|
||||
setReferrerAddress(ref);
|
||||
// Auto-open modal if coming from referral link
|
||||
setIsModalOpen(true);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-indigo-900 to-blue-900">
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-700 via-white to-red-600">
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
{/* Back to Home Button */}
|
||||
<div className="mb-8">
|
||||
{/* Back to Home Button and Invite Friend */}
|
||||
<div className="mb-8 flex justify-between items-center">
|
||||
<Button
|
||||
onClick={() => navigate('/')}
|
||||
variant="outline"
|
||||
className="bg-white/10 hover:bg-white/20 border-white/30 text-white"
|
||||
className="bg-red-600 hover:bg-red-700 border-yellow-400 border-2 text-white font-semibold shadow-lg"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Home
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setIsInviteModalOpen(true)}
|
||||
className="bg-green-600 hover:bg-green-700 border-yellow-400 border-2 text-white font-semibold shadow-lg"
|
||||
>
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
Invite Friend
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Hero Section */}
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-5xl md:text-6xl font-bold text-white mb-6">
|
||||
<h1 className="text-5xl md:text-6xl font-bold text-red-700 mb-6 drop-shadow-lg">
|
||||
🏛️ Digital Kurdistan
|
||||
</h1>
|
||||
<h2 className="text-3xl md:text-4xl font-semibold text-cyan-300 mb-4">
|
||||
<h2 className="text-3xl md:text-4xl font-semibold text-green-700 mb-4 drop-shadow-lg">
|
||||
Bibe Welati / Be a Citizen
|
||||
</h2>
|
||||
<p className="text-xl text-gray-200 max-w-3xl mx-auto">
|
||||
<p className="text-xl text-gray-800 font-semibold max-w-3xl mx-auto drop-shadow-md">
|
||||
Join the Digital Kurdistan State as a sovereign citizen. Receive your Welati Tiki NFT and unlock governance, trust scoring, and community benefits.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Benefits Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
|
||||
<Card className="bg-white/10 backdrop-blur-md border-cyan-500/30 hover:border-cyan-500 transition-all">
|
||||
<Card className="bg-red-50/90 backdrop-blur-md border-red-600/50 hover:border-red-600 transition-all shadow-lg">
|
||||
<CardHeader>
|
||||
<Shield className="h-12 w-12 text-cyan-400 mb-3" />
|
||||
<CardTitle className="text-white">Privacy Protected</CardTitle>
|
||||
<CardDescription className="text-gray-300">
|
||||
<Shield className="h-12 w-12 text-red-600 mb-3" />
|
||||
<CardTitle className="text-red-700 font-bold">Privacy Protected</CardTitle>
|
||||
<CardDescription className="text-gray-700 font-medium">
|
||||
Your data is encrypted with ZK-proofs. Only hashes are stored on-chain.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/10 backdrop-blur-md border-purple-500/30 hover:border-purple-500 transition-all">
|
||||
<Card className="bg-yellow-50/90 backdrop-blur-md border-yellow-600/50 hover:border-yellow-600 transition-all shadow-lg">
|
||||
<CardHeader>
|
||||
<Award className="h-12 w-12 text-purple-400 mb-3" />
|
||||
<CardTitle className="text-white">Welati Tiki NFT</CardTitle>
|
||||
<CardDescription className="text-gray-300">
|
||||
<Award className="h-12 w-12 text-yellow-700 mb-3" />
|
||||
<CardTitle className="text-yellow-800 font-bold">Welati Tiki NFT</CardTitle>
|
||||
<CardDescription className="text-gray-700 font-medium">
|
||||
Receive your unique soulbound citizenship NFT after KYC approval.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/10 backdrop-blur-md border-green-500/30 hover:border-green-500 transition-all">
|
||||
<Card className="bg-green-50/90 backdrop-blur-md border-green-600/50 hover:border-green-600 transition-all shadow-lg">
|
||||
<CardHeader>
|
||||
<Users className="h-12 w-12 text-green-400 mb-3" />
|
||||
<CardTitle className="text-white">Trust Scoring</CardTitle>
|
||||
<CardDescription className="text-gray-300">
|
||||
<Users className="h-12 w-12 text-green-600 mb-3" />
|
||||
<CardTitle className="text-green-700 font-bold">Trust Scoring</CardTitle>
|
||||
<CardDescription className="text-gray-700 font-medium">
|
||||
Build trust through referrals, staking, and community contributions.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-white/10 backdrop-blur-md border-yellow-500/30 hover:border-yellow-500 transition-all">
|
||||
<Card className="bg-red-50/90 backdrop-blur-md border-red-600/50 hover:border-red-600 transition-all shadow-lg">
|
||||
<CardHeader>
|
||||
<Globe className="h-12 w-12 text-yellow-400 mb-3" />
|
||||
<CardTitle className="text-white">Governance Access</CardTitle>
|
||||
<CardDescription className="text-gray-300">
|
||||
<Globe className="h-12 w-12 text-red-600 mb-3" />
|
||||
<CardTitle className="text-red-700 font-bold">Governance Access</CardTitle>
|
||||
<CardDescription className="text-gray-700 font-medium">
|
||||
Participate in on-chain governance and shape the future of Digital Kurdistan.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
@@ -82,12 +104,12 @@ const BeCitizen: React.FC = () => {
|
||||
|
||||
{/* CTA Section */}
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Card className="bg-white/5 backdrop-blur-lg border-cyan-500/50">
|
||||
<Card className="bg-gradient-to-r from-yellow-400 via-yellow-300 to-yellow-400 backdrop-blur-lg border-red-600 border-4 shadow-2xl">
|
||||
<CardContent className="pt-8 pb-8">
|
||||
<div className="text-center space-y-6">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-white mb-3">Ready to Join?</h3>
|
||||
<p className="text-gray-300 mb-6">
|
||||
<h3 className="text-2xl font-bold text-red-700 mb-3">Ready to Join?</h3>
|
||||
<p className="text-gray-800 font-medium mb-6">
|
||||
Whether you're already a citizen or want to become one, start your journey here.
|
||||
</p>
|
||||
</div>
|
||||
@@ -95,25 +117,25 @@ const BeCitizen: React.FC = () => {
|
||||
<Button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
size="lg"
|
||||
className="bg-gradient-to-r from-cyan-500 to-purple-600 hover:from-cyan-600 hover:to-purple-700 text-white font-semibold px-8 py-6 text-lg group"
|
||||
className="bg-gradient-to-r from-red-600 to-green-700 hover:from-red-700 hover:to-green-800 text-white font-bold px-8 py-6 text-lg group shadow-xl border-2 border-yellow-300"
|
||||
>
|
||||
<span>Start Citizenship Process</span>
|
||||
<ChevronRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4 justify-center items-center text-sm text-gray-400 pt-4">
|
||||
<div className="flex flex-col md:flex-row gap-4 justify-center items-center text-sm text-gray-700 font-medium pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="h-4 w-4" />
|
||||
<Shield className="h-4 w-4 text-green-700" />
|
||||
<span>Secure ZK-Proof Authentication</span>
|
||||
</div>
|
||||
<div className="hidden md:block">•</div>
|
||||
<div className="hidden md:block text-red-600">•</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Award className="h-4 w-4" />
|
||||
<Award className="h-4 w-4 text-red-600" />
|
||||
<span>Soulbound NFT Citizenship</span>
|
||||
</div>
|
||||
<div className="hidden md:block">•</div>
|
||||
<div className="hidden md:block text-red-600">•</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="h-4 w-4" />
|
||||
<Globe className="h-4 w-4 text-green-700" />
|
||||
<span>Decentralized Identity</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,18 +146,18 @@ const BeCitizen: React.FC = () => {
|
||||
|
||||
{/* Process Overview */}
|
||||
<div className="mt-16 max-w-5xl mx-auto">
|
||||
<h3 className="text-3xl font-bold text-white text-center mb-8">How It Works</h3>
|
||||
<h3 className="text-3xl font-bold text-red-700 text-center mb-8 drop-shadow-lg">How It Works</h3>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{/* Existing Citizens */}
|
||||
<Card className="bg-white/5 backdrop-blur-md border-cyan-500/30">
|
||||
<Card className="bg-red-50/90 backdrop-blur-md border-red-600/50 shadow-lg">
|
||||
<CardHeader>
|
||||
<div className="bg-cyan-500/20 w-12 h-12 rounded-full flex items-center justify-center mb-4">
|
||||
<span className="text-2xl font-bold text-cyan-400">1</span>
|
||||
<div className="bg-red-600 w-12 h-12 rounded-full flex items-center justify-center mb-4">
|
||||
<span className="text-2xl font-bold text-white">1</span>
|
||||
</div>
|
||||
<CardTitle className="text-white">Already a Citizen?</CardTitle>
|
||||
<CardTitle className="text-red-700 font-bold">Already a Citizen?</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-gray-300 space-y-2 text-sm">
|
||||
<CardContent className="text-gray-700 font-medium space-y-2 text-sm">
|
||||
<p>✓ Enter your Welati Tiki NFT number</p>
|
||||
<p>✓ Verify NFT ownership on-chain</p>
|
||||
<p>✓ Sign authentication challenge</p>
|
||||
@@ -144,14 +166,14 @@ const BeCitizen: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
{/* New Citizens */}
|
||||
<Card className="bg-white/5 backdrop-blur-md border-purple-500/30">
|
||||
<Card className="bg-yellow-50/90 backdrop-blur-md border-yellow-600/50 shadow-lg">
|
||||
<CardHeader>
|
||||
<div className="bg-purple-500/20 w-12 h-12 rounded-full flex items-center justify-center mb-4">
|
||||
<span className="text-2xl font-bold text-purple-400">2</span>
|
||||
<div className="bg-yellow-600 w-12 h-12 rounded-full flex items-center justify-center mb-4">
|
||||
<span className="text-2xl font-bold text-white">2</span>
|
||||
</div>
|
||||
<CardTitle className="text-white">New to Citizenship?</CardTitle>
|
||||
<CardTitle className="text-yellow-800 font-bold">New to Citizenship?</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-gray-300 space-y-2 text-sm">
|
||||
<CardContent className="text-gray-700 font-medium space-y-2 text-sm">
|
||||
<p>✓ Fill detailed KYC application</p>
|
||||
<p>✓ Data encrypted with ZK-proofs</p>
|
||||
<p>✓ Submit for admin approval</p>
|
||||
@@ -160,14 +182,14 @@ const BeCitizen: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
{/* After Citizenship */}
|
||||
<Card className="bg-white/5 backdrop-blur-md border-green-500/30">
|
||||
<Card className="bg-green-50/90 backdrop-blur-md border-green-600/50 shadow-lg">
|
||||
<CardHeader>
|
||||
<div className="bg-green-500/20 w-12 h-12 rounded-full flex items-center justify-center mb-4">
|
||||
<span className="text-2xl font-bold text-green-400">3</span>
|
||||
<div className="bg-green-600 w-12 h-12 rounded-full flex items-center justify-center mb-4">
|
||||
<span className="text-2xl font-bold text-white">3</span>
|
||||
</div>
|
||||
<CardTitle className="text-white">Citizen Benefits</CardTitle>
|
||||
<CardTitle className="text-green-700 font-bold">Citizen Benefits</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-gray-300 space-y-2 text-sm">
|
||||
<CardContent className="text-gray-700 font-medium space-y-2 text-sm">
|
||||
<p>✓ Trust score calculation enabled</p>
|
||||
<p>✓ Governance voting rights</p>
|
||||
<p>✓ Referral tree participation</p>
|
||||
@@ -179,13 +201,13 @@ const BeCitizen: React.FC = () => {
|
||||
|
||||
{/* Security Notice */}
|
||||
<div className="mt-12 max-w-3xl mx-auto">
|
||||
<Card className="bg-yellow-500/10 border-yellow-500/30">
|
||||
<Card className="bg-yellow-50/90 border-yellow-600/50 shadow-lg">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<Shield className="h-6 w-6 text-yellow-400 mt-1 flex-shrink-0" />
|
||||
<div className="text-sm text-gray-200">
|
||||
<p className="font-semibold text-yellow-400 mb-2">Privacy & Security</p>
|
||||
<p>
|
||||
<Shield className="h-6 w-6 text-yellow-700 mt-1 flex-shrink-0" />
|
||||
<div className="text-sm text-gray-700">
|
||||
<p className="font-bold text-yellow-800 mb-2">Privacy & Security</p>
|
||||
<p className="font-medium">
|
||||
Your personal data is encrypted using AES-GCM with your wallet-derived keys.
|
||||
Only commitment hashes are stored on the blockchain. Encrypted data is stored
|
||||
on IPFS and locally on your device. No personal information is ever publicly visible.
|
||||
@@ -198,7 +220,14 @@ const BeCitizen: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Citizenship Modal */}
|
||||
<CitizenshipModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
|
||||
<CitizenshipModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
referrerAddress={referrerAddress}
|
||||
/>
|
||||
|
||||
{/* Invite Friend Modal */}
|
||||
<InviteUserModal isOpen={isInviteModalOpen} onClose={() => setIsInviteModalOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+254
-3
@@ -7,10 +7,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { User, Mail, Phone, Globe, MapPin, Calendar, Shield, AlertCircle, ArrowLeft, Award, Users, TrendingUp } from 'lucide-react';
|
||||
import { User, Mail, Phone, Globe, MapPin, Calendar, Shield, AlertCircle, ArrowLeft, Award, Users, TrendingUp, UserMinus } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { fetchUserTikis, calculateTikiScore, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories } from '@pezkuwi/lib/tiki';
|
||||
import { fetchUserTikis, calculateTikiScore, getPrimaryRole, getTikiDisplayName, getTikiColor, getTikiEmoji, getUserRoleCategories, getAllTikiNFTDetails, generateCitizenNumber, type TikiNFTDetails } from '@pezkuwi/lib/tiki';
|
||||
import { getAllScores, type UserScores } from '@pezkuwi/lib/scores';
|
||||
import { getKycStatus } from '@pezkuwi/lib/kyc';
|
||||
import { ReferralDashboard } from '@/components/referral/ReferralDashboard';
|
||||
// Commission proposals card removed - no longer using notary system for KYC approval
|
||||
// import { CommissionProposalsCard } from '@/components/dashboard/CommissionProposalsCard';
|
||||
|
||||
export default function Dashboard() {
|
||||
const { user } = useAuth();
|
||||
@@ -28,6 +32,13 @@ export default function Dashboard() {
|
||||
totalScore: 0
|
||||
});
|
||||
const [loadingScores, setLoadingScores] = useState(false);
|
||||
const [kycStatus, setKycStatus] = useState<string>('NotStarted');
|
||||
const [renouncingCitizenship, setRenouncingCitizenship] = useState(false);
|
||||
const [nftDetails, setNftDetails] = useState<{ citizenNFT: TikiNFTDetails | null; roleNFTs: TikiNFTDetails[]; totalNFTs: number }>({
|
||||
citizenNFT: null,
|
||||
roleNFTs: [],
|
||||
totalNFTs: 0
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchProfile();
|
||||
@@ -107,6 +118,14 @@ export default function Dashboard() {
|
||||
// Also fetch tikis separately for role display (needed for role details)
|
||||
const userTikis = await fetchUserTikis(api, selectedAccount.address);
|
||||
setTikis(userTikis);
|
||||
|
||||
// Fetch NFT details including collection/item IDs
|
||||
const details = await getAllTikiNFTDetails(api, selectedAccount.address);
|
||||
setNftDetails(details);
|
||||
|
||||
// Fetch KYC status to determine if user is a citizen
|
||||
const status = await getKycStatus(api, selectedAccount.address);
|
||||
setKycStatus(status);
|
||||
} catch (error) {
|
||||
console.error('Error fetching scores and tikis:', error);
|
||||
} finally {
|
||||
@@ -180,6 +199,98 @@ export default function Dashboard() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenounceCitizenship = async () => {
|
||||
if (!api || !selectedAccount) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Please connect your wallet first",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (kycStatus !== 'Approved') {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Only citizens can renounce citizenship",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm action
|
||||
const confirmed = window.confirm(
|
||||
'Are you sure you want to renounce your citizenship? This will:\n' +
|
||||
'• Burn your Citizen (Welati) NFT\n' +
|
||||
'• Reset your KYC status to NotStarted\n' +
|
||||
'• Remove all associated citizen privileges\n\n' +
|
||||
'You can always reapply later if you change your mind.'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
setRenouncingCitizenship(true);
|
||||
try {
|
||||
const { web3FromAddress } = await import('@polkadot/extension-dapp');
|
||||
const injector = await web3FromAddress(selectedAccount.address);
|
||||
|
||||
console.log('Renouncing citizenship...');
|
||||
|
||||
const tx = api.tx.identityKyc.renounceCitizenship();
|
||||
|
||||
await tx.signAndSend(selectedAccount.address, { signer: injector.signer }, ({ status, events, dispatchError }) => {
|
||||
if (dispatchError) {
|
||||
let errorMessage = 'Transaction failed';
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
||||
} else {
|
||||
errorMessage = dispatchError.toString();
|
||||
}
|
||||
console.error(errorMessage);
|
||||
toast({
|
||||
title: "Renunciation Failed",
|
||||
description: errorMessage,
|
||||
variant: "destructive"
|
||||
});
|
||||
setRenouncingCitizenship(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.isInBlock || status.isFinalized) {
|
||||
console.log('✅ Citizenship renounced successfully');
|
||||
|
||||
// Check for CitizenshipRenounced event
|
||||
events.forEach(({ event }) => {
|
||||
if (event.section === 'identityKyc' && event.method === 'CitizenshipRenounced') {
|
||||
console.log('📢 CitizenshipRenounced event detected');
|
||||
toast({
|
||||
title: "Citizenship Renounced",
|
||||
description: "Your citizenship has been successfully renounced. You can reapply anytime."
|
||||
});
|
||||
|
||||
// Refresh data after a short delay
|
||||
setTimeout(() => {
|
||||
fetchScoresAndTikis();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
setRenouncingCitizenship(false);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Renunciation error:', err);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: err.message || 'Failed to renounce citizenship',
|
||||
variant: "destructive"
|
||||
});
|
||||
setRenouncingCitizenship(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getRoleDisplay = (): string => {
|
||||
if (loadingScores) return 'Loading...';
|
||||
if (!selectedAccount) return 'Member';
|
||||
@@ -342,6 +453,7 @@ export default function Dashboard() {
|
||||
<TabsList>
|
||||
<TabsTrigger value="profile">Profile</TabsTrigger>
|
||||
<TabsTrigger value="roles">Roles & Tikis</TabsTrigger>
|
||||
<TabsTrigger value="referrals">Referrals</TabsTrigger>
|
||||
<TabsTrigger value="security">Security</TabsTrigger>
|
||||
<TabsTrigger value="activity">Activity</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -436,7 +548,7 @@ export default function Dashboard() {
|
||||
No roles assigned yet
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Complete KYC to become a Citizen (Hemwelatî)
|
||||
Complete KYC to become a Citizen (Welati)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -475,18 +587,157 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{nftDetails.totalNFTs > 0 && (
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="font-medium mb-3">NFT Details ({nftDetails.totalNFTs})</h4>
|
||||
<div className="space-y-3">
|
||||
{nftDetails.citizenNFT && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium text-blue-900 dark:text-blue-100">
|
||||
{nftDetails.citizenNFT.tikiEmoji} Citizen NFT
|
||||
</span>
|
||||
<Badge variant="outline" className="text-blue-700 border-blue-300">
|
||||
Primary
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* NFT Number and Citizen Number - Side by Side */}
|
||||
<div className="grid grid-cols-2 gap-3 mb-3">
|
||||
{/* NFT Number */}
|
||||
<div className="p-2 bg-white dark:bg-blue-950 rounded border border-blue-300 dark:border-blue-700">
|
||||
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium">NFT Number:</span>
|
||||
<div className="font-mono text-lg font-bold text-blue-900 dark:text-blue-100">
|
||||
#{nftDetails.citizenNFT.collectionId}-{nftDetails.citizenNFT.itemId}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Citizen Number = NFT Number + 6 digits */}
|
||||
<div className="p-2 bg-white dark:bg-green-950 rounded border border-green-300 dark:border-green-700">
|
||||
<span className="text-xs text-green-600 dark:text-green-400 font-medium">Citizen Number:</span>
|
||||
<div className="font-mono text-lg font-bold text-green-900 dark:text-green-100">
|
||||
#{nftDetails.citizenNFT.collectionId}-{nftDetails.citizenNFT.itemId}-{generateCitizenNumber(
|
||||
nftDetails.citizenNFT.owner,
|
||||
nftDetails.citizenNFT.collectionId,
|
||||
nftDetails.citizenNFT.itemId
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="text-blue-700 dark:text-blue-300 font-medium">Collection ID:</span>
|
||||
<span className="ml-2 font-mono text-blue-900 dark:text-blue-100">
|
||||
{nftDetails.citizenNFT.collectionId}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-blue-700 dark:text-blue-300 font-medium">Item ID:</span>
|
||||
<span className="ml-2 font-mono text-blue-900 dark:text-blue-100">
|
||||
{nftDetails.citizenNFT.itemId}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="text-blue-700 dark:text-blue-300 font-medium">Role:</span>
|
||||
<span className="ml-2 text-blue-900 dark:text-blue-100">
|
||||
{nftDetails.citizenNFT.tikiDisplayName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="text-blue-700 dark:text-blue-300 font-medium">Tiki Type:</span>
|
||||
<span className="ml-2 font-semibold text-purple-600 dark:text-purple-400">
|
||||
{nftDetails.citizenNFT.tikiRole}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nftDetails.roleNFTs.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground font-medium">Additional Role NFTs:</p>
|
||||
{nftDetails.roleNFTs.map((nft, index) => (
|
||||
<div
|
||||
key={`${nft.collectionId}-${nft.itemId}`}
|
||||
className="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium">
|
||||
{nft.tikiEmoji} {nft.tikiDisplayName}
|
||||
</span>
|
||||
<Badge variant="outline" className={nft.tikiColor}>
|
||||
Score: {nft.tikiScore}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm text-muted-foreground">
|
||||
<div>
|
||||
<span className="font-medium">Collection:</span>
|
||||
<span className="ml-2 font-mono">{nft.collectionId}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Item:</span>
|
||||
<span className="ml-2 font-mono">{nft.itemId}</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<span className="font-medium">Tiki Type:</span>
|
||||
<span className="ml-2 font-semibold text-purple-600 dark:text-purple-400">{nft.tikiRole}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t pt-4 bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||
<h4 className="font-medium mb-2">Blockchain Address</h4>
|
||||
<p className="text-sm text-muted-foreground font-mono break-all">
|
||||
{selectedAccount.address}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{kycStatus === 'Approved' && (
|
||||
<div className="border-t pt-4">
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
||||
<h4 className="font-medium mb-2 text-yellow-800 dark:text-yellow-200 flex items-center gap-2">
|
||||
<UserMinus className="h-4 w-4" />
|
||||
Renounce Citizenship
|
||||
</h4>
|
||||
<p className="text-sm text-yellow-700 dark:text-yellow-300 mb-3">
|
||||
You can voluntarily renounce your citizenship at any time. This will:
|
||||
</p>
|
||||
<ul className="text-sm text-yellow-700 dark:text-yellow-300 mb-3 list-disc list-inside space-y-1">
|
||||
<li>Burn your Citizen (Welati) NFT</li>
|
||||
<li>Reset your KYC status</li>
|
||||
<li>Remove citizen privileges</li>
|
||||
</ul>
|
||||
<p className="text-xs text-yellow-600 dark:text-yellow-400 mb-3">
|
||||
Note: You can always reapply for citizenship later if you change your mind.
|
||||
</p>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={handleRenounceCitizenship}
|
||||
disabled={renouncingCitizenship}
|
||||
>
|
||||
{renouncingCitizenship ? 'Renouncing...' : 'Renounce Citizenship'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="referrals" className="space-y-4">
|
||||
<ReferralDashboard />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="security" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -320,8 +320,8 @@ const Login: React.FC = () => {
|
||||
<Input
|
||||
id="referral-code"
|
||||
type="text"
|
||||
placeholder={t('login.enterReferralCode', 'Enter referral code')}
|
||||
className="pl-10 bg-gray-800 border-gray-700 text-white"
|
||||
placeholder={t('login.enterReferralCode', 'Referral code (optional)')}
|
||||
className="pl-10 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 placeholder:opacity-50"
|
||||
value={signupData.referralCode}
|
||||
onChange={(e) => setSignupData({...signupData, referralCode: e.target.value})}
|
||||
/>
|
||||
|
||||
@@ -163,6 +163,13 @@ export default function CitizensIssues() {
|
||||
if (!api || !isApiReady) return;
|
||||
|
||||
try {
|
||||
// Check if welati pallet exists
|
||||
if (!api.query.welati) {
|
||||
console.log('Welati pallet not available yet');
|
||||
setIssues([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const issueCountResult = await api.query.welati.issueCount();
|
||||
const issueCount = issueCountResult.toNumber();
|
||||
|
||||
@@ -188,11 +195,7 @@ export default function CitizensIssues() {
|
||||
setIssues(fetchedIssues.reverse());
|
||||
} catch (error) {
|
||||
console.error('Error fetching issues:', error);
|
||||
toast({
|
||||
title: 'Xeletî (Error)',
|
||||
description: 'Pirsgirêk di barkirina pirsan de (Error loading issues)',
|
||||
variant: 'destructive'
|
||||
});
|
||||
setIssues([]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -200,6 +203,13 @@ export default function CitizensIssues() {
|
||||
if (!api || !isApiReady || !selectedAccount) return;
|
||||
|
||||
try {
|
||||
// Check if welati pallet exists
|
||||
if (!api.query.welati) {
|
||||
console.log('Welati pallet not available yet');
|
||||
setUserVotes(new Map());
|
||||
return;
|
||||
}
|
||||
|
||||
const votesEntries = await api.query.welati.issueVotes.entries(selectedAccount.address);
|
||||
const votes = new Map<number, boolean>();
|
||||
|
||||
@@ -212,6 +222,7 @@ export default function CitizensIssues() {
|
||||
setUserVotes(votes);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user votes:', error);
|
||||
setUserVotes(new Map());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -304,6 +315,13 @@ export default function CitizensIssues() {
|
||||
if (!api || !isApiReady) return;
|
||||
|
||||
try {
|
||||
// Check if welati pallet exists
|
||||
if (!api.query.welati) {
|
||||
console.log('Welati pallet not available yet');
|
||||
setParliamentCandidates([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const candidatesEntries = await api.query.welati.parliamentCandidates.entries();
|
||||
const candidates: ParliamentCandidate[] = [];
|
||||
|
||||
@@ -333,6 +351,7 @@ export default function CitizensIssues() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching parliament candidates:', error);
|
||||
setParliamentCandidates([]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -450,6 +469,13 @@ export default function CitizensIssues() {
|
||||
if (!api || !isApiReady) return;
|
||||
|
||||
try {
|
||||
// Check if welati pallet exists
|
||||
if (!api.query.welati) {
|
||||
console.log('Welati pallet not available yet');
|
||||
setPresidentCandidates([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const candidatesEntries = await api.query.welati.presidentCandidates.entries();
|
||||
const candidates: PresidentCandidate[] = [];
|
||||
|
||||
@@ -479,6 +505,7 @@ export default function CitizensIssues() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching president candidates:', error);
|
||||
setPresidentCandidates([]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -596,6 +623,14 @@ export default function CitizensIssues() {
|
||||
if (!api || !isApiReady) return;
|
||||
|
||||
try {
|
||||
// Check if welati pallet exists
|
||||
if (!api.query.welati) {
|
||||
console.log('Welati pallet not available yet');
|
||||
setLegislationProposals([]);
|
||||
setUserLegislationVotes(new Map());
|
||||
return;
|
||||
}
|
||||
|
||||
const proposalsEntries = await api.query.welati.legislationProposals.entries();
|
||||
const proposals: LegislationProposal[] = [];
|
||||
|
||||
@@ -631,6 +666,8 @@ export default function CitizensIssues() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching legislation proposals:', error);
|
||||
setLegislationProposals([]);
|
||||
setUserLegislationVotes(new Map());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1085,9 +1122,10 @@ export default function CitizensIssues() {
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<Input
|
||||
placeholder="5Abc123... (Substrate Address)"
|
||||
placeholder="Candidate address"
|
||||
value={nominateParliamentAddress}
|
||||
onChange={(e) => setNominateParliamentAddress(e.target.value)}
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleNominateParliament}
|
||||
@@ -1196,9 +1234,10 @@ export default function CitizensIssues() {
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<Input
|
||||
placeholder="5Abc123... (Substrate Address)"
|
||||
placeholder="Candidate address"
|
||||
value={nominatePresidentAddress}
|
||||
onChange={(e) => setNominatePresidentAddress(e.target.value)}
|
||||
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleNominatePresident}
|
||||
|
||||
Reference in New Issue
Block a user