From abd4dc7189731ff217ef8fa5201df8e3a2cad30e Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Mon, 2 Mar 2026 00:48:35 +0300 Subject: [PATCH] feat: in-app citizenship modal + referral approvals + bot DKS link - Add CitizenshipModal component for in-app citizenship application (uses connected wallet keypair, no seed phrase needed) - Replace /citizens redirect with in-app modal in Rewards section - Add pending approvals to ReferralContext - Add approveReferral and getPendingApprovals to citizenship lib - Add applyingCitizenship/applicationSuccess translations (6 langs) - Add DKS Kurdistan bot link to telegram-bot welcome message --- src/components/CitizenshipModal.tsx | 578 +++++++++++++++++++++++ src/contexts/ReferralContext.tsx | 14 + src/i18n/translations/ar.ts | 8 + src/i18n/translations/ckb.ts | 8 + src/i18n/translations/en.ts | 8 + src/i18n/translations/fa.ts | 8 + src/i18n/translations/krd.ts | 8 + src/i18n/translations/tr.ts | 8 + src/i18n/types.ts | 9 + src/lib/citizenship.ts | 113 +++++ src/sections/Rewards.tsx | 100 +++- supabase/functions/telegram-bot/index.ts | 3 + 12 files changed, 858 insertions(+), 7 deletions(-) create mode 100644 src/components/CitizenshipModal.tsx diff --git a/src/components/CitizenshipModal.tsx b/src/components/CitizenshipModal.tsx new file mode 100644 index 0000000..9a1530a --- /dev/null +++ b/src/components/CitizenshipModal.tsx @@ -0,0 +1,578 @@ +/** + * In-App Citizenship Application Modal + * Self-contained modal with 3 steps: form → processing → success + * Uses the already-connected wallet keypair (no seed phrase needed) + */ + +import { useState, useEffect, useRef } from 'react'; +import { X, Plus, Trash2, Shield, CheckCircle, Clock, ArrowRight, AlertCircle } from 'lucide-react'; +import { useWallet } from '@/contexts/WalletContext'; +import { useTelegram } from '@/hooks/useTelegram'; +import { useTranslation } from '@/i18n'; +import { KurdistanSun } from '@/components/KurdistanSun'; +import { formatAddress } from '@/lib/utils'; +import type { Region, MaritalStatus, ChildInfo } from '@/lib/citizenship'; +import { + calculateIdentityHash, + saveCitizenshipLocally, + uploadToIPFS, + applyCitizenship, +} from '@/lib/citizenship'; + +interface CitizenshipModalProps { + isOpen: boolean; + onClose: () => void; +} + +type Step = 'form' | 'processing' | 'success'; + +const REGIONS: { value: Region; labelKey: string }[] = [ + { value: 'bakur', labelKey: 'citizen.regionBakur' }, + { value: 'basur', labelKey: 'citizen.regionBasur' }, + { value: 'rojava', labelKey: 'citizen.regionRojava' }, + { value: 'rojhelat', labelKey: 'citizen.regionRojhelat' }, + { value: 'kurdistan_a_sor', labelKey: 'citizen.regionKurdistanASor' }, + { value: 'diaspora', labelKey: 'citizen.regionDiaspora' }, +]; + +export function CitizenshipModal({ isOpen, onClose }: CitizenshipModalProps) { + const { peopleApi, keypair, address } = useWallet(); + const { hapticImpact, hapticNotification } = useTelegram(); + const { t } = useTranslation(); + + const [step, setStep] = useState('form'); + const [error, setError] = useState(''); + + // Form fields + const [fullName, setFullName] = useState(''); + const [fatherName, setFatherName] = useState(''); + const [grandfatherName, setGrandfatherName] = useState(''); + const [motherName, setMotherName] = useState(''); + const [tribe, setTribe] = useState(''); + const [maritalStatus, setMaritalStatus] = useState('nezewici'); + const [childrenCount, setChildrenCount] = useState(0); + const [children, setChildren] = useState([]); + const [region, setRegion] = useState(''); + const [email, setEmail] = useState(''); + const [profession, setProfession] = useState(''); + const [referrerAddress, setReferrerAddress] = useState(''); + const [consent, setConsent] = useState(false); + + // Success data + const [identityHash, setIdentityHash] = useState(''); + + // Processing cancel ref + const cancelledRef = useRef(false); + + // Reset state when modal opens + useEffect(() => { + if (isOpen) { + setStep('form'); + setError(''); + setFullName(''); + setFatherName(''); + setGrandfatherName(''); + setMotherName(''); + setTribe(''); + setMaritalStatus('nezewici'); + setChildrenCount(0); + setChildren([]); + setRegion(''); + setEmail(''); + setProfession(''); + setReferrerAddress(''); + setConsent(false); + setIdentityHash(''); + cancelledRef.current = false; + } + }, [isOpen]); + + // --- Form handlers --- + + const handleMaritalChange = (status: MaritalStatus) => { + hapticImpact('light'); + setMaritalStatus(status); + if (status === 'nezewici') { + setChildrenCount(0); + setChildren([]); + } + }; + + const handleChildrenCountChange = (count: number) => { + const c = Math.max(0, Math.min(20, count)); + setChildrenCount(c); + setChildren((prev) => { + if (c > prev.length) { + return [ + ...prev, + ...Array.from({ length: c - prev.length }, () => ({ name: '', birthYear: 2000 })), + ]; + } + return prev.slice(0, c); + }); + }; + + const updateChild = (index: number, field: keyof ChildInfo, value: string | number) => { + setChildren((prev) => + prev.map((child, i) => (i === index ? { ...child, [field]: value } : child)) + ); + }; + + const addChild = () => { + hapticImpact('light'); + handleChildrenCountChange(childrenCount + 1); + }; + + const removeChild = (index: number) => { + hapticImpact('light'); + setChildren((prev) => prev.filter((_, i) => i !== index)); + setChildrenCount((prev) => prev - 1); + }; + + const handleFormSubmit = () => { + setError(''); + + if ( + !fullName || + !fatherName || + !grandfatherName || + !motherName || + !tribe || + !region || + !email || + !profession + ) { + setError(t('citizen.fillAllFields')); + hapticNotification('error'); + return; + } + + if (!consent) { + setError(t('citizen.acceptConsent')); + hapticNotification('error'); + return; + } + + if (!peopleApi) { + setError(t('citizen.peopleChainNotConnected')); + hapticNotification('error'); + return; + } + + if (!keypair || !address) { + setError(t('citizen.walletNotConnected')); + hapticNotification('error'); + return; + } + + hapticImpact('medium'); + setStep('processing'); + }; + + // --- Processing logic --- + + useEffect(() => { + if (step !== 'processing') return; + + cancelledRef.current = false; + + const process = async () => { + try { + if (!peopleApi || !keypair || !address) { + setError(t('citizen.walletNotConnected')); + setStep('form'); + return; + } + + // Build citizenship data for local save + const citizenshipData = { + fullName, + fatherName, + grandfatherName, + motherName, + tribe, + maritalStatus, + childrenCount: maritalStatus === 'zewici' ? childrenCount : undefined, + children: maritalStatus === 'zewici' && children.length > 0 ? children : undefined, + region: region as Region, + email, + profession, + referrerAddress: referrerAddress || undefined, + walletAddress: address, + seedPhrase: '', + timestamp: Date.now(), + }; + + // Mock IPFS upload + const ipfsCid = await uploadToIPFS(citizenshipData); + if (cancelledRef.current) return; + + // Calculate identity hash + const hash = calculateIdentityHash(fullName, email, [ipfsCid]); + if (cancelledRef.current) return; + setIdentityHash(hash); + + // Save encrypted data locally + saveCitizenshipLocally(citizenshipData); + + // Sign and submit extrinsic + const result = await applyCitizenship(peopleApi, keypair, hash, referrerAddress || null); + + if (cancelledRef.current) return; + + if (result.success) { + hapticNotification('success'); + setStep('success'); + } else { + hapticNotification('error'); + setError(result.error || t('citizen.submissionFailed')); + setStep('form'); + } + } catch (err) { + if (cancelledRef.current) return; + hapticNotification('error'); + setError(err instanceof Error ? err.message : t('citizen.submissionFailed')); + setStep('form'); + } + }; + + process(); + + return () => { + cancelledRef.current = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [step]); + + if (!isOpen) return null; + + const inputClass = 'w-full px-4 py-3 bg-muted rounded-xl text-sm'; + const labelClass = 'text-sm text-muted-foreground mb-1 block'; + + // --- Processing Step --- + if (step === 'processing') { + return ( +
+
+
+ +
+

{t('citizen.applyingCitizenship')}

+

{fullName}

+
+
+
+
+ ); + } + + // --- Success Step --- + if (step === 'success') { + return ( +
+
+
+ {/* Success Icon */} +
+ +
+ + {/* Title */} +

