mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-25 16:28:02 +00:00
refactor(core): Apply various updates and fixes across components
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user