mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 22:57:55 +00:00
fix: rewrite citizenship workflow to referral-based model
- Replace governance-based KYC with trustless referral workflow - New 3-step flow: applyForCitizenship -> approveReferral -> confirmCitizenship - Fix FOUNDER_ADDRESS (was Alice test address) - Use applications storage instead of legacy pendingKycApplications - Add approveReferral, cancelApplication, confirmCitizenship functions - Rewrite KycApprovalTab as referrer approval panel (no governance) - Fix InviteUserModal to use peopleApi for referral pallet - Add pending approvals section to ReferralDashboard
This commit is contained in:
@@ -4,8 +4,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { Loader2, CheckCircle, XCircle, Clock, User, Mail, FileText, AlertTriangle } from 'lucide-react';
|
||||
import { COMMISSIONS } from '@/config/commissions';
|
||||
import { Loader2, CheckCircle, Clock, User, AlertTriangle } from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -14,96 +13,42 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
||||
interface PendingApplication {
|
||||
address: string;
|
||||
cids: string[];
|
||||
notes: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
interface IdentityInfo {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
import { approveReferral, getPendingApprovalsForReferrer } from '@pezkuwi/lib/citizenship-workflow';
|
||||
import type { PendingApproval } from '@pezkuwi/lib/citizenship-workflow';
|
||||
|
||||
export function KycApprovalTab() {
|
||||
// identityKyc pallet is on People Chain, dynamicCommissionCollective is on Relay Chain
|
||||
const { api, isApiReady, peopleApi, isPeopleReady, selectedAccount, connectWallet } = usePezkuwi();
|
||||
// identityKyc pallet is on People Chain - use peopleApi
|
||||
const { peopleApi, isPeopleReady, selectedAccount, connectWallet } = usePezkuwi();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pendingApps, setPendingApps] = useState<PendingApplication[]>([]);
|
||||
const [identities, setIdentities] = useState<Map<string, IdentityInfo>>(new Map());
|
||||
const [selectedApp, setSelectedApp] = useState<PendingApplication | null>(null);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [showDetailsModal, setShowDetailsModal] = useState(false);
|
||||
const [pendingApps, setPendingApps] = useState<PendingApproval[]>([]);
|
||||
const [processingAddress, setProcessingAddress] = useState<string | null>(null);
|
||||
|
||||
// Load pending KYC applications from People Chain
|
||||
// Load pending applications where current user is the referrer
|
||||
useEffect(() => {
|
||||
if (!peopleApi || !isPeopleReady) {
|
||||
if (!peopleApi || !isPeopleReady || !selectedAccount) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
loadPendingApplications();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [peopleApi, isPeopleReady]);
|
||||
|
||||
}, [peopleApi, isPeopleReady, selectedAccount]);
|
||||
|
||||
const loadPendingApplications = async () => {
|
||||
// identityKyc pallet is on People Chain
|
||||
if (!peopleApi || !isPeopleReady) {
|
||||
if (!peopleApi || !isPeopleReady || !selectedAccount) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// Get all pending applications from People Chain
|
||||
const entries = await peopleApi.query.identityKyc.pendingKycApplications.entries();
|
||||
|
||||
const apps: PendingApplication[] = [];
|
||||
const identityMap = new Map<string, IdentityInfo>();
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
const address = key.args[0].toString();
|
||||
const application = value.toJSON() as Record<string, unknown>;
|
||||
|
||||
// Get identity info for this address from People Chain
|
||||
try {
|
||||
const identity = await peopleApi.query.identityKyc.identities(address);
|
||||
if (!identity.isEmpty) {
|
||||
const identityData = identity.toJSON() as Record<string, unknown>;
|
||||
identityMap.set(address, {
|
||||
name: identityData.name || 'Unknown',
|
||||
email: identityData.email || 'No email'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('Error fetching identity for', address, err);
|
||||
}
|
||||
|
||||
apps.push({
|
||||
address,
|
||||
cids: application.cids || [],
|
||||
notes: application.notes || 'No notes provided',
|
||||
timestamp: application.timestamp || Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
const apps = await getPendingApprovalsForReferrer(peopleApi, selectedAccount.address);
|
||||
setPendingApps(apps);
|
||||
setIdentities(identityMap);
|
||||
|
||||
if (import.meta.env.DEV) console.log(`Loaded ${apps.length} pending KYC applications`);
|
||||
if (import.meta.env.DEV) console.log(`Loaded ${apps.length} pending referral approvals`);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Error loading pending applications:', error);
|
||||
toast({
|
||||
@@ -116,233 +61,55 @@ export function KycApprovalTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleApprove = async (application: PendingApplication) => {
|
||||
if (!api || !selectedAccount) {
|
||||
const handleApproveReferral = async (applicantAddress: string) => {
|
||||
if (!peopleApi || !selectedAccount) {
|
||||
toast({
|
||||
title: 'Wallet Not Connected',
|
||||
description: 'Please connect your admin wallet first',
|
||||
description: 'Please connect your wallet first',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
setProcessingAddress(applicantAddress);
|
||||
try {
|
||||
const { web3FromAddress } = await import('@pezkuwi/extension-dapp');
|
||||
const injector = await web3FromAddress(selectedAccount.address);
|
||||
const result = await approveReferral(peopleApi, selectedAccount, applicantAddress);
|
||||
|
||||
if (import.meta.env.DEV) console.log('Proposing KYC approval for:', application.address);
|
||||
if (import.meta.env.DEV) console.log('Commission member wallet:', selectedAccount.address);
|
||||
|
||||
// Check if user is a member of DynamicCommissionCollective
|
||||
const members = await api.query.dynamicCommissionCollective.members();
|
||||
const memberList = members.toJSON() as string[];
|
||||
const isMember = memberList.includes(selectedAccount.address);
|
||||
|
||||
if (!isMember) {
|
||||
if (!result.success) {
|
||||
toast({
|
||||
title: 'Not a Commission Member',
|
||||
description: 'You are not a member of the KYC Approval Commission',
|
||||
title: 'Approval Failed',
|
||||
description: result.error || 'Failed to approve referral',
|
||||
variant: 'destructive',
|
||||
});
|
||||
setProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) console.log('✅ User is commission member');
|
||||
|
||||
// Create proposal for KYC approval
|
||||
const proposal = api.tx.identityKyc.approveKyc(application.address);
|
||||
const lengthBound = proposal.encodedLength;
|
||||
|
||||
// Create proposal directly (no proxy needed)
|
||||
if (import.meta.env.DEV) console.log('Creating commission proposal for KYC approval');
|
||||
if (import.meta.env.DEV) console.log('Applicant:', application.address);
|
||||
if (import.meta.env.DEV) console.log('Threshold:', COMMISSIONS.KYC.threshold);
|
||||
|
||||
const tx = api.tx.dynamicCommissionCollective.propose(
|
||||
COMMISSIONS.KYC.threshold,
|
||||
proposal,
|
||||
lengthBound
|
||||
);
|
||||
|
||||
if (import.meta.env.DEV) console.log('Transaction created:', tx.toHuman());
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
tx.signAndSend(
|
||||
selectedAccount.address,
|
||||
{ signer: injector.signer },
|
||||
({ status, dispatchError, events }) => {
|
||||
if (import.meta.env.DEV) console.log('Transaction status:', status.type);
|
||||
|
||||
if (status.isInBlock || status.isFinalized) {
|
||||
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();
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) console.error('Approval error:', errorMessage);
|
||||
toast({
|
||||
title: 'Approval Failed',
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
});
|
||||
reject(new Error(errorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for Proposed event
|
||||
if (import.meta.env.DEV) console.log('All events:', events.map(e => `${e.event.section}.${e.event.method}`));
|
||||
const proposedEvent = events.find(({ event }) =>
|
||||
event.section === 'dynamicCommissionCollective' && event.method === 'Proposed'
|
||||
);
|
||||
|
||||
if (proposedEvent) {
|
||||
if (import.meta.env.DEV) console.log('✅ KYC Approval proposal created');
|
||||
toast({
|
||||
title: 'Proposal Created',
|
||||
description: `KYC approval proposed for ${application.address.slice(0, 8)}... Waiting for other commission members to vote.`,
|
||||
});
|
||||
resolve();
|
||||
} else {
|
||||
if (import.meta.env.DEV) console.warn('Transaction included but no Proposed event');
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
).catch((error) => {
|
||||
if (import.meta.env.DEV) console.error('Failed to sign and send:', error);
|
||||
toast({
|
||||
title: 'Transaction Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to submit transaction',
|
||||
variant: 'destructive',
|
||||
});
|
||||
reject(error);
|
||||
});
|
||||
toast({
|
||||
title: 'Referral Approved',
|
||||
description: `Successfully vouched for ${applicantAddress.slice(0, 8)}...${applicantAddress.slice(-4)}`,
|
||||
});
|
||||
|
||||
// Reload applications after approval
|
||||
setTimeout(() => {
|
||||
loadPendingApplications();
|
||||
setShowDetailsModal(false);
|
||||
setSelectedApp(null);
|
||||
}, 2000);
|
||||
|
||||
// Reload after approval
|
||||
setTimeout(() => loadPendingApplications(), 2000);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Error approving KYC:', error);
|
||||
if (import.meta.env.DEV) console.error('Error approving referral:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to approve KYC',
|
||||
description: error instanceof Error ? error.message : 'Failed to approve referral',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
setProcessingAddress(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = async (application: PendingApplication) => {
|
||||
if (!api || !selectedAccount) {
|
||||
toast({
|
||||
title: 'Wallet Not Connected',
|
||||
description: 'Please connect your admin wallet first',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmReject = window.confirm(
|
||||
`Are you sure you want to REJECT KYC for ${application.address}?\n\nThis will slash their deposit.`
|
||||
);
|
||||
|
||||
if (!confirmReject) return;
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
const { web3FromAddress } = await import('@pezkuwi/extension-dapp');
|
||||
const injector = await web3FromAddress(selectedAccount.address);
|
||||
|
||||
if (import.meta.env.DEV) console.log('Rejecting KYC for:', application.address);
|
||||
|
||||
const tx = api.tx.identityKyc.rejectKyc(application.address);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
tx.signAndSend(
|
||||
selectedAccount.address,
|
||||
{ signer: injector.signer },
|
||||
({ status, dispatchError, events }) => {
|
||||
if (status.isInBlock || status.isFinalized) {
|
||||
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();
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Rejection Failed',
|
||||
description: errorMessage,
|
||||
variant: 'destructive',
|
||||
});
|
||||
reject(new Error(errorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
const rejectedEvent = events.find(({ event }) =>
|
||||
event.section === 'identityKyc' && event.method === 'KycRejected'
|
||||
);
|
||||
|
||||
if (rejectedEvent) {
|
||||
toast({
|
||||
title: 'Rejected',
|
||||
description: `KYC rejected for ${application.address.slice(0, 8)}...`,
|
||||
});
|
||||
resolve();
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
).catch(reject);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
loadPendingApplications();
|
||||
setShowDetailsModal(false);
|
||||
setSelectedApp(null);
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Error rejecting KYC:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'Failed to reject KYC',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openDetailsModal = (app: PendingApplication) => {
|
||||
setSelectedApp(app);
|
||||
setShowDetailsModal(true);
|
||||
};
|
||||
|
||||
if (!isApiReady) {
|
||||
if (!isPeopleReady) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-cyan-500" />
|
||||
<span className="ml-3 text-gray-400">Connecting to blockchain...</span>
|
||||
<span className="ml-3 text-gray-400">Connecting to People Chain...</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -356,7 +123,7 @@ export function KycApprovalTab() {
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Please connect your admin wallet to view and approve KYC applications.
|
||||
Please connect your wallet to view referral approvals.
|
||||
<Button onClick={connectWallet} variant="outline" className="ml-4">
|
||||
Connect Wallet
|
||||
</Button>
|
||||
@@ -368,196 +135,82 @@ export function KycApprovalTab() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Pending KYC Applications</CardTitle>
|
||||
<Button onClick={loadPendingApplications} variant="outline" size="sm" disabled={loading}>
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Refresh'}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-cyan-500" />
|
||||
</div>
|
||||
) : pendingApps.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-3" />
|
||||
<p className="text-gray-400">No pending applications</p>
|
||||
<p className="text-sm text-gray-600 mt-2">All KYC applications have been processed</p>
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Pending Referral Approvals</CardTitle>
|
||||
<Button onClick={loadPendingApplications} variant="outline" size="sm" disabled={loading}>
|
||||
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Refresh'}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-cyan-500" />
|
||||
</div>
|
||||
) : pendingApps.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-3" />
|
||||
<p className="text-gray-400">No pending approvals</p>
|
||||
<p className="text-sm text-gray-600 mt-2">No one is waiting for your referral approval</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
These users listed you as their referrer. Approve to vouch for their identity.
|
||||
</p>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Applicant</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Documents</TableHead>
|
||||
<TableHead>Identity Hash</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{pendingApps.map((app) => {
|
||||
const identity = identities.get(app.address);
|
||||
return (
|
||||
<TableRow key={app.address}>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{app.address.slice(0, 6)}...{app.address.slice(-4)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-gray-400" />
|
||||
{identity?.name || 'Loading...'}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="w-4 h-4 text-gray-400" />
|
||||
{identity?.email || 'Loading...'}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
{app.cids.length} CID(s)
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30">
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
Pending
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => openDetailsModal(app)}
|
||||
>
|
||||
View Details
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{pendingApps.map((app) => (
|
||||
<TableRow key={app.applicantAddress}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-gray-400" />
|
||||
<span className="font-mono text-xs">
|
||||
{app.applicantAddress.slice(0, 8)}...{app.applicantAddress.slice(-6)}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="font-mono text-xs text-gray-500">
|
||||
{app.identityHash ? `${app.identityHash.slice(0, 12)}...` : 'N/A'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30">
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
Pending Referral
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleApproveReferral(app.applicantAddress)}
|
||||
disabled={processingAddress === app.applicantAddress}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{processingAddress === app.applicantAddress ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Approve
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Details Modal */}
|
||||
<Dialog open={showDetailsModal} onOpenChange={setShowDetailsModal}>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>KYC Application Details</DialogTitle>
|
||||
<DialogDescription>
|
||||
Review application before approving or rejecting
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedApp && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-gray-400">Applicant Address</Label>
|
||||
<p className="font-mono text-sm mt-1">{selectedApp.address}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-400">Name</Label>
|
||||
<p className="text-sm mt-1">{identities.get(selectedApp.address)?.name || 'Unknown'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-400">Email</Label>
|
||||
<p className="text-sm mt-1">{identities.get(selectedApp.address)?.email || 'No email'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-gray-400">Application Time</Label>
|
||||
<p className="text-sm mt-1">
|
||||
{selectedApp.timestamp
|
||||
? new Date(selectedApp.timestamp).toLocaleString()
|
||||
: 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-gray-400">Notes</Label>
|
||||
<p className="text-sm mt-1 p-3 bg-gray-800/50 rounded border border-gray-700">
|
||||
{selectedApp.notes}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-gray-400">IPFS Documents ({selectedApp.cids.length})</Label>
|
||||
<div className="mt-2 space-y-2">
|
||||
{selectedApp.cids.map((cid, index) => (
|
||||
<div key={index} className="p-2 bg-gray-800/50 rounded border border-gray-700">
|
||||
<p className="font-mono text-xs">{cid}</p>
|
||||
<a
|
||||
href={`https://ipfs.io/ipfs/${cid}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-cyan-400 hover:text-cyan-300 text-xs"
|
||||
>
|
||||
View on IPFS →
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Alert className="bg-yellow-500/10 border-yellow-500/30">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription className="text-sm">
|
||||
<strong>Important:</strong> Approving this application will:
|
||||
<ul className="list-disc list-inside mt-2 space-y-1">
|
||||
<li>Unreserve the applicant's deposit</li>
|
||||
<li>Mint a Welati (Citizen) NFT automatically</li>
|
||||
<li>Enable trust score tracking</li>
|
||||
<li>Grant governance voting rights</li>
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDetailsModal(false)}
|
||||
disabled={processing}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => selectedApp && handleReject(selectedApp)}
|
||||
disabled={processing}
|
||||
>
|
||||
{processing ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <XCircle className="w-4 h-4 mr-2" />}
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => selectedApp && handleApprove(selectedApp)}
|
||||
disabled={processing}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{processing ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <CheckCircle className="w-4 h-4 mr-2" />}
|
||||
Approve
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function Label({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return <label className={`text-sm font-medium ${className}`}>{children}</label>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user