{t('citizen.applicationSuccess')}

+ + {/* 3-Step Process */} +
+
+ +

{t('citizen.stepApplicationSent')}

+
+
+ +

{t('citizen.stepReferrerApproval')}

+
+
+ +

{t('citizen.stepConfirm')}

+
+
+ + {/* Application Info */} +
+
+

{t('citizen.identityHash')}

+

{formatAddress(identityHash)}

+
+
+

{t('citizen.walletAddress')}

+

{formatAddress(address || '')}

+
+
+ + {/* Next Steps Info */} +

+ {t('citizen.nextStepsInfo')} +

+ + {/* Close Button */} + +
+
+
+ ); + } + + // --- Form Step --- + return ( +
+
+ {/* Header */} +
+

{t('citizen.pageTitle')}

+ +
+ + {/* Form Content */} +
+ {/* Privacy Notice */} +
+ +

{t('citizen.privacyNotice')}

+
+ + {/* Full Name */} +
+ + setFullName(e.target.value)} + className={inputClass} + placeholder={t('citizen.fullNamePlaceholder')} + /> +
+ + {/* Father's Name */} +
+ + setFatherName(e.target.value)} + className={inputClass} + placeholder={t('citizen.fatherNamePlaceholder')} + /> +
+ + {/* Grandfather's Name */} +
+ + setGrandfatherName(e.target.value)} + className={inputClass} + placeholder={t('citizen.grandfatherNamePlaceholder')} + /> +
+ + {/* Mother's Name */} +
+ + setMotherName(e.target.value)} + className={inputClass} + placeholder={t('citizen.motherNamePlaceholder')} + /> +
+ + {/* Tribe */} +
+ + setTribe(e.target.value)} + className={inputClass} + placeholder={t('citizen.tribePlaceholder')} + /> +
+ + {/* Marital Status */} +
+ +
+ + +
+
+ + {/* Children (if married) */} + {maritalStatus === 'zewici' && ( +
+ + {children.map((child, index) => ( +
+
+ + updateChild(index, 'name', e.target.value)} + className={inputClass} + placeholder={t('citizen.childNamePlaceholder')} + /> +
+
+ + + updateChild(index, 'birthYear', parseInt(e.target.value) || 2000) + } + className={inputClass} + min={1950} + max={2026} + /> +
+ +
+ ))} + +
+ )} + + {/* Region */} +
+ + +
+ + {/* Email */} +
+ + setEmail(e.target.value)} + className={inputClass} + placeholder={t('citizen.emailPlaceholder')} + /> +
+ + {/* Profession */} +
+ + setProfession(e.target.value)} + className={inputClass} + placeholder={t('citizen.professionPlaceholder')} + /> +
+ + {/* Referrer Address */} +
+ + setReferrerAddress(e.target.value)} + className={inputClass} + placeholder={t('citizen.referrerPlaceholder')} + /> +
+ + {/* Consent */} + + + {/* Error */} + {error && ( +
+ + {error} +
+ )} + + {/* Submit Button */} + +
+
+
+ ); +} diff --git a/src/contexts/ReferralContext.tsx b/src/contexts/ReferralContext.tsx index ec78edd..0e49eac 100644 --- a/src/contexts/ReferralContext.tsx +++ b/src/contexts/ReferralContext.tsx @@ -14,10 +14,12 @@ import { subscribeToReferralEvents, type ReferralStats, } from '@/lib/referral'; +import { getPendingApprovals, getCitizenshipStatus, type PendingApproval } from '@/lib/citizenship'; interface ReferralContextValue { stats: ReferralStats | null; myReferrals: string[]; + pendingApprovals: PendingApproval[]; loading: boolean; refreshStats: () => Promise; } @@ -31,6 +33,7 @@ export function ReferralProvider({ children }: { children: ReactNode }) { const [stats, setStats] = useState(null); const [myReferrals, setMyReferrals] = useState([]); + const [pendingApprovals, setPendingApprovals] = useState([]); const [loading, setLoading] = useState(true); // Fetch referral statistics from People Chain @@ -38,6 +41,7 @@ export function ReferralProvider({ children }: { children: ReactNode }) { if (!peopleApi || !address) { setStats(null); setMyReferrals([]); + setPendingApprovals([]); setLoading(false); return; } @@ -52,6 +56,15 @@ export function ReferralProvider({ children }: { children: ReactNode }) { setStats(fetchedStats); setMyReferrals(fetchedReferrals); + + // Fetch pending approvals only if user is an approved citizen (can be a referrer) + const citizenStatus = await getCitizenshipStatus(peopleApi, address); + if (citizenStatus === 'Approved') { + const approvals = await getPendingApprovals(peopleApi, address); + setPendingApprovals(approvals); + } else { + setPendingApprovals([]); + } } catch (error) { console.error('Error fetching referral stats:', error); showAlert(translate('context.referralStatsError')); @@ -92,6 +105,7 @@ export function ReferralProvider({ children }: { children: ReactNode }) { const value: ReferralContextValue = { stats, myReferrals, + pendingApprovals, loading, refreshStats: fetchStats, }; diff --git a/src/i18n/translations/ar.ts b/src/i18n/translations/ar.ts index 193af86..e42d749 100644 --- a/src/i18n/translations/ar.ts +++ b/src/i18n/translations/ar.ts @@ -171,6 +171,12 @@ const ar: Translations = { noUnclaimedRewards: 'لا توجد مكافآت غير مطالب بها', rewardHistory: 'سجل المكافآت', era: 'حقبة', + pendingApprovals: 'الموافقات المعلّقة', + approveReferral: 'موافقة', + approvingReferral: 'جاري الموافقة...', + referralApprovalSuccess: 'تمت الموافقة على الإحالة!', + referralApprovalFailed: 'فشلت الموافقة', + pendingReferralStatus: 'بانتظار موافقتك', }, wallet: { @@ -826,6 +832,8 @@ const ar: Translations = { alreadyPending: 'لديك طلب قيد الانتظار', alreadyApproved: 'مواطنتك معتمدة بالفعل!', insufficientBalance: 'رصيد غير كافٍ (١ HEZ وديعة مطلوبة)', + applyingCitizenship: 'جاري تقديم طلب المواطنة...', + applicationSuccess: 'تم تقديم الطلب!', selectLanguage: 'اختر اللغة', }, }; diff --git a/src/i18n/translations/ckb.ts b/src/i18n/translations/ckb.ts index a374663..dbacb5b 100644 --- a/src/i18n/translations/ckb.ts +++ b/src/i18n/translations/ckb.ts @@ -172,6 +172,12 @@ const ckb: Translations = { noUnclaimedRewards: 'خەڵاتی داوانەکراو نییە', rewardHistory: 'مێژووی خەڵاتەکان', era: 'سەردەم', + pendingApprovals: 'پەسەندکردنە چاوەڕوانەکان', + approveReferral: 'پەسەندکردن', + approvingReferral: 'پەسەند دەکرێت...', + referralApprovalSuccess: 'بانگهێشتکردن پەسەندکرا!', + referralApprovalFailed: 'پەسەندکردن سەرنەکەوت', + pendingReferralStatus: 'چاوەڕوانی پەسەندکردنی تۆیە', }, wallet: { @@ -829,6 +835,8 @@ const ckb: Translations = { alreadyPending: 'داواکارییەکی چاوەڕوانت هەیە', alreadyApproved: 'هاوڵاتیبوونت پێشتر پەسەند کراوە!', insufficientBalance: 'باڵانسی پێویست نییە (١ HEZ ئەمانەت پێویستە)', + applyingCitizenship: 'داواکاری هاوڵاتیبوون دەنێردرێت...', + applicationSuccess: 'داواکاری نێردرا!', selectLanguage: 'زمان هەڵبژێرە', }, }; diff --git a/src/i18n/translations/en.ts b/src/i18n/translations/en.ts index e892997..1875e71 100644 --- a/src/i18n/translations/en.ts +++ b/src/i18n/translations/en.ts @@ -171,6 +171,12 @@ const en: Translations = { noUnclaimedRewards: 'No unclaimed rewards', rewardHistory: 'Reward History', era: 'Era', + pendingApprovals: 'Pending Approvals', + approveReferral: 'Approve', + approvingReferral: 'Approving...', + referralApprovalSuccess: 'Referral approved!', + referralApprovalFailed: 'Approval failed', + pendingReferralStatus: 'Awaiting your approval', }, wallet: { @@ -829,6 +835,8 @@ const en: Translations = { alreadyPending: 'You already have a pending application', alreadyApproved: 'Your citizenship is already approved!', insufficientBalance: 'Insufficient balance (1 HEZ deposit required)', + applyingCitizenship: 'Applying for citizenship...', + applicationSuccess: 'Application submitted!', selectLanguage: 'Select language', }, }; diff --git a/src/i18n/translations/fa.ts b/src/i18n/translations/fa.ts index 2fac6a5..f96ce2f 100644 --- a/src/i18n/translations/fa.ts +++ b/src/i18n/translations/fa.ts @@ -171,6 +171,12 @@ const fa: Translations = { noUnclaimedRewards: 'پاداش مطالبه نشده‌ای وجود ندارد', rewardHistory: 'تاریخچه پاداش‌ها', era: 'دوره', + pendingApprovals: 'تأییدهای در انتظار', + approveReferral: 'تأیید', + approvingReferral: 'در حال تأیید...', + referralApprovalSuccess: 'دعوت تأیید شد!', + referralApprovalFailed: 'تأیید ناموفق بود', + pendingReferralStatus: 'در انتظار تأیید شما', }, wallet: { @@ -829,6 +835,8 @@ const fa: Translations = { alreadyPending: 'درخواست در انتظار دارید', alreadyApproved: 'شهروندی شما قبلاً تأیید شده!', insufficientBalance: 'موجودی ناکافی (۱ HEZ سپرده لازم است)', + applyingCitizenship: 'در حال ارسال درخواست شهروندی...', + applicationSuccess: 'درخواست ارسال شد!', selectLanguage: 'انتخاب زبان', }, }; diff --git a/src/i18n/translations/krd.ts b/src/i18n/translations/krd.ts index 2ab0691..c8f5fa5 100644 --- a/src/i18n/translations/krd.ts +++ b/src/i18n/translations/krd.ts @@ -176,6 +176,12 @@ const krd: Translations = { noUnclaimedRewards: 'Xelatên nedaxwazkir tune ne', rewardHistory: 'Dîroka Xelatan', era: 'Era', + pendingApprovals: 'Pejirandina li bendê', + approveReferral: 'Pejirîne', + approvingReferral: 'Tê pejirandin...', + referralApprovalSuccess: 'Referans hat pejirandin!', + referralApprovalFailed: 'Pejirandin biserneket', + pendingReferralStatus: 'Li benda pejirandina we', }, wallet: { @@ -856,6 +862,8 @@ const krd: Translations = { alreadyPending: 'Serlêdanek te ya li bendê heye', alreadyApproved: 'Welatîbûna te berê hatiye pejirandin!', insufficientBalance: 'Balansa têr nîne (1 HEZ depozîto pêwîst e)', + applyingCitizenship: 'Daxwaza welatîbûnê tê şandin...', + applicationSuccess: 'Daxwaz hat şandin!', selectLanguage: 'Ziman hilbijêre', }, }; diff --git a/src/i18n/translations/tr.ts b/src/i18n/translations/tr.ts index b16938a..579d111 100644 --- a/src/i18n/translations/tr.ts +++ b/src/i18n/translations/tr.ts @@ -171,6 +171,12 @@ const tr: Translations = { noUnclaimedRewards: 'Talep edilmemiş ödül yok', rewardHistory: 'Ödül Geçmişi', era: 'Era', + pendingApprovals: 'Bekleyen Onaylar', + approveReferral: 'Onayla', + approvingReferral: 'Onaylanıyor...', + referralApprovalSuccess: 'Referans onaylandı!', + referralApprovalFailed: 'Onaylama başarısız', + pendingReferralStatus: 'Onayınızı bekliyor', }, wallet: { @@ -829,6 +835,8 @@ const tr: Translations = { alreadyPending: 'Bekleyen bir başvurunuz var', alreadyApproved: 'Vatandaşlığınız zaten onaylanmış!', insufficientBalance: 'Yetersiz bakiye (1 HEZ depozito gerekli)', + applyingCitizenship: 'Vatandaşlık başvurusu yapılıyor...', + applicationSuccess: 'Başvuru gönderildi!', selectLanguage: 'Dil seçin', }, }; diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 443d59d..8361877 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -173,6 +173,12 @@ export interface Translations { noUnclaimedRewards: string; rewardHistory: string; era: string; + pendingApprovals: string; + approveReferral: string; + approvingReferral: string; + referralApprovalSuccess: string; + referralApprovalFailed: string; + pendingReferralStatus: string; }; // Wallet section @@ -851,6 +857,9 @@ export interface Translations { alreadyPending: string; alreadyApproved: string; insufficientBalance: string; + // In-app modal + applyingCitizenship: string; + applicationSuccess: string; // Language selector selectLanguage: string; }; diff --git a/src/lib/citizenship.ts b/src/lib/citizenship.ts index a356786..3476533 100644 --- a/src/lib/citizenship.ts +++ b/src/lib/citizenship.ts @@ -46,6 +46,11 @@ export interface CitizenshipResult { identityHash?: string; } +export interface PendingApproval { + applicantAddress: string; + identityHash: string; +} + // ── Identity Hash (Keccak-256) ────────────────────────────────────── export function calculateIdentityHash(name: string, email: string, documentCids: string[]): string { @@ -140,6 +145,114 @@ export async function getCitizenshipStatus( } } +// ── Pending Approvals ─────────────────────────────────────────────── + +/** + * Get pending referral applications that need approval from the given referrer. + * Queries identityKyc.applications entries and filters by referrer + PendingReferral status. + */ +export async function getPendingApprovals( + api: ApiPromise, + referrerAddress: string +): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (!(api?.query as any)?.identityKyc?.applications) { + return []; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const entries = await (api.query as any).identityKyc.applications.entries(); + const pending: PendingApproval[] = []; + + for (const [key, value] of entries) { + const applicantAddress = key.args[0].toString(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const appData = value.toJSON() as any; + + // Check if this application's referrer matches + if (!appData?.referrer || appData.referrer !== referrerAddress) { + continue; + } + + // Check if status is PendingReferral + const status = await getCitizenshipStatus(api, applicantAddress); + if (status === 'PendingReferral') { + pending.push({ + applicantAddress, + identityHash: appData.identityHash || '', + }); + } + } + + return pending; + } catch (error) { + console.error('[Citizenship] Error fetching pending approvals:', error); + return []; + } +} + +// ── Approve Referral ──────────────────────────────────────────────── + +export async function approveReferral( + api: ApiPromise, + keypair: KeyringPair, + applicantAddress: string +): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tx = api.tx as any; + if (!tx?.identityKyc?.approveReferral) { + return { success: false, error: 'Identity KYC pallet not available' }; + } + + const result = await new Promise((resolve) => { + tx.identityKyc + .approveReferral(applicantAddress) + .signAndSend( + keypair, + { nonce: -1 }, + ({ + status, + dispatchError, + }: { + status: { + isInBlock: boolean; + isFinalized: boolean; + asInBlock?: { toString: () => string }; + asFinalized?: { toString: () => string }; + }; + dispatchError?: { isModule: boolean; asModule: unknown; toString: () => string }; + }) => { + if (status.isInBlock || status.isFinalized) { + if (dispatchError) { + let errorMessage = 'Referral approval failed'; + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError( + dispatchError.asModule as Parameters[0] + ); + errorMessage = `${decoded.section}.${decoded.name}`; + } + resolve({ success: false, error: errorMessage }); + return; + } + const blockHash = status.asFinalized?.toString() || status.asInBlock?.toString(); + resolve({ success: true, blockHash }); + } + } + ) + .catch((error: Error) => resolve({ success: false, error: error.message })); + }); + + return result; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } +} + // ── Blockchain Submission ─────────────────────────────────────────── export async function applyCitizenship( diff --git a/src/sections/Rewards.tsx b/src/sections/Rewards.tsx index 2009248..da7f38a 100644 --- a/src/sections/Rewards.tsx +++ b/src/sections/Rewards.tsx @@ -24,10 +24,12 @@ import { Play, PenTool, Globe2, + Clock, + Loader2, } from 'lucide-react'; import { cn, formatAddress } from '@/lib/utils'; import { useTelegram } from '@/hooks/useTelegram'; - +import { useAuth } from '@/contexts/AuthContext'; import { useReferral } from '@/contexts/ReferralContext'; import { useWallet } from '@/contexts/WalletContext'; import { SocialLinks } from '@/components/SocialLinks'; @@ -62,9 +64,11 @@ import { getCitizenshipStatus, getCitizenCount, confirmCitizenship, + approveReferral, type CitizenshipStatus, } from '@/lib/citizenship'; import { KurdistanSun } from '@/components/KurdistanSun'; +import { CitizenshipModal } from '@/components/CitizenshipModal'; // Activity tracking constants const ACTIVITY_STORAGE_KEY = 'pezkuwi_last_active'; @@ -72,8 +76,8 @@ const ACTIVITY_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours export function RewardsSection() { const { hapticImpact, hapticNotification, shareUrl, showAlert } = useTelegram(); - - const { stats, myReferrals, loading, refreshStats } = useReferral(); + const { user: authUser } = useAuth(); + const { stats, myReferrals, pendingApprovals, loading, refreshStats } = useReferral(); const { isConnected, address, peopleApi, assetHubApi, keypair } = useWallet(); const { t } = useTranslation(); @@ -95,6 +99,8 @@ export function RewardsSection() { const [unclaimedRewards, setUnclaimedRewards] = useState(null); const [claimingStaking, setClaimingStaking] = useState(false); const [claimingStakingEra, setClaimingStakingEra] = useState(null); + const [approvingAddress, setApprovingAddress] = useState(null); + const [showCitizenshipModal, setShowCitizenshipModal] = useState(false); // Check activity status const checkActivityStatus = useCallback(() => { @@ -358,9 +364,31 @@ export function RewardsSection() { showAlert(t('rewards.activatedAlert')); }; - // Citizenship referral link - wallet address in start param for auto-fill - const referralLink = address - ? `https://t.me/pezkuwichainBot?start=${address}` + const handleApproveReferral = async (applicantAddress: string) => { + if (!peopleApi || !keypair) return; + setApprovingAddress(applicantAddress); + hapticImpact('medium'); + try { + const result = await approveReferral(peopleApi, keypair, applicantAddress); + if (result.success) { + hapticNotification('success'); + showAlert(t('rewards.referralApprovalSuccess')); + refreshStats(); + } else { + hapticNotification('error'); + showAlert(result.error || t('rewards.referralApprovalFailed')); + } + } catch (err) { + hapticNotification('error'); + showAlert(err instanceof Error ? err.message : t('rewards.referralApprovalFailed')); + } finally { + setApprovingAddress(null); + } + }; + + // Telegram referral link (for sharing) - use authenticated user ID + const referralLink = authUser?.telegram_id + ? `https://t.me/pezkuwichainBot?start=ref_${authUser.telegram_id}` : 'https://t.me/pezkuwichainBot'; // Full share message: invitation text + link + wallet address for manual paste @@ -496,7 +524,7 @@ export function RewardsSection() { + {/* Pending Approvals Section */} + {pendingApprovals.length > 0 && ( +
+
+ +

{t('rewards.pendingApprovals')}

+ + {pendingApprovals.length} + +
+
+ {pendingApprovals.map((approval) => ( +
+
+ + {formatAddress(approval.applicantAddress, 8)} + +

+ {t('rewards.pendingReferralStatus')} +

+
+ +
+ ))} +
+
+ )} + {loading ? (
{[1, 2, 3, 4, 5].map((i) => ( @@ -1302,6 +1377,17 @@ export function RewardsSection() {

{t('rewards.signingBlockchain')}

)} + + {/* Citizenship Application Modal */} + { + setShowCitizenshipModal(false); + if (peopleApi && address) { + getCitizenshipStatus(peopleApi, address).then(setCitizenshipStatus); + } + }} + /> ); } diff --git a/supabase/functions/telegram-bot/index.ts b/supabase/functions/telegram-bot/index.ts index 9946243..02ec3e2 100644 --- a/supabase/functions/telegram-bot/index.ts +++ b/supabase/functions/telegram-bot/index.ts @@ -59,6 +59,9 @@ Cûzdanê xwe biafirînin, zimanê xwe hilbijêrin û welatiyê Pezkuwî bibin. Start your digital journey with Pezkuwi. Create your wallet, choose your language and become a citizen. + +🤖 Dijital Kurdistan AI agentıyla sohbet etmek ve daha detaylı bilgi almak için → @DKSkurdistanBot +Chat with Digital Kurdistan AI agent for more info → @DKSkurdistanBot `; // ── DKS bot (@DKSKurdistanBot) welcome ──────────────────────────────