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