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:
2026-02-16 02:56:27 +03:00
parent e9f01685d0
commit e9d5fef39a
6 changed files with 728 additions and 743 deletions
+286 -132
View File
@@ -40,7 +40,7 @@ const web3FromAddress = async (address: string): Promise<InjectedExtension> => {
// TYPE DEFINITIONS // TYPE DEFINITIONS
// ======================================== // ========================================
export type KycStatus = 'NotStarted' | 'Pending' | 'Approved' | 'Rejected'; export type KycStatus = 'NotStarted' | 'PendingReferral' | 'ReferrerApproved' | 'Approved' | 'Revoked';
export type Region = export type Region =
| 'bakur' // North (Turkey) | 'bakur' // North (Turkey)
@@ -107,7 +107,7 @@ export interface CitizenshipStatus {
tikiNumber?: string; tikiNumber?: string;
stakingScoreTracking: boolean; stakingScoreTracking: boolean;
ipfsCid?: string; ipfsCid?: string;
nextAction: 'APPLY_KYC' | 'CLAIM_TIKI' | 'START_TRACKING' | 'COMPLETE'; nextAction: 'APPLY_KYC' | 'WAIT_REFERRER' | 'CONFIRM' | 'CLAIM_TIKI' | 'START_TRACKING' | 'COMPLETE';
} }
// ======================================== // ========================================
@@ -122,28 +122,37 @@ export async function getKycStatus(
address: string address: string
): Promise<KycStatus> { ): Promise<KycStatus> {
try { try {
// MOCK FOR DEV: Alice is Approved
if (address === '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY') {
return 'Approved';
}
if (!api?.query?.identityKyc) { if (!api?.query?.identityKyc) {
if (import.meta.env.DEV) console.log('Identity KYC pallet not available on this chain'); if (import.meta.env.DEV) console.log('Identity KYC pallet not available on this chain');
return 'NotStarted'; return 'NotStarted';
} }
const status = await api.query.identityKyc.kycStatuses(address); // Check Applications storage (new pallet API)
if (api.query.identityKyc.applications) {
const application = await api.query.identityKyc.applications(address);
if (status.isEmpty) { if (!application.isEmpty) {
return 'NotStarted'; const appData = application.toJSON() as Record<string, unknown>;
const status = appData.status as string | undefined;
if (status === 'PendingReferral') return 'PendingReferral';
if (status === 'ReferrerApproved') return 'ReferrerApproved';
if (status === 'Approved') return 'Approved';
if (status === 'Revoked') return 'Revoked';
}
} }
const statusStr = status.toString(); // Fallback: check kycStatuses if applications storage doesn't exist
if (api.query.identityKyc.kycStatuses) {
// Map on-chain status to our type const status = await api.query.identityKyc.kycStatuses(address);
if (statusStr === 'Approved') return 'Approved'; if (!status.isEmpty) {
if (statusStr === 'Pending') return 'Pending'; const statusStr = status.toString();
if (statusStr === 'Rejected') return 'Rejected'; if (statusStr === 'Approved') return 'Approved';
if (statusStr === 'PendingReferral') return 'PendingReferral';
if (statusStr === 'ReferrerApproved') return 'ReferrerApproved';
if (statusStr === 'Revoked') return 'Revoked';
}
}
return 'NotStarted'; return 'NotStarted';
} catch (error) { } catch (error) {
@@ -160,12 +169,15 @@ export async function hasPendingApplication(
address: string address: string
): Promise<boolean> { ): Promise<boolean> {
try { try {
if (!api?.query?.identityKyc?.pendingKycApplications) { if (api?.query?.identityKyc?.applications) {
return false; const application = await api.query.identityKyc.applications(address);
if (!application.isEmpty) {
const appData = application.toJSON() as Record<string, unknown>;
const status = appData.status as string | undefined;
return status === 'PendingReferral' || status === 'ReferrerApproved';
}
} }
return false;
const application = await api.query.identityKyc.pendingKycApplications(address);
return !application.isEmpty;
} catch (error) { } catch (error) {
console.error('Error checking pending application:', error); console.error('Error checking pending application:', error);
return false; return false;
@@ -344,15 +356,18 @@ export async function getCitizenshipStatus(
isStakingScoreTracking(api, address) isStakingScoreTracking(api, address)
]); ]);
const kycApproved = kycStatus === 'Approved';
const hasTiki = citizenCheck.hasTiki; const hasTiki = citizenCheck.hasTiki;
// Determine next action // Determine next action based on workflow state
let nextAction: CitizenshipStatus['nextAction']; let nextAction: CitizenshipStatus['nextAction'];
if (!kycApproved) { if (kycStatus === 'NotStarted' || kycStatus === 'Revoked') {
nextAction = 'APPLY_KYC'; nextAction = 'APPLY_KYC';
} else if (!hasTiki) { } else if (kycStatus === 'PendingReferral') {
nextAction = 'WAIT_REFERRER';
} else if (kycStatus === 'ReferrerApproved') {
nextAction = 'CONFIRM';
} else if (kycStatus === 'Approved' && !hasTiki) {
nextAction = 'CLAIM_TIKI'; nextAction = 'CLAIM_TIKI';
} else if (!stakingTracking) { } else if (!stakingTracking) {
nextAction = 'START_TRACKING'; nextAction = 'START_TRACKING';
@@ -453,172 +468,113 @@ export async function validateReferralCode(
// ======================================== // ========================================
/** /**
* Submit KYC application to blockchain * Submit citizenship application to blockchain
* This is a two-step process: * Single call: applyForCitizenship(identity_hash, referrer)
* 1. Set identity (name, email) * Requires 1 HEZ deposit (reserved by pallet automatically)
* 2. Apply for KYC (IPFS CID, notes)
*/ */
export async function submitKycApplication( export async function submitKycApplication(
api: ApiPromise, api: ApiPromise,
account: InjectedAccountWithMeta, account: InjectedAccountWithMeta,
name: string, identityHash: string,
email: string, referrerAddress?: string
ipfsCid: string,
notes: string = 'Citizenship application'
): Promise<{ success: boolean; error?: string; blockHash?: string }> { ): Promise<{ success: boolean; error?: string; blockHash?: string }> {
try { try {
if (!api?.tx?.identityKyc?.setIdentity || !api?.tx?.identityKyc?.applyForKyc) { if (!api?.tx?.identityKyc?.applyForCitizenship) {
return { success: false, error: 'Identity KYC pallet not available' }; return { success: false, error: 'Identity KYC pallet not available' };
} }
// Check if user already has a pending KYC application // Check if user already has a pending application
const pendingApp = await api.query.identityKyc.pendingKycApplications(account.address); const hasPending = await hasPendingApplication(api, account.address);
if (!pendingApp.isEmpty) { if (hasPending) {
console.log('⚠️ User already has a pending KYC application');
return { return {
success: false, success: false,
error: 'You already have a pending citizenship application. Please wait for approval.' error: 'You already have a pending citizenship application. Please wait for referrer approval.'
}; };
} }
// Check if user is already approved // Check if user is already approved
const kycStatus = await api.query.identityKyc.kycStatuses(account.address); const currentStatus = await getKycStatus(api, account.address);
if (kycStatus.toString() === 'Approved') { if (currentStatus === 'Approved') {
console.log('✅ User KYC is already approved');
return { return {
success: false, success: false,
error: 'Your citizenship application is already approved!' error: 'Your citizenship application is already approved!'
}; };
} }
// Get the injector for signing
const injector = await web3FromAddress(account.address); const injector = await web3FromAddress(account.address);
// Debug logging if (import.meta.env.DEV) {
console.log('=== submitKycApplication Debug ==='); console.log('=== submitKycApplication Debug ===');
console.log('account.address:', account.address); console.log('account.address:', account.address);
console.log('name:', name); console.log('identityHash:', identityHash);
console.log('email:', email); console.log('referrerAddress:', referrerAddress || '(default referrer)');
console.log('ipfsCid:', ipfsCid); console.log('===================================');
console.log('notes:', notes);
console.log('===================================');
// Ensure ipfsCid is a string
const cidString = String(ipfsCid);
if (!cidString || cidString === 'undefined' || cidString === '[object Object]') {
return { success: false, error: `Invalid IPFS CID received: ${cidString}` };
} }
// Step 1: Set identity first // Single call: applyForCitizenship(identity_hash, referrer)
console.log('Step 1: Setting identity...'); // referrer is Option<AccountId> - null means pallet uses DefaultReferrer
const identityResult = await new Promise<{ success: boolean; error?: string }>((resolve, reject) => { const referrerParam = referrerAddress || null;
api.tx.identityKyc
.setIdentity(name, email)
.signAndSend(account.address, { signer: injector.signer }, ({ status, dispatchError, events }) => {
console.log('Identity transaction status:', status.type);
if (status.isInBlock || status.isFinalized) {
if (dispatchError) {
let errorMessage = 'Identity 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('Identity transaction error:', errorMessage);
resolve({ success: false, error: errorMessage });
return;
}
// Check for IdentitySet event
const identitySetEvent = events.find(({ event }) =>
event.section === 'identityKyc' && event.method === 'IdentitySet'
);
if (identitySetEvent) {
console.log('✅ Identity set successfully');
resolve({ success: true });
} else {
resolve({ success: true }); // Still consider it success if in block
}
}
})
.catch((error) => {
console.error('Failed to sign and send identity transaction:', error);
reject(error);
});
});
if (!identityResult.success) {
return identityResult;
}
// Step 2: Apply for KYC
console.log('Step 2: Applying for KYC...');
const result = await new Promise<{ success: boolean; error?: string; blockHash?: string }>((resolve, reject) => { const result = await new Promise<{ success: boolean; error?: string; blockHash?: string }>((resolve, reject) => {
api.tx.identityKyc api.tx.identityKyc
.applyForKyc([cidString], notes) .applyForCitizenship(identityHash, referrerParam)
.signAndSend(account.address, { signer: injector.signer }, ({ status, dispatchError, events }) => { .signAndSend(account.address, { signer: injector.signer }, ({ status, dispatchError, events }) => {
console.log('Transaction status:', status.type); if (import.meta.env.DEV) console.log('Transaction status:', status.type);
if (status.isInBlock || status.isFinalized) { if (status.isInBlock || status.isFinalized) {
if (dispatchError) { if (dispatchError) {
let errorMessage = 'Transaction failed'; let errorMessage = 'Transaction failed';
if (dispatchError.isModule) { if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule); const decoded = api.registry.findMetaError(dispatchError.asModule);
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`; errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
} else { } else {
errorMessage = dispatchError.toString(); errorMessage = dispatchError.toString();
} }
if (import.meta.env.DEV) console.error('Transaction error:', errorMessage);
console.error('Transaction error:', errorMessage);
resolve({ success: false, error: errorMessage }); resolve({ success: false, error: errorMessage });
return; return;
} }
// Check for KycApplied event const appliedEvent = events.find(({ event }: any) =>
const kycAppliedEvent = events.find(({ event }) => event.section === 'identityKyc' && event.method === 'CitizenshipApplied'
event.section === 'identityKyc' && event.method === 'KycApplied'
); );
if (kycAppliedEvent) { if (appliedEvent) {
console.log('✅ KYC Application submitted successfully'); if (import.meta.env.DEV) console.log('Citizenship application submitted successfully');
resolve({
success: true,
blockHash: status.asInBlock.toString()
});
} else {
console.warn('Transaction included but KycApplied event not found');
resolve({ success: true });
} }
resolve({
success: true,
blockHash: status.isInBlock ? status.asInBlock.toString() : undefined
});
} }
}) })
.catch((error) => { .catch((error: any) => {
console.error('Failed to sign and send transaction:', error); if (import.meta.env.DEV) console.error('Failed to sign and send transaction:', error);
reject(error); reject(error);
}); });
}); });
return result; return result;
} catch (error: any) { } catch (error: any) {
console.error('Error submitting KYC application:', error); console.error('Error submitting citizenship application:', error);
return { return {
success: false, success: false,
error: error.message || 'Failed to submit KYC application' error: error.message || 'Failed to submit citizenship application'
}; };
} }
} }
/** /**
* Subscribe to KYC approval events for an address * Subscribe to citizenship-related events for an address
* Listens for ReferralApproved and CitizenshipConfirmed
*/ */
export function subscribeToKycApproval( export function subscribeToKycApproval(
api: ApiPromise, api: ApiPromise,
address: string, address: string,
onApproved: () => void, onApproved: () => void,
onError?: (error: string) => void onError?: (error: string) => void,
onReferralApproved?: () => void
): () => void { ): () => void {
try { try {
if (!api?.query?.system?.events) { if (!api?.query?.system?.events) {
@@ -633,11 +589,20 @@ export function subscribeToKycApproval(
events.forEach((record: any) => { events.forEach((record: any) => {
const { event } = record; const { event } = record;
if (event.section === 'identityKyc' && event.method === 'KycApproved') { // Referrer approved the application
const [approvedAddress] = event.data; if (event.section === 'identityKyc' && event.method === 'ReferralApproved') {
const [applicantAddress] = event.data;
if (applicantAddress.toString() === address) {
if (import.meta.env.DEV) console.log('Referral approved for:', address);
if (onReferralApproved) onReferralApproved();
}
}
if (approvedAddress.toString() === address) { // Citizenship fully confirmed (NFT minted)
console.log('✅ KYC Approved for:', address); if (event.section === 'identityKyc' && event.method === 'CitizenshipConfirmed') {
const [confirmedAddress] = event.data;
if (confirmedAddress.toString() === address) {
if (import.meta.env.DEV) console.log('Citizenship confirmed for:', address);
onApproved(); onApproved();
} }
} }
@@ -646,17 +611,206 @@ export function subscribeToKycApproval(
return unsubscribe as unknown as () => void; return unsubscribe as unknown as () => void;
} catch (error: any) { } catch (error: any) {
console.error('Error subscribing to KYC approval:', error); console.error('Error subscribing to citizenship events:', error);
if (onError) onError(error.message || 'Failed to subscribe to approval events'); if (onError) onError(error.message || 'Failed to subscribe to events');
return () => {}; return () => {};
} }
} }
// ========================================
// REFERRER ACTIONS
// ========================================
/**
* Approve a referral as a referrer
* Called by the referrer to vouch for an applicant
*/
export async function approveReferral(
api: ApiPromise,
account: InjectedAccountWithMeta,
applicantAddress: string
): Promise<{ success: boolean; error?: string; blockHash?: string }> {
try {
if (!api?.tx?.identityKyc?.approveReferral) {
return { success: false, error: 'Identity KYC pallet not available' };
}
const injector = await web3FromAddress(account.address);
const result = await new Promise<{ success: boolean; error?: string; blockHash?: string }>((resolve, reject) => {
api.tx.identityKyc
.approveReferral(applicantAddress)
.signAndSend(account.address, { signer: injector.signer }, ({ status, dispatchError, events }) => {
if (import.meta.env.DEV) console.log('Approve referral tx 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();
}
resolve({ success: false, error: errorMessage });
return;
}
resolve({
success: true,
blockHash: status.isInBlock ? status.asInBlock.toString() : undefined
});
}
})
.catch((error: any) => reject(error));
});
return result;
} catch (error: any) {
console.error('Error approving referral:', error);
return { success: false, error: error.message || 'Failed to approve referral' };
}
}
/**
* Cancel a pending citizenship application
* Called by the applicant to withdraw and get deposit back
*/
export async function cancelApplication(
api: ApiPromise,
account: InjectedAccountWithMeta
): Promise<{ success: boolean; error?: string }> {
try {
if (!api?.tx?.identityKyc?.cancelApplication) {
return { success: false, error: 'Identity KYC pallet not available' };
}
const injector = await web3FromAddress(account.address);
const result = await new Promise<{ success: boolean; error?: string }>((resolve, reject) => {
api.tx.identityKyc
.cancelApplication()
.signAndSend(account.address, { signer: injector.signer }, ({ status, dispatchError }) => {
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();
}
resolve({ success: false, error: errorMessage });
return;
}
resolve({ success: true });
}
})
.catch((error: any) => reject(error));
});
return result;
} catch (error: any) {
console.error('Error canceling application:', error);
return { success: false, error: error.message || 'Failed to cancel application' };
}
}
/**
* Confirm citizenship after referrer approval
* Called by the applicant to mint the Welati Tiki NFT
*/
export async function confirmCitizenship(
api: ApiPromise,
account: InjectedAccountWithMeta
): Promise<{ success: boolean; error?: string; blockHash?: string }> {
try {
if (!api?.tx?.identityKyc?.confirmCitizenship) {
return { success: false, error: 'Identity KYC pallet not available' };
}
const injector = await web3FromAddress(account.address);
const result = await new Promise<{ success: boolean; error?: string; blockHash?: string }>((resolve, reject) => {
api.tx.identityKyc
.confirmCitizenship()
.signAndSend(account.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();
}
resolve({ success: false, error: errorMessage });
return;
}
resolve({
success: true,
blockHash: status.isInBlock ? status.asInBlock.toString() : undefined
});
}
})
.catch((error: any) => reject(error));
});
return result;
} catch (error: any) {
console.error('Error confirming citizenship:', error);
return { success: false, error: error.message || 'Failed to confirm citizenship' };
}
}
export interface PendingApproval {
applicantAddress: string;
identityHash: string;
}
/**
* Get pending approvals where current user is the referrer
*/
export async function getPendingApprovalsForReferrer(
api: ApiPromise,
referrerAddress: string
): Promise<PendingApproval[]> {
try {
if (!api?.query?.identityKyc?.applications) {
return [];
}
const entries = await api.query.identityKyc.applications.entries();
const pending: PendingApproval[] = [];
for (const [key, value] of entries) {
const applicantAddress = key.args[0].toString();
const appData = (value as any).toJSON() as Record<string, unknown>;
if (
appData.status === 'PendingReferral' &&
appData.referrer?.toString() === referrerAddress
) {
pending.push({
applicantAddress,
identityHash: (appData.identityHash as string) || ''
});
}
}
return pending;
} catch (error) {
console.error('Error fetching pending approvals for referrer:', error);
return [];
}
}
// ======================================== // ========================================
// FOUNDER ADDRESS // FOUNDER ADDRESS
// ======================================== // ========================================
export const FOUNDER_ADDRESS = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'; // Satoshi Qazi Muhammed export const FOUNDER_ADDRESS = '5CyuFfbF95rzBxru7c9yEsX4XmQXUxpLUcbj9RLg9K1cGiiF'; // Satoshi Qazi Muhammed
export interface AuthChallenge { export interface AuthChallenge {
message: string; message: string;
+99 -446
View File
@@ -4,8 +4,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { Loader2, CheckCircle, XCircle, Clock, User, Mail, FileText, AlertTriangle } from 'lucide-react'; import { Loader2, CheckCircle, Clock, User, AlertTriangle } from 'lucide-react';
import { COMMISSIONS } from '@/config/commissions';
import { import {
Table, Table,
TableBody, TableBody,
@@ -14,96 +13,42 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import { approveReferral, getPendingApprovalsForReferrer } from '@pezkuwi/lib/citizenship-workflow';
interface PendingApplication { import type { PendingApproval } from '@pezkuwi/lib/citizenship-workflow';
address: string;
cids: string[];
notes: string;
timestamp?: number;
}
interface IdentityInfo {
name: string;
email: string;
}
export function KycApprovalTab() { export function KycApprovalTab() {
// identityKyc pallet is on People Chain, dynamicCommissionCollective is on Relay Chain // identityKyc pallet is on People Chain - use peopleApi
const { api, isApiReady, peopleApi, isPeopleReady, selectedAccount, connectWallet } = usePezkuwi(); const { peopleApi, isPeopleReady, selectedAccount, connectWallet } = usePezkuwi();
const { toast } = useToast(); const { toast } = useToast();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [pendingApps, setPendingApps] = useState<PendingApplication[]>([]); const [pendingApps, setPendingApps] = useState<PendingApproval[]>([]);
const [identities, setIdentities] = useState<Map<string, IdentityInfo>>(new Map()); const [processingAddress, setProcessingAddress] = useState<string | null>(null);
const [selectedApp, setSelectedApp] = useState<PendingApplication | null>(null);
const [processing, setProcessing] = useState(false);
const [showDetailsModal, setShowDetailsModal] = useState(false);
// Load pending KYC applications from People Chain // Load pending applications where current user is the referrer
useEffect(() => { useEffect(() => {
if (!peopleApi || !isPeopleReady) { if (!peopleApi || !isPeopleReady || !selectedAccount) {
setLoading(false);
return; return;
} }
loadPendingApplications(); loadPendingApplications();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [peopleApi, isPeopleReady]); }, [peopleApi, isPeopleReady, selectedAccount]);
const loadPendingApplications = async () => { const loadPendingApplications = async () => {
// identityKyc pallet is on People Chain if (!peopleApi || !isPeopleReady || !selectedAccount) {
if (!peopleApi || !isPeopleReady) {
setLoading(false); setLoading(false);
return; return;
} }
setLoading(true); setLoading(true);
try { try {
// Get all pending applications from People Chain const apps = await getPendingApprovalsForReferrer(peopleApi, selectedAccount.address);
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()
});
}
setPendingApps(apps); 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) { } catch (error) {
if (import.meta.env.DEV) console.error('Error loading pending applications:', error); if (import.meta.env.DEV) console.error('Error loading pending applications:', error);
toast({ toast({
@@ -116,233 +61,55 @@ export function KycApprovalTab() {
} }
}; };
const handleApprove = async (application: PendingApplication) => { const handleApproveReferral = async (applicantAddress: string) => {
if (!api || !selectedAccount) { if (!peopleApi || !selectedAccount) {
toast({ toast({
title: 'Wallet Not Connected', title: 'Wallet Not Connected',
description: 'Please connect your admin wallet first', description: 'Please connect your wallet first',
variant: 'destructive', variant: 'destructive',
}); });
return; return;
} }
setProcessing(true); setProcessingAddress(applicantAddress);
try { try {
const { web3FromAddress } = await import('@pezkuwi/extension-dapp'); const result = await approveReferral(peopleApi, selectedAccount, applicantAddress);
const injector = await web3FromAddress(selectedAccount.address);
if (import.meta.env.DEV) console.log('Proposing KYC approval for:', application.address); if (!result.success) {
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) {
toast({ toast({
title: 'Not a Commission Member', title: 'Approval Failed',
description: 'You are not a member of the KYC Approval Commission', description: result.error || 'Failed to approve referral',
variant: 'destructive', variant: 'destructive',
}); });
setProcessing(false);
return; return;
} }
if (import.meta.env.DEV) console.log('✅ User is commission member'); toast({
title: 'Referral Approved',
// Create proposal for KYC approval description: `Successfully vouched for ${applicantAddress.slice(0, 8)}...${applicantAddress.slice(-4)}`,
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);
});
}); });
// Reload applications after approval // Reload after approval
setTimeout(() => { setTimeout(() => loadPendingApplications(), 2000);
loadPendingApplications();
setShowDetailsModal(false);
setSelectedApp(null);
}, 2000);
} catch (error) { } 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({ toast({
title: 'Error', title: 'Error',
description: error instanceof Error ? error.message : 'Failed to approve KYC', description: error instanceof Error ? error.message : 'Failed to approve referral',
variant: 'destructive', variant: 'destructive',
}); });
} finally { } finally {
setProcessing(false); setProcessingAddress(null);
} }
}; };
const handleReject = async (application: PendingApplication) => { if (!isPeopleReady) {
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) {
return ( return (
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-cyan-500" /> <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> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -356,7 +123,7 @@ export function KycApprovalTab() {
<Alert> <Alert>
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
<AlertDescription> <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"> <Button onClick={connectWallet} variant="outline" className="ml-4">
Connect Wallet Connect Wallet
</Button> </Button>
@@ -368,196 +135,82 @@ export function KycApprovalTab() {
} }
return ( return (
<> <Card>
<Card> <CardHeader className="flex flex-row items-center justify-between">
<CardHeader className="flex flex-row items-center justify-between"> <CardTitle>Pending Referral Approvals</CardTitle>
<CardTitle>Pending KYC Applications</CardTitle> <Button onClick={loadPendingApplications} variant="outline" size="sm" disabled={loading}>
<Button onClick={loadPendingApplications} variant="outline" size="sm" disabled={loading}> {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Refresh'}
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Refresh'} </Button>
</Button> </CardHeader>
</CardHeader> <CardContent>
<CardContent> {loading ? (
{loading ? ( <div className="flex items-center justify-center py-12">
<div className="flex items-center justify-center py-12"> <Loader2 className="w-8 h-8 animate-spin text-cyan-500" />
<Loader2 className="w-8 h-8 animate-spin text-cyan-500" /> </div>
</div> ) : pendingApps.length === 0 ? (
) : pendingApps.length === 0 ? ( <div className="text-center py-12">
<div className="text-center py-12"> <CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-3" />
<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-gray-400">No pending applications</p> <p className="text-sm text-gray-600 mt-2">No one is waiting for your referral approval</p>
<p className="text-sm text-gray-600 mt-2">All KYC applications have been processed</p> </div>
</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> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Applicant</TableHead> <TableHead>Applicant</TableHead>
<TableHead>Name</TableHead> <TableHead>Identity Hash</TableHead>
<TableHead>Email</TableHead>
<TableHead>Documents</TableHead>
<TableHead>Status</TableHead> <TableHead>Status</TableHead>
<TableHead>Actions</TableHead> <TableHead>Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{pendingApps.map((app) => { {pendingApps.map((app) => (
const identity = identities.get(app.address); <TableRow key={app.applicantAddress}>
return ( <TableCell>
<TableRow key={app.address}> <div className="flex items-center gap-2">
<TableCell className="font-mono text-xs"> <User className="w-4 h-4 text-gray-400" />
{app.address.slice(0, 6)}...{app.address.slice(-4)} <span className="font-mono text-xs">
</TableCell> {app.applicantAddress.slice(0, 8)}...{app.applicantAddress.slice(-6)}
<TableCell> </span>
<div className="flex items-center gap-2"> </div>
<User className="w-4 h-4 text-gray-400" /> </TableCell>
{identity?.name || 'Loading...'} <TableCell>
</div> <span className="font-mono text-xs text-gray-500">
</TableCell> {app.identityHash ? `${app.identityHash.slice(0, 12)}...` : 'N/A'}
<TableCell> </span>
<div className="flex items-center gap-2"> </TableCell>
<Mail className="w-4 h-4 text-gray-400" /> <TableCell>
{identity?.email || 'Loading...'} <Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30">
</div> <Clock className="w-3 h-3 mr-1" />
</TableCell> Pending Referral
<TableCell> </Badge>
<Badge variant="outline"> </TableCell>
<FileText className="w-3 h-3 mr-1" /> <TableCell>
{app.cids.length} CID(s) <Button
</Badge> size="sm"
</TableCell> onClick={() => handleApproveReferral(app.applicantAddress)}
<TableCell> disabled={processingAddress === app.applicantAddress}
<Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30"> className="bg-green-600 hover:bg-green-700"
<Clock className="w-3 h-3 mr-1" /> >
Pending {processingAddress === app.applicantAddress ? (
</Badge> <Loader2 className="w-4 h-4 animate-spin mr-2" />
</TableCell> ) : (
<TableCell> <CheckCircle className="w-4 h-4 mr-2" />
<div className="flex gap-2"> )}
<Button Approve
size="sm" </Button>
variant="outline" </TableCell>
onClick={() => openDetailsModal(app)} </TableRow>
> ))}
View Details
</Button>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody> </TableBody>
</Table> </Table>
)} </>
</CardContent> )}
</Card> </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&apos;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>
</>
); );
} }
function Label({ children, className }: { children: React.ReactNode; className?: string }) {
return <label className={`text-sm font-medium ${className}`}>{children}</label>;
}
@@ -10,9 +10,10 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Loader2, AlertTriangle, CheckCircle, User, Users as UsersIcon, MapPin, Briefcase, Mail, Check, X, AlertCircle } from 'lucide-react'; import { Loader2, AlertTriangle, CheckCircle, User, Users as UsersIcon, MapPin, Briefcase, Mail, Check, X, AlertCircle } from 'lucide-react';
import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { usePezkuwi } from '@/contexts/PezkuwiContext';
import type { CitizenshipData, Region, MaritalStatus } from '@pezkuwi/lib/citizenship-workflow'; import { blake2AsHex } from '@pezkuwi/util-crypto';
import { FOUNDER_ADDRESS, submitKycApplication, subscribeToKycApproval, getKycStatus } from '@pezkuwi/lib/citizenship-workflow'; import type { CitizenshipData, Region, MaritalStatus, KycStatus } from '@pezkuwi/lib/citizenship-workflow';
import { generateCommitmentHash, generateNullifierHash, encryptData, saveLocalCitizenshipData, uploadToIPFS } from '@pezkuwi/lib/citizenship-workflow'; import { FOUNDER_ADDRESS, submitKycApplication, subscribeToKycApproval, getKycStatus, cancelApplication, confirmCitizenship } from '@pezkuwi/lib/citizenship-workflow';
import { encryptData, saveLocalCitizenshipData, uploadToIPFS } from '@pezkuwi/lib/citizenship-workflow';
interface NewCitizenApplicationProps { interface NewCitizenApplicationProps {
onClose: () => void; onClose: () => void;
@@ -28,83 +29,75 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false); const [submitted, setSubmitted] = useState(false);
const [waitingForApproval, setWaitingForApproval] = useState(false); const [currentStatus, setCurrentStatus] = useState<KycStatus>('NotStarted');
const [kycApproved, setKycApproved] = useState(false); const [kycApproved, setKycApproved] = useState(false);
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 [confirming, setConfirming] = useState(false); const [confirming, setConfirming] = useState(false);
const [canceling, setCanceling] = useState(false);
const [applicationHash, setApplicationHash] = useState<string>(''); const [applicationHash, setApplicationHash] = useState<string>('');
const maritalStatus = watch('maritalStatus'); const maritalStatus = watch('maritalStatus');
const childrenCount = watch('childrenCount'); const childrenCount = watch('childrenCount');
const handleApprove = async () => { const handleConfirmCitizenship = async () => {
// identityKyc pallet is on People Chain
if (!peopleApi || !isPeopleReady || !selectedAccount) { if (!peopleApi || !isPeopleReady || !selectedAccount) {
setError('Please connect your wallet and wait for People Chain connection'); setError('Please connect your wallet and wait for People Chain connection');
return; return;
} }
setConfirming(true); setConfirming(true);
setError(null);
try { try {
const { web3FromAddress } = await import('@pezkuwi/extension-dapp'); const result = await confirmCitizenship(peopleApi, selectedAccount);
const injector = await web3FromAddress(selectedAccount.address);
if (import.meta.env.DEV) console.log('Confirming citizenship application on People Chain (self-confirmation)...'); if (!result.success) {
setError(result.error || 'Failed to confirm citizenship');
setConfirming(false);
return;
}
// Call confirm_citizenship() extrinsic on People Chain - self-confirmation for Welati Tiki setKycApproved(true);
const tx = peopleApi.tx.identityKyc.confirmCitizenship(); setCurrentStatus('Approved');
await tx.signAndSend(selectedAccount.address, { signer: injector.signer }, ({ status, events, dispatchError }) => {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = peopleApi.registry.findMetaError(dispatchError.asModule);
if (import.meta.env.DEV) console.error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`);
setError(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`);
} else {
if (import.meta.env.DEV) console.error(dispatchError.toString());
setError(dispatchError.toString());
}
setConfirming(false);
return;
}
if (status.isInBlock || status.isFinalized) {
if (import.meta.env.DEV) console.log('✅ Citizenship confirmed successfully on People Chain!');
if (import.meta.env.DEV) console.log('Block hash:', status.asInBlock || status.asFinalized);
// Check for CitizenshipConfirmed event
events.forEach(({ event }) => {
if (event.section === 'identityKyc' && event.method === 'CitizenshipConfirmed') {
if (import.meta.env.DEV) 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);
}
});
setTimeout(() => {
onClose();
window.location.href = '/dashboard';
}, 2000);
} catch (err) { } catch (err) {
if (import.meta.env.DEV) console.error('Approval error:', err); if (import.meta.env.DEV) console.error('Confirmation error:', err);
setError((err as Error).message || 'Failed to approve application'); setError((err as Error).message || 'Failed to confirm citizenship');
} finally {
setConfirming(false); setConfirming(false);
} }
}; };
const handleReject = async () => { const handleCancelApplication = async () => {
// Cancel/withdraw the application - simply close modal and go back if (!peopleApi || !isPeopleReady || !selectedAccount) {
// No blockchain interaction needed - application will remain Pending until confirmed or admin-rejected setError('Please connect your wallet and wait for People Chain connection');
if (import.meta.env.DEV) console.log('Canceling citizenship application (no blockchain interaction)'); return;
onClose(); }
window.location.href = '/';
setCanceling(true);
setError(null);
try {
const result = await cancelApplication(peopleApi, selectedAccount);
if (!result.success) {
setError(result.error || 'Failed to cancel application');
setCanceling(false);
return;
}
if (import.meta.env.DEV) console.log('Application canceled, deposit returned');
onClose();
window.location.href = '/';
} catch (err) {
if (import.meta.env.DEV) console.error('Cancel error:', err);
setError((err as Error).message || 'Failed to cancel application');
} finally {
setCanceling(false);
}
}; };
// Check KYC status on mount (identityKyc pallet is on People Chain) // Check KYC status on mount (identityKyc pallet is on People Chain)
@@ -117,19 +110,14 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
try { try {
const status = await getKycStatus(peopleApi, selectedAccount.address); const status = await getKycStatus(peopleApi, selectedAccount.address);
if (import.meta.env.DEV) console.log('Current KYC Status from People Chain:', status); if (import.meta.env.DEV) console.log('Current KYC Status from People Chain:', status);
setCurrentStatus(status);
if (status === 'Approved') { if (status === 'Approved') {
if (import.meta.env.DEV) console.log('KYC already approved! Redirecting to dashboard...');
setKycApproved(true); setKycApproved(true);
// Redirect to dashboard after 2 seconds
setTimeout(() => { setTimeout(() => {
onClose(); onClose();
window.location.href = '/dashboard'; window.location.href = '/dashboard';
}, 2000); }, 2000);
} else if (status === 'Pending') {
// If pending, show the waiting screen
setWaitingForApproval(true);
} }
} catch (err) { } catch (err) {
if (import.meta.env.DEV) console.error('Error checking KYC status:', err); if (import.meta.env.DEV) console.error('Error checking KYC status:', err);
@@ -139,31 +127,34 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
checkKycStatus(); checkKycStatus();
}, [peopleApi, isPeopleReady, selectedAccount, onClose]); }, [peopleApi, isPeopleReady, selectedAccount, onClose]);
// Subscribe to KYC approval events on People Chain // Subscribe to citizenship events on People Chain
useEffect(() => { useEffect(() => {
if (!peopleApi || !isPeopleReady || !selectedAccount || !waitingForApproval) { const isPending = currentStatus === 'PendingReferral' || currentStatus === 'ReferrerApproved';
if (!peopleApi || !isPeopleReady || !selectedAccount || !isPending) {
return; return;
} }
if (import.meta.env.DEV) console.log('Setting up KYC approval listener on People Chain for:', selectedAccount.address); if (import.meta.env.DEV) console.log('Setting up citizenship event listener on People Chain for:', selectedAccount.address);
const unsubscribe = subscribeToKycApproval( const unsubscribe = subscribeToKycApproval(
peopleApi, peopleApi,
selectedAccount.address, selectedAccount.address,
() => { () => {
if (import.meta.env.DEV) console.log('KYC Approved on People Chain! Redirecting to dashboard...'); // CitizenshipConfirmed
setKycApproved(true); setKycApproved(true);
setWaitingForApproval(false); setCurrentStatus('Approved');
// Redirect to citizen dashboard after 2 seconds
setTimeout(() => { setTimeout(() => {
onClose(); onClose();
window.location.href = '/dashboard'; window.location.href = '/dashboard';
}, 2000); }, 2000);
}, },
(error) => { (error) => {
if (import.meta.env.DEV) console.error('KYC approval subscription error:', error); if (import.meta.env.DEV) console.error('Citizenship event subscription error:', error);
setError(`Failed to monitor approval status: ${error}`); setError(`Failed to monitor status: ${error}`);
},
() => {
// ReferralApproved - referrer vouched, now user can confirm
setCurrentStatus('ReferrerApproved');
} }
); );
@@ -172,7 +163,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
unsubscribe(); unsubscribe();
} }
}; };
}, [peopleApi, isPeopleReady, selectedAccount, waitingForApproval, onClose]); }, [peopleApi, isPeopleReady, selectedAccount, currentStatus, onClose]);
const onSubmit = async (data: FormData) => { const onSubmit = async (data: FormData) => {
// identityKyc pallet is on People Chain // identityKyc pallet is on People Chain
@@ -191,10 +182,10 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
try { try {
// Check KYC status before submitting (from People Chain) // Check KYC status before submitting (from People Chain)
const currentStatus = await getKycStatus(peopleApi, selectedAccount.address); const status = await getKycStatus(peopleApi, selectedAccount.address);
if (currentStatus === 'Approved') { if (status === 'Approved') {
setError('Your KYC has already been approved! Redirecting to dashboard...'); setError('Your citizenship is already approved! Redirecting to dashboard...');
setKycApproved(true); setKycApproved(true);
setTimeout(() => { setTimeout(() => {
onClose(); onClose();
@@ -203,82 +194,71 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
return; return;
} }
if (currentStatus === 'Pending') { if (status === 'PendingReferral' || status === 'ReferrerApproved') {
setError('You already have a pending KYC application. Please wait for admin approval.'); setError('You already have a pending citizenship application.');
setWaitingForApproval(true); setCurrentStatus(status);
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) {
if (import.meta.env.DEV) console.log(`KYC application with referrer: ${referrerAddress}`);
}
// Prepare complete citizenship data // Prepare complete citizenship data
const citizenshipData: CitizenshipData = { const citizenshipData: CitizenshipData = {
...data, ...data,
walletAddress: selectedAccount.address, walletAddress: selectedAccount.address,
timestamp: Date.now(), timestamp: Date.now(),
referralCode: data.referralCode || FOUNDER_ADDRESS // Auto-assign to founder if empty referralCode: data.referralCode || referrerAddress || undefined
}; };
// Generate commitment and nullifier hashes // Compute identity hash (H256) from citizenship data
const commitmentHash = await generateCommitmentHash(citizenshipData); const identityHash = blake2AsHex(
const nullifierHash = await generateNullifierHash(selectedAccount.address, citizenshipData.timestamp); JSON.stringify({
fullName: citizenshipData.fullName,
fatherName: citizenshipData.fatherName,
grandfatherName: citizenshipData.grandfatherName,
motherName: citizenshipData.motherName,
tribe: citizenshipData.tribe,
region: citizenshipData.region,
email: citizenshipData.email,
profession: citizenshipData.profession,
walletAddress: citizenshipData.walletAddress
}),
256
);
if (import.meta.env.DEV) console.log('Commitment Hash:', commitmentHash); if (import.meta.env.DEV) console.log('Identity Hash:', identityHash);
if (import.meta.env.DEV) console.log('Nullifier Hash:', nullifierHash);
// Encrypt data // Encrypt data and upload to IPFS as off-chain backup
const encryptedData = await encryptData(citizenshipData, selectedAccount.address); const encryptedData = encryptData(citizenshipData);
saveLocalCitizenshipData(citizenshipData);
// Save to local storage (backup)
await saveLocalCitizenshipData(citizenshipData, selectedAccount.address);
// Upload to IPFS
const ipfsCid = await uploadToIPFS(encryptedData); const ipfsCid = await uploadToIPFS(encryptedData);
if (import.meta.env.DEV) console.log('IPFS CID (off-chain backup):', ipfsCid);
if (import.meta.env.DEV) console.log('IPFS CID:', ipfsCid); // Determine referrer: explicit referrer address > referral code > default (pallet uses DefaultReferrer)
if (import.meta.env.DEV) console.log('IPFS CID type:', typeof ipfsCid); const effectiveReferrer = referrerAddress || data.referralCode || undefined;
if (import.meta.env.DEV) console.log('IPFS CID value:', JSON.stringify(ipfsCid));
// Ensure ipfsCid is a string // Submit to blockchain - single call: applyForCitizenship(identity_hash, referrer)
const cidString = String(ipfsCid); if (import.meta.env.DEV) console.log('Submitting citizenship application to People Chain...');
if (!cidString || cidString === 'undefined' || cidString === '[object Object]') {
throw new Error(`Invalid IPFS CID: ${cidString}`);
}
// Submit to blockchain (identityKyc pallet is on People Chain)
if (import.meta.env.DEV) console.log('Submitting KYC application to People Chain...');
const result = await submitKycApplication( const result = await submitKycApplication(
peopleApi, peopleApi,
selectedAccount, selectedAccount,
citizenshipData.fullName, identityHash,
citizenshipData.email, effectiveReferrer
cidString,
`Citizenship application for ${citizenshipData.fullName}`
); );
if (!result.success) { if (!result.success) {
setError(result.error || 'Failed to submit KYC application to blockchain'); setError(result.error || 'Failed to submit citizenship application');
setSubmitting(false); setSubmitting(false);
return; return;
} }
if (import.meta.env.DEV) console.log('✅ KYC application submitted to blockchain'); if (import.meta.env.DEV) console.log('Citizenship application submitted to blockchain');
if (import.meta.env.DEV) console.log('Block hash:', result.blockHash);
// Save block hash for display
if (result.blockHash) { if (result.blockHash) {
setApplicationHash(result.blockHash.slice(0, 16) + '...'); setApplicationHash(result.blockHash.slice(0, 16) + '...');
} }
// Move to waiting for approval state
setSubmitted(true); setSubmitted(true);
setSubmitting(false); setSubmitting(false);
setWaitingForApproval(true); setCurrentStatus('PendingReferral');
} catch (err) { } catch (err) {
if (import.meta.env.DEV) console.error('Submission error:', err); if (import.meta.env.DEV) console.error('Submission error:', err);
@@ -320,34 +300,32 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
); );
} }
// Waiting for self-confirmation // PendingReferral - waiting for referrer to approve
if (waitingForApproval) { if (currentStatus === 'PendingReferral') {
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">
{/* Icon */}
<div className="relative"> <div className="relative">
<div className="h-24 w-24 rounded-full border-4 border-primary/20 flex items-center justify-center"> <div className="h-24 w-24 rounded-full border-4 border-yellow-500/20 flex items-center justify-center">
<CheckCircle className="h-10 w-10 text-primary" /> <Loader2 className="h-10 w-10 text-yellow-500 animate-spin" />
</div> </div>
</div> </div>
<div className="text-center space-y-2"> <div className="text-center space-y-2">
<h3 className="text-lg font-semibold">Confirm Your Citizenship Application</h3> <h3 className="text-lg font-semibold">Waiting for Referrer Approval</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. Please review and confirm your identity to mint your Citizen NFT (Welati Tiki). Your application has been submitted. Your referrer needs to vouch for your identity before you can proceed.
</p> </p>
</div> </div>
{/* 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"> <div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-900"> <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" /> <Check className="h-5 w-5 text-green-600 dark:text-green-400" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-medium">Data Encrypted</p> <p className="text-sm font-medium">Application Submitted</p>
<p className="text-xs text-muted-foreground">Your KYC data has been encrypted and stored on IPFS</p> <p className="text-xs text-muted-foreground">Transaction hash: {applicationHash || 'Confirmed'}</p>
</div> </div>
</div> </div>
@@ -356,8 +334,92 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
<Check className="h-5 w-5 text-green-600 dark:text-green-400" /> <Check className="h-5 w-5 text-green-600 dark:text-green-400" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-medium">Blockchain Submitted</p> <p className="text-sm font-medium">1 HEZ Deposit Reserved</p>
<p className="text-xs text-muted-foreground">Transaction hash: {applicationHash || 'Processing...'}</p> <p className="text-xs text-muted-foreground">Deposit will be returned if you cancel</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-yellow-100 dark:bg-yellow-900">
<Loader2 className="h-5 w-5 text-yellow-600 dark:text-yellow-400 animate-spin" />
</div>
<div className="flex-1">
<p className="text-sm font-medium">Waiting for Referrer</p>
<p className="text-xs text-muted-foreground">Your referrer must approve your identity</p>
</div>
</div>
</div>
<div className="flex gap-3 w-full max-w-md pt-4">
<Button
onClick={handleCancelApplication}
disabled={canceling}
variant="destructive"
className="flex-1"
>
{canceling ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Canceling...
</>
) : (
<>
<X className="h-4 w-4 mr-2" />
Cancel Application
</>
)}
</Button>
</div>
{error && (
<Alert variant="destructive" className="w-full max-w-md">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button variant="outline" onClick={onClose} className="mt-2">
Close
</Button>
</CardContent>
</Card>
);
}
// ReferrerApproved - referrer vouched, now user can confirm
if (currentStatus === 'ReferrerApproved') {
return (
<Card>
<CardContent className="pt-6 flex flex-col items-center justify-center py-8 space-y-6">
<div className="relative">
<div className="h-24 w-24 rounded-full border-4 border-green-500/20 flex items-center justify-center">
<CheckCircle className="h-10 w-10 text-green-500" />
</div>
</div>
<div className="text-center space-y-2">
<h3 className="text-lg font-semibold text-green-600">Referrer Approved!</h3>
<p className="text-sm text-muted-foreground max-w-md">
Your referrer has vouched for you. Confirm your citizenship to mint your Welati Tiki NFT.
</p>
</div>
<div className="w-full max-w-md space-y-3 pt-4">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
<Check className="h-5 w-5 text-green-600 dark:text-green-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium">Application Submitted</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
<Check className="h-5 w-5 text-green-600 dark:text-green-400" />
</div>
<div className="flex-1">
<p className="text-sm font-medium">Referrer Approved</p>
</div> </div>
</div> </div>
@@ -366,16 +428,15 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
<AlertCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" /> <AlertCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-medium">Awaiting Your Confirmation</p> <p className="text-sm font-medium">Confirm Citizenship</p>
<p className="text-xs text-muted-foreground">Confirm or reject your application below</p> <p className="text-xs text-muted-foreground">Click below to mint your Welati Tiki NFT</p>
</div> </div>
</div> </div>
</div> </div>
{/* Action buttons */}
<div className="flex gap-3 w-full max-w-md pt-4"> <div className="flex gap-3 w-full max-w-md pt-4">
<Button <Button
onClick={handleApprove} onClick={handleConfirmCitizenship}
disabled={confirming} disabled={confirming}
className="flex-1 bg-green-600 hover:bg-green-700" className="flex-1 bg-green-600 hover:bg-green-700"
> >
@@ -387,25 +448,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
) : ( ) : (
<> <>
<Check className="h-4 w-4 mr-2" /> <Check className="h-4 w-4 mr-2" />
Approve Confirm Citizenship
</>
)}
</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> </Button>
@@ -427,7 +470,7 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
} }
// Initial submission success (before blockchain confirmation) // Initial submission success (before blockchain confirmation)
if (submitted && !waitingForApproval) { if (submitted && currentStatus === 'NotStarted') {
return ( return (
<Card> <Card>
<CardContent className="pt-6 flex flex-col items-center justify-center py-8 space-y-4"> <CardContent className="pt-6 flex flex-col items-center justify-center py-8 space-y-4">
@@ -630,6 +673,21 @@ export const NewCitizenApplication: React.FC<NewCitizenApplicationProps> = ({ on
</CardContent> </CardContent>
</Card> </Card>
{/* Deposit Notice */}
<Card className="bg-yellow-500/10 border-yellow-500/30">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-yellow-500 mt-0.5 flex-shrink-0" />
<div className="text-sm">
<p className="font-semibold text-yellow-600">1 HEZ Deposit Required</p>
<p className="text-muted-foreground mt-1">
A deposit of 1 HEZ will be reserved when you submit your application. It will be returned if you cancel your application.
</p>
</div>
</div>
</CardContent>
</Card>
{/* Terms Agreement */} {/* Terms Agreement */}
<Card> <Card>
<CardContent className="pt-6 space-y-4"> <CardContent className="pt-6 space-y-4">
@@ -21,7 +21,7 @@ interface InviteUserModalProps {
} }
export const InviteUserModal: React.FC<InviteUserModalProps> = ({ isOpen, onClose }) => { export const InviteUserModal: React.FC<InviteUserModalProps> = ({ isOpen, onClose }) => {
const { api, selectedAccount } = usePezkuwi(); const { peopleApi, isPeopleReady, selectedAccount } = usePezkuwi();
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [inviteeAddress, setInviteeAddress] = useState(''); const [inviteeAddress, setInviteeAddress] = useState('');
const [initiating, setInitiating] = useState(false); const [initiating, setInitiating] = useState(false);
@@ -69,8 +69,8 @@ export const InviteUserModal: React.FC<InviteUserModalProps> = ({ isOpen, onClos
}; };
const handleInitiateReferral = async () => { const handleInitiateReferral = async () => {
if (!api || !selectedAccount || !inviteeAddress) { if (!peopleApi || !isPeopleReady || !selectedAccount || !inviteeAddress) {
setInitiateError('Please enter a valid address'); setInitiateError('Please connect wallet and enter a valid address');
return; return;
} }
@@ -84,13 +84,14 @@ export const InviteUserModal: React.FC<InviteUserModalProps> = ({ isOpen, onClos
if (import.meta.env.DEV) console.log(`Initiating referral from ${selectedAccount.address} to ${inviteeAddress}...`); if (import.meta.env.DEV) console.log(`Initiating referral from ${selectedAccount.address} to ${inviteeAddress}...`);
const tx = api.tx.referral.initiateReferral(inviteeAddress); // referral pallet is on People Chain
const tx = peopleApi.tx.referral.initiateReferral(inviteeAddress);
await tx.signAndSend(selectedAccount.address, { signer: injector.signer }, ({ status, dispatchError }) => { await tx.signAndSend(selectedAccount.address, { signer: injector.signer }, ({ status, dispatchError }) => {
if (dispatchError) { if (dispatchError) {
let errorMessage = 'Transaction failed'; let errorMessage = 'Transaction failed';
if (dispatchError.isModule) { if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule); const decoded = peopleApi.registry.findMetaError(dispatchError.asModule);
errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`; errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
} else { } else {
errorMessage = dispatchError.toString(); errorMessage = dispatchError.toString();
@@ -110,7 +111,7 @@ export const InviteUserModal: React.FC<InviteUserModalProps> = ({ isOpen, onClos
}); });
} catch (err: unknown) { } catch (err: unknown) {
if (import.meta.env.DEV) console.error('Failed to initiate referral:', err); if (import.meta.env.DEV) console.error('Failed to initiate referral:', err);
setInitiateError(err.message || 'Failed to initiate referral'); setInitiateError(err instanceof Error ? err.message : 'Failed to initiate referral');
setInitiating(false); setInitiating(false);
} }
}; };
@@ -1,13 +1,72 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { useReferral } from '@/contexts/ReferralContext'; import { useReferral } from '@/contexts/ReferralContext';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useToast } from '@/hooks/use-toast';
import { InviteUserModal } from './InviteUserModal'; import { InviteUserModal } from './InviteUserModal';
import { Users, UserPlus, Trophy, Award, Loader2 } from 'lucide-react'; import { Users, UserPlus, Trophy, Award, Loader2, CheckCircle, Clock, User } from 'lucide-react';
import { getPendingApprovalsForReferrer, approveReferral } from '@pezkuwi/lib/citizenship-workflow';
import type { PendingApproval } from '@pezkuwi/lib/citizenship-workflow';
export const ReferralDashboard: React.FC = () => { export const ReferralDashboard: React.FC = () => {
const { stats, myReferrals, loading } = useReferral(); const { stats, myReferrals, loading } = useReferral();
const { peopleApi, isPeopleReady, selectedAccount } = usePezkuwi();
const { toast } = useToast();
const [showInviteModal, setShowInviteModal] = useState(false); const [showInviteModal, setShowInviteModal] = useState(false);
const [pendingApprovals, setPendingApprovals] = useState<PendingApproval[]>([]);
const [loadingApprovals, setLoadingApprovals] = useState(false);
const [processingAddress, setProcessingAddress] = useState<string | null>(null);
// Load pending approvals for this referrer
useEffect(() => {
if (!peopleApi || !isPeopleReady || !selectedAccount) return;
const loadApprovals = async () => {
setLoadingApprovals(true);
try {
const approvals = await getPendingApprovalsForReferrer(peopleApi, selectedAccount.address);
setPendingApprovals(approvals);
} catch (err) {
if (import.meta.env.DEV) console.error('Error loading pending approvals:', err);
} finally {
setLoadingApprovals(false);
}
};
loadApprovals();
}, [peopleApi, isPeopleReady, selectedAccount]);
const handleApprove = async (applicantAddress: string) => {
if (!peopleApi || !selectedAccount) return;
setProcessingAddress(applicantAddress);
try {
const result = await approveReferral(peopleApi, selectedAccount, applicantAddress);
if (result.success) {
toast({
title: 'Referral Approved',
description: `Vouched for ${applicantAddress.slice(0, 8)}...${applicantAddress.slice(-4)}`,
});
setPendingApprovals(prev => prev.filter(a => a.applicantAddress !== applicantAddress));
} else {
toast({
title: 'Approval Failed',
description: result.error || 'Failed to approve',
variant: 'destructive',
});
}
} catch (err) {
toast({
title: 'Error',
description: err instanceof Error ? err.message : 'Failed to approve referral',
variant: 'destructive',
});
} finally {
setProcessingAddress(null);
}
};
if (loading) { if (loading) {
return ( return (
@@ -135,6 +194,66 @@ export const ReferralDashboard: React.FC = () => {
</CardContent> </CardContent>
</Card> </Card>
{/* Pending Approvals */}
{(pendingApprovals.length > 0 || loadingApprovals) && (
<Card className="bg-yellow-900/20 border-yellow-600/30">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Clock className="w-5 h-5 text-yellow-500" />
Pending Approvals ({pendingApprovals.length})
</CardTitle>
<CardDescription className="text-gray-400">
These users listed you as their referrer and are waiting for your approval
</CardDescription>
</CardHeader>
<CardContent>
{loadingApprovals ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="w-6 h-6 animate-spin text-yellow-500" />
</div>
) : (
<div className="space-y-2">
{pendingApprovals.map((approval) => (
<div
key={approval.applicantAddress}
className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-yellow-600/20 flex items-center justify-center">
<User className="w-4 h-4 text-yellow-400" />
</div>
<div>
<div className="text-sm font-mono text-white">
{approval.applicantAddress.slice(0, 10)}...{approval.applicantAddress.slice(-8)}
</div>
<div className="text-xs text-gray-500">
<Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/30 text-[10px] px-1 py-0">
Pending Referral
</Badge>
</div>
</div>
</div>
<Button
size="sm"
onClick={() => handleApprove(approval.applicantAddress)}
disabled={processingAddress === approval.applicantAddress}
className="bg-green-600 hover:bg-green-700"
>
{processingAddress === approval.applicantAddress ? (
<Loader2 className="w-4 h-4 animate-spin mr-1" />
) : (
<CheckCircle className="w-4 h-4 mr-1" />
)}
Approve
</Button>
</div>
))}
</div>
)}
</CardContent>
</Card>
)}
{/* My Referrals List */} {/* My Referrals List */}
<Card className="bg-gray-900 border-gray-800"> <Card className="bg-gray-900 border-gray-800">
<CardHeader> <CardHeader>
+2 -2
View File
@@ -176,8 +176,8 @@ const BeCitizen: React.FC = () => {
<CardContent className="text-gray-700 font-medium 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> Your referrer approves your identity</p>
<p> Receive your Welati Tiki NFT</p> <p> Confirm and receive your Welati Tiki NFT</p>
</CardContent> </CardContent>
</Card> </Card>