From 86dc8c1fcd3c37a5960a671f763cef513a74c0eb Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Mon, 16 Feb 2026 02:56:27 +0300 Subject: [PATCH] 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 --- shared/lib/citizenship-workflow.ts | 418 +++++++++----- web/src/components/admin/KycApprovalTab.tsx | 545 ++++-------------- .../citizenship/NewCitizenApplication.tsx | 368 +++++++----- .../components/referral/InviteUserModal.tsx | 13 +- .../components/referral/ReferralDashboard.tsx | 123 +++- web/src/pages/BeCitizen.tsx | 4 +- 6 files changed, 728 insertions(+), 743 deletions(-) diff --git a/shared/lib/citizenship-workflow.ts b/shared/lib/citizenship-workflow.ts index 0e130bff..dbda6595 100644 --- a/shared/lib/citizenship-workflow.ts +++ b/shared/lib/citizenship-workflow.ts @@ -40,7 +40,7 @@ const web3FromAddress = async (address: string): Promise => { // TYPE DEFINITIONS // ======================================== -export type KycStatus = 'NotStarted' | 'Pending' | 'Approved' | 'Rejected'; +export type KycStatus = 'NotStarted' | 'PendingReferral' | 'ReferrerApproved' | 'Approved' | 'Revoked'; export type Region = | 'bakur' // North (Turkey) @@ -107,7 +107,7 @@ export interface CitizenshipStatus { tikiNumber?: string; stakingScoreTracking: boolean; 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 ): Promise { try { - // MOCK FOR DEV: Alice is Approved - if (address === '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY') { - return 'Approved'; - } - if (!api?.query?.identityKyc) { if (import.meta.env.DEV) console.log('Identity KYC pallet not available on this chain'); 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) { - return 'NotStarted'; + if (!application.isEmpty) { + const appData = application.toJSON() as Record; + 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(); - - // Map on-chain status to our type - if (statusStr === 'Approved') return 'Approved'; - if (statusStr === 'Pending') return 'Pending'; - if (statusStr === 'Rejected') return 'Rejected'; + // Fallback: check kycStatuses if applications storage doesn't exist + if (api.query.identityKyc.kycStatuses) { + const status = await api.query.identityKyc.kycStatuses(address); + if (!status.isEmpty) { + const statusStr = status.toString(); + if (statusStr === 'Approved') return 'Approved'; + if (statusStr === 'PendingReferral') return 'PendingReferral'; + if (statusStr === 'ReferrerApproved') return 'ReferrerApproved'; + if (statusStr === 'Revoked') return 'Revoked'; + } + } return 'NotStarted'; } catch (error) { @@ -160,12 +169,15 @@ export async function hasPendingApplication( address: string ): Promise { try { - if (!api?.query?.identityKyc?.pendingKycApplications) { - return false; + if (api?.query?.identityKyc?.applications) { + const application = await api.query.identityKyc.applications(address); + if (!application.isEmpty) { + const appData = application.toJSON() as Record; + const status = appData.status as string | undefined; + return status === 'PendingReferral' || status === 'ReferrerApproved'; + } } - - const application = await api.query.identityKyc.pendingKycApplications(address); - return !application.isEmpty; + return false; } catch (error) { console.error('Error checking pending application:', error); return false; @@ -344,15 +356,18 @@ export async function getCitizenshipStatus( isStakingScoreTracking(api, address) ]); - const kycApproved = kycStatus === 'Approved'; const hasTiki = citizenCheck.hasTiki; - // Determine next action + // Determine next action based on workflow state let nextAction: CitizenshipStatus['nextAction']; - if (!kycApproved) { + if (kycStatus === 'NotStarted' || kycStatus === 'Revoked') { 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'; } else if (!stakingTracking) { nextAction = 'START_TRACKING'; @@ -453,172 +468,113 @@ export async function validateReferralCode( // ======================================== /** - * Submit KYC application to blockchain - * This is a two-step process: - * 1. Set identity (name, email) - * 2. Apply for KYC (IPFS CID, notes) + * Submit citizenship application to blockchain + * Single call: applyForCitizenship(identity_hash, referrer) + * Requires 1 HEZ deposit (reserved by pallet automatically) */ export async function submitKycApplication( api: ApiPromise, account: InjectedAccountWithMeta, - name: string, - email: string, - ipfsCid: string, - notes: string = 'Citizenship application' + identityHash: string, + referrerAddress?: string ): Promise<{ success: boolean; error?: string; blockHash?: string }> { try { - if (!api?.tx?.identityKyc?.setIdentity || !api?.tx?.identityKyc?.applyForKyc) { + if (!api?.tx?.identityKyc?.applyForCitizenship) { return { success: false, error: 'Identity KYC pallet not available' }; } - // Check if user already has a pending KYC application - const pendingApp = await api.query.identityKyc.pendingKycApplications(account.address); - if (!pendingApp.isEmpty) { - console.log('⚠️ User already has a pending KYC application'); + // Check if user already has a pending application + const hasPending = await hasPendingApplication(api, account.address); + if (hasPending) { return { 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 - const kycStatus = await api.query.identityKyc.kycStatuses(account.address); - if (kycStatus.toString() === 'Approved') { - console.log('✅ User KYC is already approved'); + const currentStatus = await getKycStatus(api, account.address); + if (currentStatus === 'Approved') { return { success: false, error: 'Your citizenship application is already approved!' }; } - // Get the injector for signing const injector = await web3FromAddress(account.address); - // Debug logging - console.log('=== submitKycApplication Debug ==='); - console.log('account.address:', account.address); - console.log('name:', name); - console.log('email:', email); - console.log('ipfsCid:', ipfsCid); - 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}` }; + if (import.meta.env.DEV) { + console.log('=== submitKycApplication Debug ==='); + console.log('account.address:', account.address); + console.log('identityHash:', identityHash); + console.log('referrerAddress:', referrerAddress || '(default referrer)'); + console.log('==================================='); } - // Step 1: Set identity first - console.log('Step 1: Setting identity...'); - const identityResult = await new Promise<{ success: boolean; error?: string }>((resolve, reject) => { - api.tx.identityKyc - .setIdentity(name, email) - .signAndSend(account.address, { signer: injector.signer }, ({ status, dispatchError, events }) => { - console.log('Identity transaction status:', status.type); + // Single call: applyForCitizenship(identity_hash, referrer) + // referrer is Option - null means pallet uses DefaultReferrer + const referrerParam = referrerAddress || null; - 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) => { api.tx.identityKyc - .applyForKyc([cidString], notes) + .applyForCitizenship(identityHash, referrerParam) .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 (dispatchError) { let errorMessage = 'Transaction failed'; - if (dispatchError.isModule) { const decoded = api.registry.findMetaError(dispatchError.asModule); errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`; } else { errorMessage = dispatchError.toString(); } - - console.error('Transaction error:', errorMessage); + if (import.meta.env.DEV) console.error('Transaction error:', errorMessage); resolve({ success: false, error: errorMessage }); return; } - // Check for KycApplied event - const kycAppliedEvent = events.find(({ event }) => - event.section === 'identityKyc' && event.method === 'KycApplied' + const appliedEvent = events.find(({ event }: any) => + event.section === 'identityKyc' && event.method === 'CitizenshipApplied' ); - if (kycAppliedEvent) { - console.log('✅ KYC Application submitted successfully'); - resolve({ - success: true, - blockHash: status.asInBlock.toString() - }); - } else { - console.warn('Transaction included but KycApplied event not found'); - resolve({ success: true }); + if (appliedEvent) { + if (import.meta.env.DEV) console.log('Citizenship application submitted successfully'); } + + resolve({ + success: true, + blockHash: status.isInBlock ? status.asInBlock.toString() : undefined + }); } }) - .catch((error) => { - console.error('Failed to sign and send transaction:', error); + .catch((error: any) => { + if (import.meta.env.DEV) console.error('Failed to sign and send transaction:', error); reject(error); }); }); return result; } catch (error: any) { - console.error('Error submitting KYC application:', error); + console.error('Error submitting citizenship application:', error); return { 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( api: ApiPromise, address: string, onApproved: () => void, - onError?: (error: string) => void + onError?: (error: string) => void, + onReferralApproved?: () => void ): () => void { try { if (!api?.query?.system?.events) { @@ -633,11 +589,20 @@ export function subscribeToKycApproval( events.forEach((record: any) => { const { event } = record; - if (event.section === 'identityKyc' && event.method === 'KycApproved') { - const [approvedAddress] = event.data; + // Referrer approved the application + 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) { - console.log('✅ KYC Approved for:', address); + // Citizenship fully confirmed (NFT minted) + 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(); } } @@ -646,17 +611,206 @@ export function subscribeToKycApproval( return unsubscribe as unknown as () => void; } catch (error: any) { - console.error('Error subscribing to KYC approval:', error); - if (onError) onError(error.message || 'Failed to subscribe to approval events'); + console.error('Error subscribing to citizenship events:', error); + if (onError) onError(error.message || 'Failed to subscribe to events'); 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 { + 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; + + 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 // ======================================== -export const FOUNDER_ADDRESS = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'; // Satoshi Qazi Muhammed +export const FOUNDER_ADDRESS = '5CyuFfbF95rzBxru7c9yEsX4XmQXUxpLUcbj9RLg9K1cGiiF'; // Satoshi Qazi Muhammed export interface AuthChallenge { message: string; diff --git a/web/src/components/admin/KycApprovalTab.tsx b/web/src/components/admin/KycApprovalTab.tsx index 21e2db7f..390a5815 100644 --- a/web/src/components/admin/KycApprovalTab.tsx +++ b/web/src/components/admin/KycApprovalTab.tsx @@ -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([]); - const [identities, setIdentities] = useState>(new Map()); - const [selectedApp, setSelectedApp] = useState(null); - const [processing, setProcessing] = useState(false); - const [showDetailsModal, setShowDetailsModal] = useState(false); + const [pendingApps, setPendingApps] = useState([]); + const [processingAddress, setProcessingAddress] = useState(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(); - - for (const [key, value] of entries) { - const address = key.args[0].toString(); - const application = value.toJSON() as Record; - - // 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; - 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((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((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 (
- Connecting to blockchain... + Connecting to People Chain...
@@ -356,7 +123,7 @@ export function KycApprovalTab() { - Please connect your admin wallet to view and approve KYC applications. + Please connect your wallet to view referral approvals. @@ -368,196 +135,82 @@ export function KycApprovalTab() { } return ( - <> - - - Pending KYC Applications - - - - {loading ? ( -
- -
- ) : pendingApps.length === 0 ? ( -
- -

No pending applications

-

All KYC applications have been processed

-
- ) : ( + + + Pending Referral Approvals + + + + {loading ? ( +
+ +
+ ) : pendingApps.length === 0 ? ( +
+ +

No pending approvals

+

No one is waiting for your referral approval

+
+ ) : ( + <> +

+ These users listed you as their referrer. Approve to vouch for their identity. +

Applicant - Name - Email - Documents + Identity Hash Status Actions - {pendingApps.map((app) => { - const identity = identities.get(app.address); - return ( - - - {app.address.slice(0, 6)}...{app.address.slice(-4)} - - -
- - {identity?.name || 'Loading...'} -
-
- -
- - {identity?.email || 'Loading...'} -
-
- - - - {app.cids.length} CID(s) - - - - - - Pending - - - -
- -
-
-
- ); - })} + {pendingApps.map((app) => ( + + +
+ + + {app.applicantAddress.slice(0, 8)}...{app.applicantAddress.slice(-6)} + +
+
+ + + {app.identityHash ? `${app.identityHash.slice(0, 12)}...` : 'N/A'} + + + + + + Pending Referral + + + + + +
+ ))}
- )} -
-
- - {/* Details Modal */} - - - - KYC Application Details - - Review application before approving or rejecting - - - - {selectedApp && ( -
-
-
- -

{selectedApp.address}

-
-
- -

{identities.get(selectedApp.address)?.name || 'Unknown'}

-
-
- -

{identities.get(selectedApp.address)?.email || 'No email'}

-
-
- -

- {selectedApp.timestamp - ? new Date(selectedApp.timestamp).toLocaleString() - : 'Unknown'} -

-
-
- -
- -

- {selectedApp.notes} -

-
- -
- -
- {selectedApp.cids.map((cid, index) => ( - - ))} -
-
- - - - - Important: Approving this application will: -
    -
  • Unreserve the applicant's deposit
  • -
  • Mint a Welati (Citizen) NFT automatically
  • -
  • Enable trust score tracking
  • -
  • Grant governance voting rights
  • -
-
-
-
- )} - - - - - - -
-
- + + )} +
+
); } - -function Label({ children, className }: { children: React.ReactNode; className?: string }) { - return ; -} diff --git a/web/src/components/citizenship/NewCitizenApplication.tsx b/web/src/components/citizenship/NewCitizenApplication.tsx index 3e78bbac..a7ec8077 100644 --- a/web/src/components/citizenship/NewCitizenApplication.tsx +++ b/web/src/components/citizenship/NewCitizenApplication.tsx @@ -10,9 +10,10 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Checkbox } from '@/components/ui/checkbox'; import { Loader2, AlertTriangle, CheckCircle, User, Users as UsersIcon, MapPin, Briefcase, Mail, Check, X, AlertCircle } from 'lucide-react'; import { usePezkuwi } from '@/contexts/PezkuwiContext'; -import type { CitizenshipData, Region, MaritalStatus } from '@pezkuwi/lib/citizenship-workflow'; -import { FOUNDER_ADDRESS, submitKycApplication, subscribeToKycApproval, getKycStatus } from '@pezkuwi/lib/citizenship-workflow'; -import { generateCommitmentHash, generateNullifierHash, encryptData, saveLocalCitizenshipData, uploadToIPFS } from '@pezkuwi/lib/citizenship-workflow'; +import { blake2AsHex } from '@pezkuwi/util-crypto'; +import type { CitizenshipData, Region, MaritalStatus, KycStatus } 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 { onClose: () => void; @@ -28,83 +29,75 @@ export const NewCitizenApplication: React.FC = ({ on const [submitting, setSubmitting] = useState(false); const [submitted, setSubmitted] = useState(false); - const [waitingForApproval, setWaitingForApproval] = useState(false); + const [currentStatus, setCurrentStatus] = useState('NotStarted'); const [kycApproved, setKycApproved] = useState(false); const [error, setError] = useState(null); const [agreed, setAgreed] = useState(false); const [confirming, setConfirming] = useState(false); + const [canceling, setCanceling] = useState(false); const [applicationHash, setApplicationHash] = useState(''); const maritalStatus = watch('maritalStatus'); const childrenCount = watch('childrenCount'); - const handleApprove = async () => { - // identityKyc pallet is on People Chain + const handleConfirmCitizenship = async () => { if (!peopleApi || !isPeopleReady || !selectedAccount) { setError('Please connect your wallet and wait for People Chain connection'); return; } setConfirming(true); + setError(null); try { - const { web3FromAddress } = await import('@pezkuwi/extension-dapp'); - const injector = await web3FromAddress(selectedAccount.address); + const result = await confirmCitizenship(peopleApi, selectedAccount); - 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 - const tx = peopleApi.tx.identityKyc.confirmCitizenship(); - - 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); - } - }); + setKycApproved(true); + setCurrentStatus('Approved'); + setTimeout(() => { + onClose(); + window.location.href = '/dashboard'; + }, 2000); } catch (err) { - if (import.meta.env.DEV) console.error('Approval error:', err); - setError((err as Error).message || 'Failed to approve application'); + if (import.meta.env.DEV) console.error('Confirmation error:', err); + setError((err as Error).message || 'Failed to confirm citizenship'); + } finally { setConfirming(false); } }; - const handleReject = async () => { - // Cancel/withdraw the application - simply close modal and go back - // No blockchain interaction needed - application will remain Pending until confirmed or admin-rejected - if (import.meta.env.DEV) console.log('Canceling citizenship application (no blockchain interaction)'); - onClose(); - window.location.href = '/'; + const handleCancelApplication = async () => { + if (!peopleApi || !isPeopleReady || !selectedAccount) { + setError('Please connect your wallet and wait for People Chain connection'); + return; + } + + 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) @@ -117,19 +110,14 @@ export const NewCitizenApplication: React.FC = ({ on try { const status = await getKycStatus(peopleApi, selectedAccount.address); if (import.meta.env.DEV) console.log('Current KYC Status from People Chain:', status); + setCurrentStatus(status); if (status === 'Approved') { - if (import.meta.env.DEV) console.log('KYC already approved! Redirecting to dashboard...'); setKycApproved(true); - - // Redirect to dashboard after 2 seconds setTimeout(() => { onClose(); window.location.href = '/dashboard'; }, 2000); - } else if (status === 'Pending') { - // If pending, show the waiting screen - setWaitingForApproval(true); } } catch (err) { if (import.meta.env.DEV) console.error('Error checking KYC status:', err); @@ -139,31 +127,34 @@ export const NewCitizenApplication: React.FC = ({ on checkKycStatus(); }, [peopleApi, isPeopleReady, selectedAccount, onClose]); - // Subscribe to KYC approval events on People Chain + // Subscribe to citizenship events on People Chain useEffect(() => { - if (!peopleApi || !isPeopleReady || !selectedAccount || !waitingForApproval) { + const isPending = currentStatus === 'PendingReferral' || currentStatus === 'ReferrerApproved'; + if (!peopleApi || !isPeopleReady || !selectedAccount || !isPending) { 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( peopleApi, selectedAccount.address, () => { - if (import.meta.env.DEV) console.log('KYC Approved on People Chain! Redirecting to dashboard...'); + // CitizenshipConfirmed setKycApproved(true); - setWaitingForApproval(false); - - // Redirect to citizen dashboard after 2 seconds + setCurrentStatus('Approved'); setTimeout(() => { onClose(); window.location.href = '/dashboard'; }, 2000); }, (error) => { - if (import.meta.env.DEV) console.error('KYC approval subscription error:', error); - setError(`Failed to monitor approval status: ${error}`); + if (import.meta.env.DEV) console.error('Citizenship event subscription error:', 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 = ({ on unsubscribe(); } }; - }, [peopleApi, isPeopleReady, selectedAccount, waitingForApproval, onClose]); + }, [peopleApi, isPeopleReady, selectedAccount, currentStatus, onClose]); const onSubmit = async (data: FormData) => { // identityKyc pallet is on People Chain @@ -191,10 +182,10 @@ export const NewCitizenApplication: React.FC = ({ on try { // 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') { - setError('Your KYC has already been approved! Redirecting to dashboard...'); + if (status === 'Approved') { + setError('Your citizenship is already approved! Redirecting to dashboard...'); setKycApproved(true); setTimeout(() => { onClose(); @@ -203,82 +194,71 @@ export const NewCitizenApplication: React.FC = ({ on return; } - if (currentStatus === 'Pending') { - setError('You already have a pending KYC application. Please wait for admin approval.'); - setWaitingForApproval(true); + if (status === 'PendingReferral' || status === 'ReferrerApproved') { + setError('You already have a pending citizenship application.'); + setCurrentStatus(status); 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 const citizenshipData: CitizenshipData = { ...data, walletAddress: selectedAccount.address, timestamp: Date.now(), - referralCode: data.referralCode || FOUNDER_ADDRESS // Auto-assign to founder if empty + referralCode: data.referralCode || referrerAddress || undefined }; - // Generate commitment and nullifier hashes - const commitmentHash = await generateCommitmentHash(citizenshipData); - const nullifierHash = await generateNullifierHash(selectedAccount.address, citizenshipData.timestamp); + // Compute identity hash (H256) from citizenship data + const identityHash = blake2AsHex( + 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('Nullifier Hash:', nullifierHash); + if (import.meta.env.DEV) console.log('Identity Hash:', identityHash); - // Encrypt data - const encryptedData = await encryptData(citizenshipData, selectedAccount.address); - - // Save to local storage (backup) - await saveLocalCitizenshipData(citizenshipData, selectedAccount.address); - - // Upload to IPFS + // Encrypt data and upload to IPFS as off-chain backup + const encryptedData = encryptData(citizenshipData); + saveLocalCitizenshipData(citizenshipData); 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); - if (import.meta.env.DEV) console.log('IPFS CID type:', typeof ipfsCid); - if (import.meta.env.DEV) console.log('IPFS CID value:', JSON.stringify(ipfsCid)); + // Determine referrer: explicit referrer address > referral code > default (pallet uses DefaultReferrer) + const effectiveReferrer = referrerAddress || data.referralCode || undefined; - // Ensure ipfsCid is a string - const cidString = String(ipfsCid); - 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...'); + // Submit to blockchain - single call: applyForCitizenship(identity_hash, referrer) + if (import.meta.env.DEV) console.log('Submitting citizenship application to People Chain...'); const result = await submitKycApplication( peopleApi, selectedAccount, - citizenshipData.fullName, - citizenshipData.email, - cidString, - `Citizenship application for ${citizenshipData.fullName}` + identityHash, + effectiveReferrer ); if (!result.success) { - setError(result.error || 'Failed to submit KYC application to blockchain'); + setError(result.error || 'Failed to submit citizenship application'); setSubmitting(false); return; } - if (import.meta.env.DEV) console.log('✅ KYC application submitted to blockchain'); - if (import.meta.env.DEV) console.log('Block hash:', result.blockHash); + if (import.meta.env.DEV) console.log('Citizenship application submitted to blockchain'); - // Save block hash for display if (result.blockHash) { setApplicationHash(result.blockHash.slice(0, 16) + '...'); } - // Move to waiting for approval state setSubmitted(true); setSubmitting(false); - setWaitingForApproval(true); + setCurrentStatus('PendingReferral'); } catch (err) { if (import.meta.env.DEV) console.error('Submission error:', err); @@ -320,34 +300,32 @@ export const NewCitizenApplication: React.FC = ({ on ); } - // Waiting for self-confirmation - if (waitingForApproval) { + // PendingReferral - waiting for referrer to approve + if (currentStatus === 'PendingReferral') { return ( - {/* Icon */}
-
- +
+
-

Confirm Your Citizenship Application

+

Waiting for Referrer Approval

- 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.

- {/* Status steps */}
-

Data Encrypted

-

Your KYC data has been encrypted and stored on IPFS

+

Application Submitted

+

Transaction hash: {applicationHash || 'Confirmed'}

@@ -356,8 +334,92 @@ export const NewCitizenApplication: React.FC = ({ on
-

Blockchain Submitted

-

Transaction hash: {applicationHash || 'Processing...'}

+

1 HEZ Deposit Reserved

+

Deposit will be returned if you cancel

+
+
+ +
+
+ +
+
+

Waiting for Referrer

+

Your referrer must approve your identity

+
+
+ + +
+ +
+ + {error && ( + + + {error} + + )} + + +
+
+ ); + } + + // ReferrerApproved - referrer vouched, now user can confirm + if (currentStatus === 'ReferrerApproved') { + return ( + + +
+
+ +
+
+ +
+

Referrer Approved!

+

+ Your referrer has vouched for you. Confirm your citizenship to mint your Welati Tiki NFT. +

+
+ +
+
+
+ +
+
+

Application Submitted

+
+
+ +
+
+ +
+
+

Referrer Approved

@@ -366,16 +428,15 @@ export const NewCitizenApplication: React.FC = ({ on
-

Awaiting Your Confirmation

-

Confirm or reject your application below

+

Confirm Citizenship

+

Click below to mint your Welati Tiki NFT

- {/* Action buttons */}
- @@ -427,7 +470,7 @@ export const NewCitizenApplication: React.FC = ({ on } // Initial submission success (before blockchain confirmation) - if (submitted && !waitingForApproval) { + if (submitted && currentStatus === 'NotStarted') { return ( @@ -630,6 +673,21 @@ export const NewCitizenApplication: React.FC = ({ on + {/* Deposit Notice */} + + +
+ +
+

1 HEZ Deposit Required

+

+ A deposit of 1 HEZ will be reserved when you submit your application. It will be returned if you cancel your application. +

+
+
+
+
+ {/* Terms Agreement */} diff --git a/web/src/components/referral/InviteUserModal.tsx b/web/src/components/referral/InviteUserModal.tsx index 299f488b..89070504 100644 --- a/web/src/components/referral/InviteUserModal.tsx +++ b/web/src/components/referral/InviteUserModal.tsx @@ -21,7 +21,7 @@ interface InviteUserModalProps { } export const InviteUserModal: React.FC = ({ isOpen, onClose }) => { - const { api, selectedAccount } = usePezkuwi(); + const { peopleApi, isPeopleReady, selectedAccount } = usePezkuwi(); const [copied, setCopied] = useState(false); const [inviteeAddress, setInviteeAddress] = useState(''); const [initiating, setInitiating] = useState(false); @@ -69,8 +69,8 @@ export const InviteUserModal: React.FC = ({ isOpen, onClos }; const handleInitiateReferral = async () => { - if (!api || !selectedAccount || !inviteeAddress) { - setInitiateError('Please enter a valid address'); + if (!peopleApi || !isPeopleReady || !selectedAccount || !inviteeAddress) { + setInitiateError('Please connect wallet and enter a valid address'); return; } @@ -84,13 +84,14 @@ export const InviteUserModal: React.FC = ({ isOpen, onClos 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 }) => { if (dispatchError) { let errorMessage = 'Transaction failed'; 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(' ')}`; } else { errorMessage = dispatchError.toString(); @@ -110,7 +111,7 @@ export const InviteUserModal: React.FC = ({ isOpen, onClos }); } catch (err: unknown) { 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); } }; diff --git a/web/src/components/referral/ReferralDashboard.tsx b/web/src/components/referral/ReferralDashboard.tsx index 33506018..5001d899 100644 --- a/web/src/components/referral/ReferralDashboard.tsx +++ b/web/src/components/referral/ReferralDashboard.tsx @@ -1,13 +1,72 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useReferral } from '@/contexts/ReferralContext'; +import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { useToast } from '@/hooks/use-toast'; 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 = () => { const { stats, myReferrals, loading } = useReferral(); + const { peopleApi, isPeopleReady, selectedAccount } = usePezkuwi(); + const { toast } = useToast(); const [showInviteModal, setShowInviteModal] = useState(false); + const [pendingApprovals, setPendingApprovals] = useState([]); + const [loadingApprovals, setLoadingApprovals] = useState(false); + const [processingAddress, setProcessingAddress] = useState(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) { return ( @@ -135,6 +194,66 @@ export const ReferralDashboard: React.FC = () => { + {/* Pending Approvals */} + {(pendingApprovals.length > 0 || loadingApprovals) && ( + + + + + Pending Approvals ({pendingApprovals.length}) + + + These users listed you as their referrer and are waiting for your approval + + + + {loadingApprovals ? ( +
+ +
+ ) : ( +
+ {pendingApprovals.map((approval) => ( +
+
+
+ +
+
+
+ {approval.applicantAddress.slice(0, 10)}...{approval.applicantAddress.slice(-8)} +
+
+ + Pending Referral + +
+
+
+ +
+ ))} +
+ )} +
+
+ )} + {/* My Referrals List */} diff --git a/web/src/pages/BeCitizen.tsx b/web/src/pages/BeCitizen.tsx index 14c8ed55..be02af25 100644 --- a/web/src/pages/BeCitizen.tsx +++ b/web/src/pages/BeCitizen.tsx @@ -176,8 +176,8 @@ const BeCitizen: React.FC = () => {

✓ Fill detailed KYC application

✓ Data encrypted with ZK-proofs

-

✓ Submit for admin approval

-

✓ Receive your Welati Tiki NFT

+

✓ Your referrer approves your identity

+

✓ Confirm and receive your Welati Tiki NFT