mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-13 09:01:00 +00:00
refactor(core): Apply various updates and fixes across components
This commit is contained in:
@@ -78,8 +78,9 @@ export async function checkValidatorStatus(
|
|||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
// Tiki role enum mapping (from pallet-tiki)
|
// Tiki role enum mapping (from pallet-tiki)
|
||||||
|
// IMPORTANT: Must match /Pezkuwi-SDK/pezkuwi/pallets/tiki/src/lib.rs
|
||||||
const TIKI_ROLES = [
|
const TIKI_ROLES = [
|
||||||
'Hemwelatî', // 0 - Citizen
|
'Welati', // 0 - Citizen
|
||||||
'Parlementer', // 1 - Parliament Member
|
'Parlementer', // 1 - Parliament Member
|
||||||
'SerokiMeclise', // 2 - Speaker of Parliament
|
'SerokiMeclise', // 2 - Speaker of Parliament
|
||||||
'Serok', // 3 - President
|
'Serok', // 3 - President
|
||||||
@@ -127,7 +128,7 @@ const TIKI_ROLES = [
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if user has specific Tiki role
|
* Check if user has specific Tiki role
|
||||||
* @param role - Kurdish name of role (e.g., 'Hemwelatî', 'Perwerdekar')
|
* @param role - Kurdish name of role (e.g., 'Welati', 'Perwerdekar')
|
||||||
*/
|
*/
|
||||||
export async function checkTikiRole(
|
export async function checkTikiRole(
|
||||||
api: ApiPromise | null,
|
api: ApiPromise | null,
|
||||||
|
|||||||
+5
-2
@@ -24,6 +24,7 @@ import { WebSocketProvider } from '@/contexts/WebSocketContext';
|
|||||||
import { IdentityProvider } from '@/contexts/IdentityContext';
|
import { IdentityProvider } from '@/contexts/IdentityContext';
|
||||||
import { AuthProvider } from '@/contexts/AuthContext';
|
import { AuthProvider } from '@/contexts/AuthContext';
|
||||||
import { DashboardProvider } from '@/contexts/DashboardContext';
|
import { DashboardProvider } from '@/contexts/DashboardContext';
|
||||||
|
import { ReferralProvider } from '@/contexts/ReferralContext';
|
||||||
import { ProtectedRoute } from '@/components/ProtectedRoute';
|
import { ProtectedRoute } from '@/components/ProtectedRoute';
|
||||||
import NotFound from '@/pages/NotFound';
|
import NotFound from '@/pages/NotFound';
|
||||||
import { Toaster } from '@/components/ui/toaster';
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
@@ -42,7 +43,8 @@ function App() {
|
|||||||
<WebSocketProvider>
|
<WebSocketProvider>
|
||||||
<IdentityProvider>
|
<IdentityProvider>
|
||||||
<DashboardProvider>
|
<DashboardProvider>
|
||||||
<Router>
|
<ReferralProvider>
|
||||||
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
|
||||||
@@ -100,7 +102,8 @@ function App() {
|
|||||||
} />
|
} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
</ReferralProvider>
|
||||||
</DashboardProvider>
|
</DashboardProvider>
|
||||||
</IdentityProvider>
|
</IdentityProvider>
|
||||||
</WebSocketProvider>
|
</WebSocketProvider>
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import type { TikiInfo } from '@pezkuwi/lib/citizenship-workflow';
|
|||||||
const getTikiIcon = (role: string) => {
|
const getTikiIcon = (role: string) => {
|
||||||
const roleLower = role.toLowerCase();
|
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" />;
|
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" />;
|
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 <Award className="w-6 h-6 text-purple-500" />;
|
||||||
}
|
}
|
||||||
return <Users className="w-6 h-6 text-green-500" />;
|
return <Users className="w-6 h-6 text-green-500" />;
|
||||||
@@ -26,13 +26,13 @@ const getTikiIcon = (role: string) => {
|
|||||||
const getRoleBadgeColor = (role: string) => {
|
const getRoleBadgeColor = (role: string) => {
|
||||||
const roleLower = role.toLowerCase();
|
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';
|
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';
|
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-purple-500/10 text-purple-500 border-purple-500/30';
|
||||||
}
|
}
|
||||||
return 'bg-green-500/10 text-green-500 border-green-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}
|
Tiki #{tiki.id}
|
||||||
</h3>
|
</h3>
|
||||||
<Badge className={getRoleBadgeColor(tiki.role)}>
|
<Badge className={getRoleBadgeColor(tiki.role)}>
|
||||||
{tiki.role === 'Hemwelatî' ? 'Welati' : tiki.role}
|
{tiki.role}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
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 {
|
interface ProtectedRouteProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -13,11 +15,71 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|||||||
requireAdmin = false
|
requireAdmin = false
|
||||||
}) => {
|
}) => {
|
||||||
const { user, loading, isAdmin } = useAuth();
|
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 (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -27,7 +89,20 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (requireAdmin && !isAdmin) {
|
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}</>;
|
return <>{children}</>;
|
||||||
|
|||||||
@@ -863,8 +863,8 @@ const TokenSwap = () => {
|
|||||||
type="number"
|
type="number"
|
||||||
value={fromAmount}
|
value={fromAmount}
|
||||||
onChange={(e) => setFromAmount(e.target.value)}
|
onChange={(e) => setFromAmount(e.target.value)}
|
||||||
placeholder="0.0"
|
placeholder="Amount"
|
||||||
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-600"
|
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-500 placeholder:opacity-50"
|
||||||
disabled={!selectedAccount}
|
disabled={!selectedAccount}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
@@ -934,8 +934,8 @@ const TokenSwap = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
value={toAmount}
|
value={toAmount}
|
||||||
readOnly
|
readOnly
|
||||||
placeholder="0.0"
|
placeholder="Amount"
|
||||||
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-600"
|
className="text-2xl font-bold border-0 bg-transparent text-white placeholder:text-gray-500 placeholder:opacity-50"
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={toToken}
|
value={toToken}
|
||||||
|
|||||||
@@ -269,8 +269,8 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
|||||||
id="recipient"
|
id="recipient"
|
||||||
value={recipient}
|
value={recipient}
|
||||||
onChange={(e) => setRecipient(e.target.value)}
|
onChange={(e) => setRecipient(e.target.value)}
|
||||||
placeholder="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
|
placeholder="Recipient address"
|
||||||
className="bg-gray-800 border-gray-700 text-white mt-2"
|
className="bg-gray-800 border-gray-700 text-white mt-2 placeholder:text-gray-500 placeholder:opacity-50"
|
||||||
disabled={isTransferring}
|
disabled={isTransferring}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,8 +283,8 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose, s
|
|||||||
step={selectedToken === 'HEZ' || selectedToken === 'PEZ' ? '0.0001' : '0.000001'}
|
step={selectedToken === 'HEZ' || selectedToken === 'PEZ' ? '0.0001' : '0.000001'}
|
||||||
value={amount}
|
value={amount}
|
||||||
onChange={(e) => setAmount(e.target.value)}
|
onChange={(e) => setAmount(e.target.value)}
|
||||||
placeholder="0.0000"
|
placeholder="Amount"
|
||||||
className="bg-gray-800 border-gray-700 text-white mt-2"
|
className="bg-gray-800 border-gray-700 text-white mt-2 placeholder:text-gray-500 placeholder:opacity-50"
|
||||||
disabled={isTransferring}
|
disabled={isTransferring}
|
||||||
/>
|
/>
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
|||||||
@@ -214,8 +214,8 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
|||||||
type="number"
|
type="number"
|
||||||
value={depositAmount}
|
value={depositAmount}
|
||||||
onChange={(e) => setDepositAmount(e.target.value)}
|
onChange={(e) => setDepositAmount(e.target.value)}
|
||||||
placeholder="0.00"
|
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"
|
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}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -279,9 +279,9 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
|||||||
type="number"
|
type="number"
|
||||||
value={withdrawAmount}
|
value={withdrawAmount}
|
||||||
onChange={(e) => setWithdrawAmount(e.target.value)}
|
onChange={(e) => setWithdrawAmount(e.target.value)}
|
||||||
placeholder="0.00"
|
placeholder="Amount"
|
||||||
max={wusdtBalance}
|
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}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -300,8 +300,8 @@ export const USDTBridge: React.FC<USDTBridgeProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
value={withdrawAddress}
|
value={withdrawAddress}
|
||||||
onChange={(e) => setWithdrawAddress(e.target.value)}
|
onChange={(e) => setWithdrawAddress(e.target.value)}
|
||||||
placeholder="Enter bank account or crypto address"
|
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"
|
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}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -391,10 +391,10 @@ export function CommissionSetupTab() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<textarea
|
<textarea
|
||||||
placeholder="Paste addresses, one per line Example: 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty 5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y"
|
placeholder="Member addresses, one per line"
|
||||||
value={newMemberAddress}
|
value={newMemberAddress}
|
||||||
onChange={(e) => setNewMemberAddress(e.target.value)}
|
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
|
<Button
|
||||||
onClick={handleAddMember}
|
onClick={handleAddMember}
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ import { NewCitizenApplication } from './NewCitizenApplication';
|
|||||||
interface CitizenshipModalProps {
|
interface CitizenshipModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
referrerAddress?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onClose }) => {
|
export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onClose, referrerAddress }) => {
|
||||||
const [activeTab, setActiveTab] = useState<'existing' | 'new'>('existing');
|
const [activeTab, setActiveTab] = useState<'existing' | 'new'>(referrerAddress ? 'new' : 'existing');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
@@ -26,7 +27,9 @@ export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onCl
|
|||||||
🏛️ Digital Kurdistan Citizenship
|
🏛️ Digital Kurdistan Citizenship
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<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>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -41,7 +44,7 @@ export const CitizenshipModal: React.FC<CitizenshipModalProps> = ({ isOpen, onCl
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="new" className="mt-6">
|
<TabsContent value="new" className="mt-6">
|
||||||
<NewCitizenApplication onClose={onClose} />
|
<NewCitizenApplication onClose={onClose} referrerAddress={referrerAddress} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Loader2, CheckCircle, AlertTriangle, Shield } from 'lucide-react';
|
import { Loader2, CheckCircle, AlertTriangle, Shield } from 'lucide-react';
|
||||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
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 { generateAuthChallenge, signChallenge, verifySignature, saveCitizenSession } from '@pezkuwi/lib/citizenship-workflow';
|
||||||
import type { AuthChallenge } 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 }) => {
|
export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClose }) => {
|
||||||
const { api, isApiReady, selectedAccount, connectWallet } = usePolkadot();
|
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 [step, setStep] = useState<'input' | 'verifying' | 'signing' | 'success' | 'error'>('input');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [challenge, setChallenge] = useState<AuthChallenge | null>(null);
|
const [challenge, setChallenge] = useState<AuthChallenge | null>(null);
|
||||||
@@ -28,8 +28,8 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tikiNumber.trim()) {
|
if (!citizenNumber.trim()) {
|
||||||
setError('Please enter your Welati Tiki NFT number');
|
setError('Please enter your Citizen Number');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,22 +37,22 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
|||||||
setStep('verifying');
|
setStep('verifying');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify NFT ownership
|
// Verify Citizen Number
|
||||||
const ownsNFT = await verifyNftOwnership(api, tikiNumber, selectedAccount.address);
|
const isValid = await verifyCitizenNumber(api, citizenNumber, selectedAccount.address);
|
||||||
|
|
||||||
if (!ownsNFT) {
|
if (!isValid) {
|
||||||
setError(`NFT #${tikiNumber} not found in your wallet or not a Welati Tiki`);
|
setError(`Invalid Citizen Number or it doesn't match your wallet`);
|
||||||
setStep('error');
|
setStep('error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate challenge for signature
|
// Generate challenge for signature
|
||||||
const authChallenge = generateAuthChallenge(tikiNumber);
|
const authChallenge = generateAuthChallenge(citizenNumber);
|
||||||
setChallenge(authChallenge);
|
setChallenge(authChallenge);
|
||||||
setStep('signing');
|
setStep('signing');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Verification error:', err);
|
console.error('Verification error:', err);
|
||||||
setError('Failed to verify NFT ownership');
|
setError('Failed to verify Citizen Number');
|
||||||
setStep('error');
|
setStep('error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -80,7 +80,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
|||||||
|
|
||||||
// Save session
|
// Save session
|
||||||
const session = {
|
const session = {
|
||||||
tikiNumber,
|
tikiNumber: citizenNumber,
|
||||||
walletAddress: selectedAccount.address,
|
walletAddress: selectedAccount.address,
|
||||||
sessionToken: signature, // In production, use proper JWT
|
sessionToken: signature, // In production, use proper JWT
|
||||||
lastAuthenticated: Date.now(),
|
lastAuthenticated: Date.now(),
|
||||||
@@ -91,11 +91,10 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
|||||||
|
|
||||||
setStep('success');
|
setStep('success');
|
||||||
|
|
||||||
// Redirect to citizen dashboard after 2 seconds
|
// Redirect to citizens page after 2 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// TODO: Navigate to citizen dashboard
|
|
||||||
onClose();
|
onClose();
|
||||||
window.location.href = '/dashboard'; // Or use router.push('/dashboard')
|
window.location.href = '/citizens';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Signature error:', err);
|
console.error('Signature error:', err);
|
||||||
@@ -121,24 +120,24 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
|||||||
Authenticate as Citizen
|
Authenticate as Citizen
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Enter your Welati Tiki NFT number to authenticate
|
Enter your Citizen Number from your Dashboard to authenticate
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* Step 1: Enter NFT Number */}
|
{/* Step 1: Enter Citizen Number */}
|
||||||
{step === 'input' && (
|
{step === 'input' && (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="tikiNumber">Welati Tiki NFT Number</Label>
|
<Label htmlFor="citizenNumber">Citizen Number</Label>
|
||||||
<Input
|
<Input
|
||||||
id="tikiNumber"
|
id="citizenNumber"
|
||||||
placeholder="e.g., 12345"
|
placeholder="e.g., #42-0-123456"
|
||||||
value={tikiNumber}
|
value={citizenNumber}
|
||||||
onChange={(e) => setTikiNumber(e.target.value)}
|
onChange={(e) => setCitizenNumber(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleVerifyNFT()}
|
onKeyDown={(e) => e.key === 'Enter' && handleVerifyNFT()}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -148,7 +147,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={handleVerifyNFT} className="w-full">
|
<Button onClick={handleVerifyNFT} className="w-full">
|
||||||
Verify NFT Ownership
|
Verify Citizen Number
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -158,7 +157,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
|||||||
{step === 'verifying' && (
|
{step === 'verifying' && (
|
||||||
<div className="flex flex-col items-center justify-center py-8 space-y-4">
|
<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" />
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -191,7 +190,7 @@ export const ExistingCitizenAuth: React.FC<ExistingCitizenAuthProps> = ({ onClos
|
|||||||
<CheckCircle className="h-16 w-16 text-green-500" />
|
<CheckCircle className="h-16 w-16 text-green-500" />
|
||||||
<h3 className="text-lg font-semibold">Authentication Successful!</h3>
|
<h3 className="text-lg font-semibold">Authentication Successful!</h3>
|
||||||
<p className="text-sm text-muted-foreground text-center">
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
Welcome back, Citizen #{tikiNumber}
|
Welcome back, Citizen #{citizenNumber}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Redirecting to citizen dashboard...
|
Redirecting to citizen dashboard...
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
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 { usePolkadot } from '@/contexts/PolkadotContext';
|
||||||
import type { CitizenshipData, Region, MaritalStatus } from '@pezkuwi/lib/citizenship-workflow';
|
import type { CitizenshipData, Region, MaritalStatus } from '@pezkuwi/lib/citizenship-workflow';
|
||||||
import { FOUNDER_ADDRESS, submitKycApplication, subscribeToKycApproval, getKycStatus } 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 {
|
interface NewCitizenApplicationProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
referrerAddress?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormData = Omit<CitizenshipData, 'walletAddress' | 'timestamp'>;
|
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 { api, isApiReady, selectedAccount, connectWallet } = usePolkadot();
|
||||||
const { register, handleSubmit, watch, setValue, formState: { errors } } = useForm<FormData>();
|
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 [error, setError] = useState<string | null>(null);
|
||||||
const [agreed, setAgreed] = useState(false);
|
const [agreed, setAgreed] = useState(false);
|
||||||
const [checkingStatus, setCheckingStatus] = useState(false);
|
const [checkingStatus, setCheckingStatus] = useState(false);
|
||||||
|
const [confirming, setConfirming] = useState(false);
|
||||||
|
const [applicationHash, setApplicationHash] = useState<string>('');
|
||||||
|
|
||||||
const maritalStatus = watch('maritalStatus');
|
const maritalStatus = watch('maritalStatus');
|
||||||
const childrenCount = watch('childrenCount');
|
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
|
// Check KYC status on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkKycStatus = async () => {
|
const checkKycStatus = async () => {
|
||||||
@@ -139,6 +210,13 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
|||||||
return;
|
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
|
// Prepare complete citizenship data
|
||||||
const citizenshipData: CitizenshipData = {
|
const citizenshipData: CitizenshipData = {
|
||||||
...data,
|
...data,
|
||||||
@@ -193,6 +271,11 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
|||||||
console.log('✅ KYC application submitted to blockchain');
|
console.log('✅ KYC application submitted to blockchain');
|
||||||
console.log('Block hash:', result.blockHash);
|
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
|
// Move to waiting for approval state
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
@@ -238,59 +321,107 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Waiting for approval - Loading state
|
// Waiting for self-confirmation
|
||||||
if (waitingForApproval) {
|
if (waitingForApproval) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="pt-6 flex flex-col items-center justify-center py-8 space-y-6">
|
<CardContent className="pt-6 flex flex-col items-center justify-center py-8 space-y-6">
|
||||||
{/* Animated Loader with Halos */}
|
{/* Icon */}
|
||||||
<div className="relative flex items-center justify-center">
|
<div className="relative">
|
||||||
{/* Outer halo */}
|
<div className="h-24 w-24 rounded-full border-4 border-primary/20 flex items-center justify-center">
|
||||||
<div className="absolute w-32 h-32 border-4 border-cyan-500/20 rounded-full animate-ping"></div>
|
<CheckCircle className="h-10 w-10 text-primary" />
|
||||||
{/* Middle halo */}
|
|
||||||
<div className="absolute w-24 h-24 border-4 border-purple-500/30 rounded-full animate-pulse"></div>
|
|
||||||
{/* Inner spinning sun */}
|
|
||||||
<div className="relative w-16 h-16 flex items-center justify-center">
|
|
||||||
<Loader2 className="w-16 h-16 text-cyan-500 animate-spin" />
|
|
||||||
<Clock className="absolute w-8 h-8 text-yellow-400 animate-pulse" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center space-y-2">
|
<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">
|
<p className="text-sm text-muted-foreground max-w-md">
|
||||||
Your application has been submitted to the blockchain and is waiting for admin approval.
|
Your application has been submitted to the blockchain. Please review and confirm your identity to mint your Citizen NFT (Welati Tiki).
|
||||||
This page will automatically update when your citizenship is approved.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status steps */}
|
{/* Status steps */}
|
||||||
<div className="w-full max-w-md space-y-3 pt-4">
|
<div className="w-full max-w-md space-y-3 pt-4">
|
||||||
<div className="flex items-center gap-3 text-sm">
|
<div className="flex items-center gap-3">
|
||||||
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0" />
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
|
||||||
<span className="text-muted-foreground">Application encrypted and stored on IPFS</span>
|
<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>
|
||||||
<div className="flex items-center gap-3 text-sm">
|
|
||||||
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0" />
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-muted-foreground">Transaction submitted to blockchain</span>
|
<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>
|
||||||
<div className="flex items-center gap-3 text-sm">
|
|
||||||
<Loader2 className="h-5 w-5 text-cyan-500 animate-spin flex-shrink-0" />
|
<div className="flex items-center gap-3">
|
||||||
<span className="font-medium">Waiting for admin to approve KYC...</span>
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
|
||||||
</div>
|
<AlertCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||||
<div className="flex items-center gap-3 text-sm opacity-50">
|
</div>
|
||||||
<Clock className="h-5 w-5 text-gray-400 flex-shrink-0" />
|
<div className="flex-1">
|
||||||
<span className="text-muted-foreground">Receive Welati Tiki NFT</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info */}
|
{/* Action buttons */}
|
||||||
<Alert className="bg-cyan-500/10 border-cyan-500/30">
|
<div className="flex gap-3 w-full max-w-md pt-4">
|
||||||
<AlertDescription className="text-xs">
|
<Button
|
||||||
<strong>Note:</strong> Do not close this page. The system is monitoring the blockchain
|
onClick={handleApprove}
|
||||||
for approval events in real-time. You will be automatically redirected once approved.
|
disabled={confirming}
|
||||||
</AlertDescription>
|
className="flex-1 bg-green-600 hover:bg-green-700"
|
||||||
</Alert>
|
>
|
||||||
|
{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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -493,7 +624,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<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">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
If empty, you will be automatically linked to the Founder (Satoshi Qazi Muhammed)
|
If empty, you will be automatically linked to the Founder (Satoshi Qazi Muhammed)
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -178,7 +178,8 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
value={amountCrypto}
|
value={amountCrypto}
|
||||||
onChange={e => setAmountCrypto(e.target.value)}
|
onChange={e => setAmountCrypto(e.target.value)}
|
||||||
placeholder="10.00"
|
placeholder="Amount"
|
||||||
|
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,7 +209,8 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
value={fiatAmount}
|
value={fiatAmount}
|
||||||
onChange={e => setFiatAmount(e.target.value)}
|
onChange={e => setFiatAmount(e.target.value)}
|
||||||
placeholder="1000.00"
|
placeholder="Amount"
|
||||||
|
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,6 +256,7 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
|||||||
value={paymentDetails[field] || ''}
|
value={paymentDetails[field] || ''}
|
||||||
onChange={(e) => handlePaymentDetailChange(field, e.target.value)}
|
onChange={(e) => handlePaymentDetailChange(field, e.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -270,7 +273,8 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
value={minOrderAmount}
|
value={minOrderAmount}
|
||||||
onChange={e => setMinOrderAmount(e.target.value)}
|
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>
|
||||||
<div>
|
<div>
|
||||||
@@ -281,7 +285,8 @@ export function CreateAd({ onAdCreated }: CreateAdProps) {
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
value={maxOrderAmount}
|
value={maxOrderAmount}
|
||||||
onChange={e => setMaxOrderAmount(e.target.value)}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ export function TradeModal({ offer, onClose }: TradeModalProps) {
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
value={amount}
|
value={amount}
|
||||||
onChange={(e) => setAmount(e.target.value)}
|
onChange={(e) => setAmount(e.target.value)}
|
||||||
placeholder={`Enter amount (max ${offer.remaining_amount})`}
|
placeholder="Amount"
|
||||||
className="bg-gray-800 border-gray-700 text-white"
|
className="bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 placeholder:opacity-50"
|
||||||
/>
|
/>
|
||||||
{offer.min_order_amount && (
|
{offer.min_order_amount && (
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<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
|
// Check active sessions and sets the user
|
||||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||||
setUser(session?.user ?? null);
|
setUser(session?.user ?? null);
|
||||||
if (session?.user) {
|
checkAdminStatus(); // Check admin status regardless of Supabase session
|
||||||
checkAdminStatus();
|
|
||||||
}
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// If Supabase is not available, continue without auth
|
// If Supabase is not available, still check wallet-based admin
|
||||||
|
checkAdminStatus();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for changes on auth state
|
// Listen for changes on auth state
|
||||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||||
setUser(session?.user ?? null);
|
setUser(session?.user ?? null);
|
||||||
if (session?.user) {
|
checkAdminStatus(); // Check admin status on auth change
|
||||||
checkAdminStatus();
|
|
||||||
}
|
|
||||||
setLoading(false);
|
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 () => {
|
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 {
|
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();
|
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
|
if (!error && data && ['admin', 'super_admin'].includes(data.role)) {
|
||||||
.from('admin_roles')
|
console.log('✅ Admin access granted (Supabase-based)');
|
||||||
.select('role')
|
setIsAdmin(true);
|
||||||
.eq('user_id', user.id)
|
return true;
|
||||||
.maybeSingle();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const adminStatus = !error && data && ['admin', 'super_admin'].includes(data.role);
|
console.log('❌ Admin access denied');
|
||||||
setIsAdmin(adminStatus);
|
setIsAdmin(false);
|
||||||
return adminStatus;
|
return false;
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.error('Admin check error:', err);
|
||||||
|
setIsAdmin(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,6 +33,19 @@ export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
|
|||||||
const [selectedAccount, setSelectedAccount] = useState<InjectedAccountWithMeta | null>(null);
|
const [selectedAccount, setSelectedAccount] = useState<InjectedAccountWithMeta | null>(null);
|
||||||
const [error, setError] = useState<string | 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
|
// Initialize Polkadot API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initApi = async () => {
|
const initApi = async () => {
|
||||||
@@ -76,6 +89,36 @@ export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
|
|||||||
};
|
};
|
||||||
}, [endpoint]);
|
}, [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)
|
// Connect wallet (Polkadot.js extension)
|
||||||
const connectWallet = async () => {
|
const connectWallet = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -101,7 +144,15 @@ export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setAccounts(allAccounts);
|
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)`);
|
console.log(`✅ Found ${allAccounts.length} account(s)`);
|
||||||
|
|
||||||
@@ -114,7 +165,7 @@ export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
|
|||||||
// Disconnect wallet
|
// Disconnect wallet
|
||||||
const disconnectWallet = () => {
|
const disconnectWallet = () => {
|
||||||
setAccounts([]);
|
setAccounts([]);
|
||||||
setSelectedAccount(null);
|
handleSetSelectedAccount(null);
|
||||||
console.log('🔌 Wallet disconnected');
|
console.log('🔌 Wallet disconnected');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,7 +175,7 @@ export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
|
|||||||
isConnected: isApiReady, // Alias for backward compatibility
|
isConnected: isApiReady, // Alias for backward compatibility
|
||||||
accounts,
|
accounts,
|
||||||
selectedAccount,
|
selectedAccount,
|
||||||
setSelectedAccount,
|
setSelectedAccount: handleSetSelectedAccount,
|
||||||
connectWallet,
|
connectWallet,
|
||||||
disconnectWallet,
|
disconnectWallet,
|
||||||
error,
|
error,
|
||||||
|
|||||||
@@ -38,9 +38,10 @@ export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||||||
const connectionAttempts = useRef(0);
|
const connectionAttempts = useRef(0);
|
||||||
|
|
||||||
const ENDPOINTS = [
|
const ENDPOINTS = [
|
||||||
'wss://ws.pezkuwichain.io', // Production WebSocket
|
'ws://localhost:8082', // Local Vite dev server
|
||||||
'ws://localhost:9944', // Local development node
|
'ws://127.0.0.1:9944', // Local development node (primary)
|
||||||
'ws://127.0.0.1:9944', // Alternative local address
|
'ws://localhost:9944', // Local development node (alternative)
|
||||||
|
'wss://ws.pezkuwichain.io', // Production WebSocket (fallback)
|
||||||
];
|
];
|
||||||
|
|
||||||
const connect = useCallback((endpointIndex: number = 0) => {
|
const connect = useCallback((endpointIndex: number = 0) => {
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ import {
|
|||||||
import { SessionMonitor } from '@/components/security/SessionMonitor';
|
import { SessionMonitor } from '@/components/security/SessionMonitor';
|
||||||
import { PermissionEditor } from '@/components/security/PermissionEditor';
|
import { PermissionEditor } from '@/components/security/PermissionEditor';
|
||||||
import { SecurityAudit } from '@/components/security/SecurityAudit';
|
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() {
|
export default function AdminPanel() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [users, setUsers] = useState<any[]>([]);
|
const [users, setUsers] = useState<any[]>([]);
|
||||||
@@ -149,38 +153,58 @@ export default function AdminPanel() {
|
|||||||
</button>
|
</button>
|
||||||
<h1 className="text-3xl font-bold mb-8">Admin Panel</h1>
|
<h1 className="text-3xl font-bold mb-8">Admin Panel</h1>
|
||||||
|
|
||||||
<Tabs defaultValue="users" className="space-y-4">
|
<Tabs defaultValue="setup" className="space-y-4">
|
||||||
<TabsList className="grid w-full grid-cols-7">
|
<TabsList className="grid w-full grid-cols-10 h-auto">
|
||||||
<TabsTrigger value="users">
|
<TabsTrigger value="setup" className="flex-col h-auto py-3">
|
||||||
<Users className="mr-2 h-4 w-4" />
|
<Shield className="h-4 w-4 mb-1" />
|
||||||
Users
|
<span className="text-xs leading-tight">Commission<br/>Setup</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="roles">
|
<TabsTrigger value="kyc" className="flex-col h-auto py-3">
|
||||||
<Shield className="mr-2 h-4 w-4" />
|
<Users className="h-4 w-4 mb-1" />
|
||||||
Roles
|
<span className="text-xs leading-tight">KYC<br/>Approvals</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="sessions">
|
<TabsTrigger value="voting" className="flex-col h-auto py-3">
|
||||||
<Monitor className="mr-2 h-4 w-4" />
|
<Activity className="h-4 w-4 mb-1" />
|
||||||
Sessions
|
<span className="text-xs leading-tight">Commission<br/>Voting</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="permissions">
|
<TabsTrigger value="users" className="flex-col h-auto py-3">
|
||||||
<Lock className="mr-2 h-4 w-4" />
|
<Users className="h-4 w-4 mb-1" />
|
||||||
Permissions
|
<span className="text-xs leading-tight">Users</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="security">
|
<TabsTrigger value="roles" className="flex-col h-auto py-3">
|
||||||
<AlertTriangle className="mr-2 h-4 w-4" />
|
<Shield className="h-4 w-4 mb-1" />
|
||||||
Security
|
<span className="text-xs leading-tight">Roles</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="activity">
|
<TabsTrigger value="sessions" className="flex-col h-auto py-3">
|
||||||
<Activity className="mr-2 h-4 w-4" />
|
<Monitor className="h-4 w-4 mb-1" />
|
||||||
Activity
|
<span className="text-xs leading-tight">Sessions</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="settings">
|
<TabsTrigger value="permissions" className="flex-col h-auto py-3">
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Lock className="h-4 w-4 mb-1" />
|
||||||
Settings
|
<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>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="kyc">
|
||||||
|
<KycApprovalTab />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="voting">
|
||||||
|
<CommissionVotingTab />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="users">
|
<TabsContent value="users">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -323,6 +347,9 @@ export default function AdminPanel() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="setup">
|
||||||
|
<CommissionSetupTab />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+87
-58
@@ -1,79 +1,101 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { CitizenshipModal } from '@/components/citizenship/CitizenshipModal';
|
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 BeCitizen: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
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 (
|
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">
|
<div className="container mx-auto px-4 py-16">
|
||||||
{/* Back to Home Button */}
|
{/* Back to Home Button and Invite Friend */}
|
||||||
<div className="mb-8">
|
<div className="mb-8 flex justify-between items-center">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate('/')}
|
onClick={() => navigate('/')}
|
||||||
variant="outline"
|
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" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
Back to Home
|
Back to Home
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div className="text-center mb-16">
|
<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
|
🏛️ Digital Kurdistan
|
||||||
</h1>
|
</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
|
Bibe Welati / Be a Citizen
|
||||||
</h2>
|
</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.
|
Join the Digital Kurdistan State as a sovereign citizen. Receive your Welati Tiki NFT and unlock governance, trust scoring, and community benefits.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Benefits Grid */}
|
{/* Benefits Grid */}
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
|
<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>
|
<CardHeader>
|
||||||
<Shield className="h-12 w-12 text-cyan-400 mb-3" />
|
<Shield className="h-12 w-12 text-red-600 mb-3" />
|
||||||
<CardTitle className="text-white">Privacy Protected</CardTitle>
|
<CardTitle className="text-red-700 font-bold">Privacy Protected</CardTitle>
|
||||||
<CardDescription className="text-gray-300">
|
<CardDescription className="text-gray-700 font-medium">
|
||||||
Your data is encrypted with ZK-proofs. Only hashes are stored on-chain.
|
Your data is encrypted with ZK-proofs. Only hashes are stored on-chain.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</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>
|
<CardHeader>
|
||||||
<Award className="h-12 w-12 text-purple-400 mb-3" />
|
<Award className="h-12 w-12 text-yellow-700 mb-3" />
|
||||||
<CardTitle className="text-white">Welati Tiki NFT</CardTitle>
|
<CardTitle className="text-yellow-800 font-bold">Welati Tiki NFT</CardTitle>
|
||||||
<CardDescription className="text-gray-300">
|
<CardDescription className="text-gray-700 font-medium">
|
||||||
Receive your unique soulbound citizenship NFT after KYC approval.
|
Receive your unique soulbound citizenship NFT after KYC approval.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</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>
|
<CardHeader>
|
||||||
<Users className="h-12 w-12 text-green-400 mb-3" />
|
<Users className="h-12 w-12 text-green-600 mb-3" />
|
||||||
<CardTitle className="text-white">Trust Scoring</CardTitle>
|
<CardTitle className="text-green-700 font-bold">Trust Scoring</CardTitle>
|
||||||
<CardDescription className="text-gray-300">
|
<CardDescription className="text-gray-700 font-medium">
|
||||||
Build trust through referrals, staking, and community contributions.
|
Build trust through referrals, staking, and community contributions.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</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>
|
<CardHeader>
|
||||||
<Globe className="h-12 w-12 text-yellow-400 mb-3" />
|
<Globe className="h-12 w-12 text-red-600 mb-3" />
|
||||||
<CardTitle className="text-white">Governance Access</CardTitle>
|
<CardTitle className="text-red-700 font-bold">Governance Access</CardTitle>
|
||||||
<CardDescription className="text-gray-300">
|
<CardDescription className="text-gray-700 font-medium">
|
||||||
Participate in on-chain governance and shape the future of Digital Kurdistan.
|
Participate in on-chain governance and shape the future of Digital Kurdistan.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -82,12 +104,12 @@ const BeCitizen: React.FC = () => {
|
|||||||
|
|
||||||
{/* CTA Section */}
|
{/* CTA Section */}
|
||||||
<div className="max-w-4xl mx-auto">
|
<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">
|
<CardContent className="pt-8 pb-8">
|
||||||
<div className="text-center space-y-6">
|
<div className="text-center space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-bold text-white mb-3">Ready to Join?</h3>
|
<h3 className="text-2xl font-bold text-red-700 mb-3">Ready to Join?</h3>
|
||||||
<p className="text-gray-300 mb-6">
|
<p className="text-gray-800 font-medium mb-6">
|
||||||
Whether you're already a citizen or want to become one, start your journey here.
|
Whether you're already a citizen or want to become one, start your journey here.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,25 +117,25 @@ const BeCitizen: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => setIsModalOpen(true)}
|
onClick={() => setIsModalOpen(true)}
|
||||||
size="lg"
|
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>
|
<span>Start Citizenship Process</span>
|
||||||
<ChevronRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
<ChevronRight className="ml-2 h-5 w-5 group-hover:translate-x-1 transition-transform" />
|
||||||
</Button>
|
</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">
|
<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>
|
<span>Secure ZK-Proof Authentication</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:block">•</div>
|
<div className="hidden md:block text-red-600">•</div>
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span>Soulbound NFT Citizenship</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:block">•</div>
|
<div className="hidden md:block text-red-600">•</div>
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span>Decentralized Identity</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,18 +146,18 @@ const BeCitizen: React.FC = () => {
|
|||||||
|
|
||||||
{/* Process Overview */}
|
{/* Process Overview */}
|
||||||
<div className="mt-16 max-w-5xl mx-auto">
|
<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">
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
{/* Existing Citizens */}
|
{/* 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>
|
<CardHeader>
|
||||||
<div className="bg-cyan-500/20 w-12 h-12 rounded-full flex items-center justify-center mb-4">
|
<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-cyan-400">1</span>
|
<span className="text-2xl font-bold text-white">1</span>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-white">Already a Citizen?</CardTitle>
|
<CardTitle className="text-red-700 font-bold">Already a Citizen?</CardTitle>
|
||||||
</CardHeader>
|
</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>✓ Enter your Welati Tiki NFT number</p>
|
||||||
<p>✓ Verify NFT ownership on-chain</p>
|
<p>✓ Verify NFT ownership on-chain</p>
|
||||||
<p>✓ Sign authentication challenge</p>
|
<p>✓ Sign authentication challenge</p>
|
||||||
@@ -144,14 +166,14 @@ const BeCitizen: React.FC = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* New Citizens */}
|
{/* 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>
|
<CardHeader>
|
||||||
<div className="bg-purple-500/20 w-12 h-12 rounded-full flex items-center justify-center mb-4">
|
<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-purple-400">2</span>
|
<span className="text-2xl font-bold text-white">2</span>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-white">New to Citizenship?</CardTitle>
|
<CardTitle className="text-yellow-800 font-bold">New to Citizenship?</CardTitle>
|
||||||
</CardHeader>
|
</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>✓ Fill detailed KYC application</p>
|
||||||
<p>✓ Data encrypted with ZK-proofs</p>
|
<p>✓ Data encrypted with ZK-proofs</p>
|
||||||
<p>✓ Submit for admin approval</p>
|
<p>✓ Submit for admin approval</p>
|
||||||
@@ -160,14 +182,14 @@ const BeCitizen: React.FC = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* After Citizenship */}
|
{/* 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>
|
<CardHeader>
|
||||||
<div className="bg-green-500/20 w-12 h-12 rounded-full flex items-center justify-center mb-4">
|
<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-green-400">3</span>
|
<span className="text-2xl font-bold text-white">3</span>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-white">Citizen Benefits</CardTitle>
|
<CardTitle className="text-green-700 font-bold">Citizen Benefits</CardTitle>
|
||||||
</CardHeader>
|
</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>✓ Trust score calculation enabled</p>
|
||||||
<p>✓ Governance voting rights</p>
|
<p>✓ Governance voting rights</p>
|
||||||
<p>✓ Referral tree participation</p>
|
<p>✓ Referral tree participation</p>
|
||||||
@@ -179,13 +201,13 @@ const BeCitizen: React.FC = () => {
|
|||||||
|
|
||||||
{/* Security Notice */}
|
{/* Security Notice */}
|
||||||
<div className="mt-12 max-w-3xl mx-auto">
|
<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">
|
<CardContent className="pt-6">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<Shield className="h-6 w-6 text-yellow-400 mt-1 flex-shrink-0" />
|
<Shield className="h-6 w-6 text-yellow-700 mt-1 flex-shrink-0" />
|
||||||
<div className="text-sm text-gray-200">
|
<div className="text-sm text-gray-700">
|
||||||
<p className="font-semibold text-yellow-400 mb-2">Privacy & Security</p>
|
<p className="font-bold text-yellow-800 mb-2">Privacy & Security</p>
|
||||||
<p>
|
<p className="font-medium">
|
||||||
Your personal data is encrypted using AES-GCM with your wallet-derived keys.
|
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
|
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.
|
on IPFS and locally on your device. No personal information is ever publicly visible.
|
||||||
@@ -198,7 +220,14 @@ const BeCitizen: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Citizenship Modal */}
|
{/* 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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+254
-3
@@ -7,10 +7,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||||
import { supabase } from '@/lib/supabase';
|
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 { 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 { 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() {
|
export default function Dashboard() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -28,6 +32,13 @@ export default function Dashboard() {
|
|||||||
totalScore: 0
|
totalScore: 0
|
||||||
});
|
});
|
||||||
const [loadingScores, setLoadingScores] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
fetchProfile();
|
fetchProfile();
|
||||||
@@ -107,6 +118,14 @@ export default function Dashboard() {
|
|||||||
// Also fetch tikis separately for role display (needed for role details)
|
// Also fetch tikis separately for role display (needed for role details)
|
||||||
const userTikis = await fetchUserTikis(api, selectedAccount.address);
|
const userTikis = await fetchUserTikis(api, selectedAccount.address);
|
||||||
setTikis(userTikis);
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching scores and tikis:', error);
|
console.error('Error fetching scores and tikis:', error);
|
||||||
} finally {
|
} 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 => {
|
const getRoleDisplay = (): string => {
|
||||||
if (loadingScores) return 'Loading...';
|
if (loadingScores) return 'Loading...';
|
||||||
if (!selectedAccount) return 'Member';
|
if (!selectedAccount) return 'Member';
|
||||||
@@ -342,6 +453,7 @@ export default function Dashboard() {
|
|||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="profile">Profile</TabsTrigger>
|
<TabsTrigger value="profile">Profile</TabsTrigger>
|
||||||
<TabsTrigger value="roles">Roles & Tikis</TabsTrigger>
|
<TabsTrigger value="roles">Roles & Tikis</TabsTrigger>
|
||||||
|
<TabsTrigger value="referrals">Referrals</TabsTrigger>
|
||||||
<TabsTrigger value="security">Security</TabsTrigger>
|
<TabsTrigger value="security">Security</TabsTrigger>
|
||||||
<TabsTrigger value="activity">Activity</TabsTrigger>
|
<TabsTrigger value="activity">Activity</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -436,7 +548,7 @@ export default function Dashboard() {
|
|||||||
No roles assigned yet
|
No roles assigned yet
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Complete KYC to become a Citizen (Hemwelatî)
|
Complete KYC to become a Citizen (Welati)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -475,18 +587,157 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<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>
|
<h4 className="font-medium mb-2">Blockchain Address</h4>
|
||||||
<p className="text-sm text-muted-foreground font-mono break-all">
|
<p className="text-sm text-muted-foreground font-mono break-all">
|
||||||
{selectedAccount.address}
|
{selectedAccount.address}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="referrals" className="space-y-4">
|
||||||
|
<ReferralDashboard />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="security" className="space-y-4">
|
<TabsContent value="security" className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -320,8 +320,8 @@ const Login: React.FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
id="referral-code"
|
id="referral-code"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('login.enterReferralCode', 'Enter referral code')}
|
placeholder={t('login.enterReferralCode', 'Referral code (optional)')}
|
||||||
className="pl-10 bg-gray-800 border-gray-700 text-white"
|
className="pl-10 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 placeholder:opacity-50"
|
||||||
value={signupData.referralCode}
|
value={signupData.referralCode}
|
||||||
onChange={(e) => setSignupData({...signupData, referralCode: e.target.value})}
|
onChange={(e) => setSignupData({...signupData, referralCode: e.target.value})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -163,6 +163,13 @@ export default function CitizensIssues() {
|
|||||||
if (!api || !isApiReady) return;
|
if (!api || !isApiReady) return;
|
||||||
|
|
||||||
try {
|
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 issueCountResult = await api.query.welati.issueCount();
|
||||||
const issueCount = issueCountResult.toNumber();
|
const issueCount = issueCountResult.toNumber();
|
||||||
|
|
||||||
@@ -188,11 +195,7 @@ export default function CitizensIssues() {
|
|||||||
setIssues(fetchedIssues.reverse());
|
setIssues(fetchedIssues.reverse());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching issues:', error);
|
console.error('Error fetching issues:', error);
|
||||||
toast({
|
setIssues([]);
|
||||||
title: 'Xeletî (Error)',
|
|
||||||
description: 'Pirsgirêk di barkirina pirsan de (Error loading issues)',
|
|
||||||
variant: 'destructive'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -200,6 +203,13 @@ export default function CitizensIssues() {
|
|||||||
if (!api || !isApiReady || !selectedAccount) return;
|
if (!api || !isApiReady || !selectedAccount) return;
|
||||||
|
|
||||||
try {
|
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 votesEntries = await api.query.welati.issueVotes.entries(selectedAccount.address);
|
||||||
const votes = new Map<number, boolean>();
|
const votes = new Map<number, boolean>();
|
||||||
|
|
||||||
@@ -212,6 +222,7 @@ export default function CitizensIssues() {
|
|||||||
setUserVotes(votes);
|
setUserVotes(votes);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching user votes:', error);
|
console.error('Error fetching user votes:', error);
|
||||||
|
setUserVotes(new Map());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -304,6 +315,13 @@ export default function CitizensIssues() {
|
|||||||
if (!api || !isApiReady) return;
|
if (!api || !isApiReady) return;
|
||||||
|
|
||||||
try {
|
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 candidatesEntries = await api.query.welati.parliamentCandidates.entries();
|
||||||
const candidates: ParliamentCandidate[] = [];
|
const candidates: ParliamentCandidate[] = [];
|
||||||
|
|
||||||
@@ -333,6 +351,7 @@ export default function CitizensIssues() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching parliament candidates:', error);
|
console.error('Error fetching parliament candidates:', error);
|
||||||
|
setParliamentCandidates([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -450,6 +469,13 @@ export default function CitizensIssues() {
|
|||||||
if (!api || !isApiReady) return;
|
if (!api || !isApiReady) return;
|
||||||
|
|
||||||
try {
|
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 candidatesEntries = await api.query.welati.presidentCandidates.entries();
|
||||||
const candidates: PresidentCandidate[] = [];
|
const candidates: PresidentCandidate[] = [];
|
||||||
|
|
||||||
@@ -479,6 +505,7 @@ export default function CitizensIssues() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching president candidates:', error);
|
console.error('Error fetching president candidates:', error);
|
||||||
|
setPresidentCandidates([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -596,6 +623,14 @@ export default function CitizensIssues() {
|
|||||||
if (!api || !isApiReady) return;
|
if (!api || !isApiReady) return;
|
||||||
|
|
||||||
try {
|
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 proposalsEntries = await api.query.welati.legislationProposals.entries();
|
||||||
const proposals: LegislationProposal[] = [];
|
const proposals: LegislationProposal[] = [];
|
||||||
|
|
||||||
@@ -631,6 +666,8 @@ export default function CitizensIssues() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching legislation proposals:', error);
|
console.error('Error fetching legislation proposals:', error);
|
||||||
|
setLegislationProposals([]);
|
||||||
|
setUserLegislationVotes(new Map());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1085,9 +1122,10 @@ export default function CitizensIssues() {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<Input
|
<Input
|
||||||
placeholder="5Abc123... (Substrate Address)"
|
placeholder="Candidate address"
|
||||||
value={nominateParliamentAddress}
|
value={nominateParliamentAddress}
|
||||||
onChange={(e) => setNominateParliamentAddress(e.target.value)}
|
onChange={(e) => setNominateParliamentAddress(e.target.value)}
|
||||||
|
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleNominateParliament}
|
onClick={handleNominateParliament}
|
||||||
@@ -1196,9 +1234,10 @@ export default function CitizensIssues() {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
<Input
|
<Input
|
||||||
placeholder="5Abc123... (Substrate Address)"
|
placeholder="Candidate address"
|
||||||
value={nominatePresidentAddress}
|
value={nominatePresidentAddress}
|
||||||
onChange={(e) => setNominatePresidentAddress(e.target.value)}
|
onChange={(e) => setNominatePresidentAddress(e.target.value)}
|
||||||
|
className="placeholder:text-gray-500 placeholder:opacity-50"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleNominatePresident}
|
onClick={handleNominatePresident}
|
||||||
|
|||||||
Reference in New Issue
Block a user