From b8ab86028fec1d6208a69344ada1ea1d3abc64c7 Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Sat, 14 Feb 2026 20:44:17 +0300 Subject: [PATCH] feat: add Be Citizen page with 6-language support --- package.json | 2 +- src/App.tsx | 18 + src/components/citizen/CitizenForm.tsx | 362 +++++++++++++++++++ src/components/citizen/CitizenProcessing.tsx | 143 ++++++++ src/components/citizen/CitizenSuccess.tsx | 64 ++++ src/i18n/translations/ar.ts | 56 +++ src/i18n/translations/ckb.ts | 56 +++ src/i18n/translations/en.ts | 56 +++ src/i18n/translations/fa.ts | 56 +++ src/i18n/translations/krd.ts | 56 +++ src/i18n/translations/tr.ts | 56 +++ src/i18n/types.ts | 64 ++++ src/lib/citizenship.ts | 274 ++++++++++++++ src/pages/CitizenPage.tsx | 195 ++++++++++ src/version.json | 6 +- supabase/functions/telegram-bot/index.ts | 233 ++++++++++-- 16 files changed, 1653 insertions(+), 44 deletions(-) create mode 100644 src/components/citizen/CitizenForm.tsx create mode 100644 src/components/citizen/CitizenProcessing.tsx create mode 100644 src/components/citizen/CitizenSuccess.tsx create mode 100644 src/lib/citizenship.ts create mode 100644 src/pages/CitizenPage.tsx diff --git a/package.json b/package.json index a69a13d..4972484 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pezkuwi-telegram-miniapp", - "version": "1.0.190", + "version": "1.0.192", "type": "module", "description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards", "author": "Pezkuwichain Team", diff --git a/src/App.tsx b/src/App.tsx index 13af9ce..b356ae6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,9 @@ const RewardsSection = lazy(() => const WalletSection = lazy(() => import('@/sections/Wallet').then((m) => ({ default: m.WalletSection })) ); +const CitizenPage = lazy(() => + import('@/pages/CitizenPage').then((m) => ({ default: m.CitizenPage })) +); // Loading fallback component function SectionLoader() { @@ -51,7 +54,22 @@ const NAV_ITEMS: NavItem[] = [ // P2P Web App URL - Mobile-optimized P2P const P2P_WEB_URL = 'https://telegram.pezkuwichain.io/p2p'; +// Check for standalone pages via URL query params (evaluated once at module level) +const PAGE_PARAM = new URLSearchParams(window.location.search).get('page'); + export default function App() { + if (PAGE_PARAM === 'citizen') { + return ( + }> + + + ); + } + + return ; +} + +function MainApp() { const [activeSection, setActiveSection] = useState
('announcements'); const [showP2PModal, setShowP2PModal] = useState(false); const { sessionToken } = useAuth(); diff --git a/src/components/citizen/CitizenForm.tsx b/src/components/citizen/CitizenForm.tsx new file mode 100644 index 0000000..102e72c --- /dev/null +++ b/src/components/citizen/CitizenForm.tsx @@ -0,0 +1,362 @@ +/** + * Citizen Application Form + * Collects citizenship data from the user + */ + +import { useState } from 'react'; +import { Plus, Trash2 } from 'lucide-react'; +import { useTranslation } from '@/i18n'; +import { useTelegram } from '@/hooks/useTelegram'; +import type { CitizenshipData, Region, MaritalStatus, ChildInfo } from '@/lib/citizenship'; + +interface Props { + walletAddress: string; + onSubmit: (data: CitizenshipData) => void; +} + +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 CitizenForm({ walletAddress, onSubmit }: Props) { + const { t } = useTranslation(); + const { hapticImpact, hapticNotification } = useTelegram(); + + 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 [referralCode, setReferralCode] = useState(''); + const [consent, setConsent] = useState(false); + const [error, setError] = useState(''); + + 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 handleSubmit = () => { + 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; + } + + hapticImpact('medium'); + + const data: 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, + referralCode: referralCode || undefined, + walletAddress, + timestamp: Date.now(), + }; + + onSubmit(data); + }; + + const inputClass = 'w-full px-4 py-3 bg-muted rounded-xl text-sm'; + const labelClass = 'text-sm text-muted-foreground mb-1 block'; + + return ( +
+ {/* 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')} + /> +
+ + {/* Referral Code */} +
+ + setReferralCode(e.target.value)} + className={inputClass} + placeholder={t('citizen.referralCodePlaceholder')} + /> +
+ + {/* Consent */} + + + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Submit Button */} + +
+ ); +} diff --git a/src/components/citizen/CitizenProcessing.tsx b/src/components/citizen/CitizenProcessing.tsx new file mode 100644 index 0000000..aec17b2 --- /dev/null +++ b/src/components/citizen/CitizenProcessing.tsx @@ -0,0 +1,143 @@ +/** + * Citizen Processing Component + * Shows KurdistanSun animation while preparing data, + * then enables sign button when ready + */ + +import { useState, useEffect, useCallback } from 'react'; +import { KurdistanSun } from '@/components/KurdistanSun'; +import { useTranslation } from '@/i18n'; +import { useTelegram } from '@/hooks/useTelegram'; +import { useWallet } from '@/contexts/WalletContext'; +import type { CitizenshipData } from '@/lib/citizenship'; +import { + generateCommitmentHash, + generateNullifierHash, + saveCitizenshipLocally, + uploadToIPFS, + submitCitizenshipApplication, +} from '@/lib/citizenship'; + +interface Props { + citizenshipData: CitizenshipData; + onSuccess: (blockHash?: string) => void; + onError: (error: string) => void; +} + +type ProcessingState = 'preparing' | 'ready' | 'signing'; + +export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props) { + const { t } = useTranslation(); + const { hapticImpact, hapticNotification } = useTelegram(); + const { peopleApi, keypair } = useWallet(); + + const [state, setState] = useState('preparing'); + const [ipfsCid, setIpfsCid] = useState(''); + + // Prepare data on mount + useEffect(() => { + const prepare = async () => { + try { + // Generate commitment hash + generateCommitmentHash(citizenshipData); + generateNullifierHash(citizenshipData.walletAddress, citizenshipData.timestamp); + + // Save encrypted data locally + saveCitizenshipLocally(citizenshipData); + + // Mock IPFS upload + const cid = await uploadToIPFS(citizenshipData); + setIpfsCid(cid); + + // Small delay to show animation + await new Promise((resolve) => setTimeout(resolve, 1500)); + + setState('ready'); + hapticNotification('success'); + } catch (err) { + onError(err instanceof Error ? err.message : 'Preparation failed'); + } + }; + + prepare(); + }, [citizenshipData, hapticNotification, onError]); + + const handleSign = useCallback(async () => { + if (!peopleApi || !keypair) { + onError(t('citizen.walletNotConnected')); + return; + } + + setState('signing'); + hapticImpact('medium'); + + try { + const result = await submitCitizenshipApplication( + peopleApi, + keypair, + citizenshipData.fullName, + citizenshipData.email, + ipfsCid, + `Citizenship application - ${citizenshipData.region}` + ); + + if (result.success) { + hapticNotification('success'); + onSuccess(result.blockHash); + } else { + hapticNotification('error'); + onError(result.error || t('citizen.submissionFailed')); + } + } catch (err) { + hapticNotification('error'); + onError(err instanceof Error ? err.message : t('citizen.submissionFailed')); + } + }, [ + peopleApi, + keypair, + citizenshipData, + ipfsCid, + hapticImpact, + hapticNotification, + onSuccess, + onError, + t, + ]); + + const isReady = state === 'ready'; + const isSigning = state === 'signing'; + + return ( +
+ {/* Kurdistan Sun Animation */} +
+ +
+ + {/* Status Message */} +
+

+ {state === 'preparing' && t('citizen.preparingData')} + {state === 'ready' && t('citizen.readyToSign')} + {state === 'signing' && t('citizen.signingTx')} +

+ {state === 'preparing' && ( +

{citizenshipData.fullName}

+ )} +
+ + {/* Sign Button */} + +
+ ); +} diff --git a/src/components/citizen/CitizenSuccess.tsx b/src/components/citizen/CitizenSuccess.tsx new file mode 100644 index 0000000..7a85e7d --- /dev/null +++ b/src/components/citizen/CitizenSuccess.tsx @@ -0,0 +1,64 @@ +/** + * Citizen Success Screen + * Shows after successful citizenship application submission + */ + +import { CheckCircle } from 'lucide-react'; +import { useTranslation } from '@/i18n'; +import { useTelegram } from '@/hooks/useTelegram'; +import { formatAddress } from '@/lib/wallet-service'; +import { generateCitizenNumber } from '@/lib/citizenship'; + +interface Props { + address: string; + onOpenApp: () => void; +} + +export function CitizenSuccess({ address, onOpenApp }: Props) { + const { t } = useTranslation(); + const { hapticImpact } = useTelegram(); + + // Generate a citizen number based on address + const citizenNumber = generateCitizenNumber(address, 42, 0); + const citizenId = `#42-0-${citizenNumber}`; + + const handleOpenApp = () => { + hapticImpact('medium'); + onOpenApp(); + }; + + return ( +
+ {/* Success Icon */} +
+ +
+ + {/* Title */} +
+

{t('citizen.successTitle')}

+

{t('citizen.successSubtitle')}

+
+ + {/* Citizen ID Card */} +
+
+

{t('citizen.citizenId')}

+

{citizenId}

+
+
+

{t('citizen.walletAddress')}

+

{formatAddress(address)}

+
+
+ + {/* Open App Button */} + +
+ ); +} diff --git a/src/i18n/translations/ar.ts b/src/i18n/translations/ar.ts index 026df7e..3cb44bf 100644 --- a/src/i18n/translations/ar.ts +++ b/src/i18n/translations/ar.ts @@ -574,6 +574,62 @@ const ar: Translations = { wrongPasswordError: 'كلمة مرور خاطئة', walletSyncFailed: 'فشل مزامنة عنوان المحفظة مع قاعدة البيانات', }, + + citizen: { + pageTitle: 'كن مواطناً', + fullName: 'الاسم الكامل', + fullNamePlaceholder: 'أدخل اسمك الكامل', + fatherName: 'اسم الأب', + fatherNamePlaceholder: 'أدخل اسم والدك', + grandfatherName: 'اسم الجد', + grandfatherNamePlaceholder: 'أدخل اسم جدك', + motherName: 'اسم الأم', + motherNamePlaceholder: 'أدخل اسم والدتك', + tribe: 'العشيرة', + tribePlaceholder: 'أدخل اسم عشيرتك', + maritalStatus: 'الحالة الاجتماعية', + married: 'متزوج', + single: 'أعزب', + childrenCount: 'عدد الأطفال', + childName: 'اسم الطفل {index}', + childNamePlaceholder: 'الاسم', + childBirthYear: 'سنة الميلاد', + addChild: 'إضافة طفل', + removeChild: 'إزالة', + region: 'المنطقة', + regionPlaceholder: 'اختر منطقتك', + regionBakur: 'باكور (تركيا)', + regionBasur: 'باشور (العراق)', + regionRojava: 'روج آفا (سوريا)', + regionRojhelat: 'روج هلات (إيران)', + regionKurdistanASor: 'كردستان الحمراء', + regionDiaspora: 'الشتات', + email: 'البريد الإلكتروني', + emailPlaceholder: 'name@mail.com', + profession: 'المهنة', + professionPlaceholder: 'أدخل مهنتك', + referralCode: 'رمز الإحالة (اختياري)', + referralCodePlaceholder: 'أدخل رمز الإحالة', + consentCheckbox: 'بياناتي محمية بـ ZK-proof، فقط الهاش يُخزن على البلوكشين', + submit: 'إرسال', + sign: 'وقّع', + openApp: 'فتح التطبيق', + preparingData: 'جاري تحضير البيانات...', + readyToSign: 'جاهز للتوقيع', + signingTx: 'جاري التوقيع...', + successTitle: 'أصبحت مواطناً في بزكوي!', + successSubtitle: 'مرحباً بك في وطنك الرقمي!', + citizenId: 'رقم المواطنة', + walletAddress: 'عنوان المحفظة', + fillAllFields: 'يرجى ملء جميع الحقول', + acceptConsent: 'يرجى تحديد مربع الموافقة', + walletNotConnected: 'المحفظة غير متصلة', + peopleChainNotConnected: 'People Chain غير متصل', + submissionFailed: 'فشل الإرسال', + alreadyPending: 'لديك طلب قيد الانتظار', + alreadyApproved: 'مواطنتك معتمدة بالفعل!', + selectLanguage: 'اختر اللغة', + }, }; export default ar; diff --git a/src/i18n/translations/ckb.ts b/src/i18n/translations/ckb.ts index 252a588..9abfe91 100644 --- a/src/i18n/translations/ckb.ts +++ b/src/i18n/translations/ckb.ts @@ -576,6 +576,62 @@ const ckb: Translations = { wrongPasswordError: 'وشەی نهێنی هەڵەیە', walletSyncFailed: 'هاوکاتکردنی ناونیشانی جزدان لەگەڵ DB سەرنەکەوت', }, + + citizen: { + pageTitle: 'ببە هاوڵاتی', + fullName: 'ناوی تەواو', + fullNamePlaceholder: 'ناوی تەواوت بنووسە', + fatherName: 'ناوی باوک', + fatherNamePlaceholder: 'ناوی باوکت بنووسە', + grandfatherName: 'ناوی باپیر', + grandfatherNamePlaceholder: 'ناوی باپیرت بنووسە', + motherName: 'ناوی دایک', + motherNamePlaceholder: 'ناوی دایکت بنووسە', + tribe: 'هۆز / عەشیرەت', + tribePlaceholder: 'ناوی هۆزەکەت بنووسە', + maritalStatus: 'بارودۆخی خێزانی', + married: 'خێزاندار', + single: 'بێ خێزان', + childrenCount: 'ژمارەی منداڵان', + childName: 'ناوی منداڵی {index}', + childNamePlaceholder: 'ناو', + childBirthYear: 'ساڵی لەدایکبوون', + addChild: 'منداڵ زیاد بکە', + removeChild: 'لاببە', + region: 'هەرێم', + regionPlaceholder: 'هەرێمەکەت هەڵبژێرە', + regionBakur: 'باکوور (تورکیا)', + regionBasur: 'باشوور (عێراق)', + regionRojava: 'ڕۆژاڤا (سووریا)', + regionRojhelat: 'ڕۆژهەڵات (ئێران)', + regionKurdistanASor: 'کوردستانی سوور', + regionDiaspora: 'دیاسپۆرا', + email: 'ئیمەیل', + emailPlaceholder: 'ناو@mail.com', + profession: 'پیشە', + professionPlaceholder: 'پیشەکەت بنووسە', + referralCode: 'کۆدی ڕیفێڕاڵ (ئارەزوومەندانە)', + referralCodePlaceholder: 'کۆدی ڕیفێڕاڵ بنووسە', + consentCheckbox: 'داتاکانم بە ZK-proof پارێزراون، تەنها هاش لە بلۆکچەین تۆمار دەکرێت', + submit: 'ناردن', + sign: 'واژووبکە', + openApp: 'ئەپ بکەرەوە', + preparingData: 'داتا ئامادە دەکرێت...', + readyToSign: 'ئامادەیە بۆ واژوو', + signingTx: 'واژوو دەکرێت...', + successTitle: 'بوویت بە هاوڵاتی پێزکووی!', + successSubtitle: 'بەخێر بێیت بۆ نیشتمانی دیجیتاڵت!', + citizenId: 'ناسنامەی هاوڵاتی', + walletAddress: 'ناونیشانی جزدان', + fillAllFields: 'تکایە هەموو خانەکان پڕ بکەرەوە', + acceptConsent: 'تکایە خانەی ڕەزامەندی نیشان بدە', + walletNotConnected: 'جزدان پەیوەندی نییە', + peopleChainNotConnected: 'People Chain پەیوەندی نییە', + submissionFailed: 'ناردن سەرنەکەوت', + alreadyPending: 'داواکارییەکی چاوەڕوانت هەیە', + alreadyApproved: 'هاوڵاتیبوونت پێشتر پەسەند کراوە!', + selectLanguage: 'زمان هەڵبژێرە', + }, }; export default ckb; diff --git a/src/i18n/translations/en.ts b/src/i18n/translations/en.ts index 8436bfc..3b31018 100644 --- a/src/i18n/translations/en.ts +++ b/src/i18n/translations/en.ts @@ -575,6 +575,62 @@ const en: Translations = { wrongPasswordError: 'Wrong password', walletSyncFailed: 'Wallet address sync to DB failed', }, + + citizen: { + pageTitle: 'Be Citizen', + fullName: 'Full Name', + fullNamePlaceholder: 'Enter your full name', + fatherName: "Father's Name", + fatherNamePlaceholder: "Enter your father's name", + grandfatherName: "Grandfather's Name", + grandfatherNamePlaceholder: "Enter your grandfather's name", + motherName: "Mother's Name", + motherNamePlaceholder: "Enter your mother's name", + tribe: 'Tribe', + tribePlaceholder: 'Enter your tribe name', + maritalStatus: 'Marital Status', + married: 'Married', + single: 'Single', + childrenCount: 'Number of Children', + childName: 'Child {index} Name', + childNamePlaceholder: 'Name', + childBirthYear: 'Birth Year', + addChild: 'Add child', + removeChild: 'Remove', + region: 'Region', + regionPlaceholder: 'Select your region', + regionBakur: 'Bakur (Turkey)', + regionBasur: 'Bashur (Iraq)', + regionRojava: 'Rojava (Syria)', + regionRojhelat: 'Rojhelat (Iran)', + regionKurdistanASor: 'Red Kurdistan', + regionDiaspora: 'Diaspora', + email: 'E-mail', + emailPlaceholder: 'name@mail.com', + profession: 'Profession', + professionPlaceholder: 'Enter your profession', + referralCode: 'Referral Code (Optional)', + referralCodePlaceholder: 'Enter referral code', + consentCheckbox: 'My data is protected with ZK-proof, only a hash is stored on the blockchain', + submit: 'Submit', + sign: 'Sign', + openApp: 'Open App', + preparingData: 'Preparing data...', + readyToSign: 'Ready to sign', + signingTx: 'Signing...', + successTitle: 'You are now a Pezkuwi Citizen!', + successSubtitle: 'Welcome to your digital homeland!', + citizenId: 'Citizen ID', + walletAddress: 'Wallet Address', + fillAllFields: 'Please fill in all fields', + acceptConsent: 'Please accept the consent checkbox', + walletNotConnected: 'Wallet not connected', + peopleChainNotConnected: 'People Chain not connected', + submissionFailed: 'Submission failed', + alreadyPending: 'You already have a pending application', + alreadyApproved: 'Your citizenship is already approved!', + selectLanguage: 'Select language', + }, }; export default en; diff --git a/src/i18n/translations/fa.ts b/src/i18n/translations/fa.ts index dc39147..c55ff79 100644 --- a/src/i18n/translations/fa.ts +++ b/src/i18n/translations/fa.ts @@ -575,6 +575,62 @@ const fa: Translations = { wrongPasswordError: 'رمز عبور اشتباه', walletSyncFailed: 'همگام‌سازی آدرس کیف پول با DB ناموفق', }, + + citizen: { + pageTitle: 'شهروند شوید', + fullName: 'نام کامل', + fullNamePlaceholder: 'نام کامل خود را وارد کنید', + fatherName: 'نام پدر', + fatherNamePlaceholder: 'نام پدر خود را وارد کنید', + grandfatherName: 'نام پدربزرگ', + grandfatherNamePlaceholder: 'نام پدربزرگ خود را وارد کنید', + motherName: 'نام مادر', + motherNamePlaceholder: 'نام مادر خود را وارد کنید', + tribe: 'قبیله / عشیره', + tribePlaceholder: 'نام قبیله خود را وارد کنید', + maritalStatus: 'وضعیت تأهل', + married: 'متأهل', + single: 'مجرد', + childrenCount: 'تعداد فرزندان', + childName: 'نام فرزند {index}', + childNamePlaceholder: 'نام', + childBirthYear: 'سال تولد', + addChild: 'افزودن فرزند', + removeChild: 'حذف', + region: 'منطقه', + regionPlaceholder: 'منطقه خود را انتخاب کنید', + regionBakur: 'باکور (ترکیه)', + regionBasur: 'باشور (عراق)', + regionRojava: 'روژاوا (سوریه)', + regionRojhelat: 'روژهلات (ایران)', + regionKurdistanASor: 'کردستان سرخ', + regionDiaspora: 'دیاسپورا', + email: 'ایمیل', + emailPlaceholder: 'name@mail.com', + profession: 'شغل', + professionPlaceholder: 'شغل خود را وارد کنید', + referralCode: 'کد معرف (اختیاری)', + referralCodePlaceholder: 'کد معرف را وارد کنید', + consentCheckbox: 'داده‌های من با ZK-proof محافظت می‌شوند، فقط هش در بلاکچین ثبت می‌شود', + submit: 'ارسال', + sign: 'امضا کنید', + openApp: 'باز کردن برنامه', + preparingData: 'آماده‌سازی داده‌ها...', + readyToSign: 'آماده برای امضا', + signingTx: 'در حال امضا...', + successTitle: 'شما شهروند پزکوی شدید!', + successSubtitle: 'به سرزمین دیجیتالتان خوش آمدید!', + citizenId: 'شناسه شهروندی', + walletAddress: 'آدرس کیف پول', + fillAllFields: 'لطفاً همه فیلدها را پر کنید', + acceptConsent: 'لطفاً کادر رضایت را علامت بزنید', + walletNotConnected: 'کیف پول متصل نیست', + peopleChainNotConnected: 'People Chain متصل نیست', + submissionFailed: 'ارسال ناموفق', + alreadyPending: 'درخواست در انتظار دارید', + alreadyApproved: 'شهروندی شما قبلاً تأیید شده!', + selectLanguage: 'انتخاب زبان', + }, }; export default fa; diff --git a/src/i18n/translations/krd.ts b/src/i18n/translations/krd.ts index bc14966..b739d0f 100644 --- a/src/i18n/translations/krd.ts +++ b/src/i18n/translations/krd.ts @@ -600,6 +600,62 @@ const krd: Translations = { wrongPasswordError: '\u015e\u00eefre (password) \u00e7ewt e', walletSyncFailed: 'Wallet adresa DB-\u00ea re senkron\u00eeze neb\u00fb', }, + + citizen: { + pageTitle: 'Bibe Welatî', + fullName: 'Navê Te', + fullNamePlaceholder: 'Navê xwe yê temam binivîse', + fatherName: 'Navê Bavê Te', + fatherNamePlaceholder: 'Navê bavê xwe binivîse', + grandfatherName: 'Navê Bavkalê Te', + grandfatherNamePlaceholder: 'Navê bavkalê xwe binivîse', + motherName: 'Navê Dayika Te', + motherNamePlaceholder: 'Navê dayika xwe binivîse', + tribe: 'Eşîra Te', + tribePlaceholder: 'Navê eşîra xwe binivîse', + maritalStatus: 'Rewşa Zewacê', + married: 'Zewicî', + single: 'Nezewicî', + childrenCount: 'Hejmara Zarokan', + childName: 'Navê Zaroka {index}', + childNamePlaceholder: 'Nav', + childBirthYear: 'Sala Bûyînê', + addChild: 'Zaroka zêde bike', + removeChild: 'Jê bibe', + region: 'Herêm', + regionPlaceholder: 'Herêmê hilbijêre', + regionBakur: 'Bakur (Tirkiye)', + regionBasur: 'Başûr (Iraq)', + regionRojava: 'Rojava (Sûriye)', + regionRojhelat: 'Rojhelat (Îran)', + regionKurdistanASor: 'Kurdistan a Sor', + regionDiaspora: 'Diaspora', + email: 'E-mail', + emailPlaceholder: 'navê@mail.com', + profession: 'Pîşeya Te', + professionPlaceholder: 'Pîşeya xwe binivîse', + referralCode: 'Koda Referral (Opsiyonel)', + referralCodePlaceholder: 'Koda referral binivîse', + consentCheckbox: 'Daneyên min bi ZK-proof ewle ne, tenê hash li blockchain tê tomarkirin', + submit: 'Bişîne', + sign: 'Îmze Bike', + openApp: 'Serîlêdanê Veke', + preparingData: 'Dane têne amadekirin...', + readyToSign: 'Ji bo îmzekirinê amade ye', + signingTx: 'Tê îmzekirin...', + successTitle: 'Welatiyê Pezkuwî bûn!', + successSubtitle: 'Serî hatî welatê xwe yê dîjîtal!', + citizenId: 'Nasnameya Welatî', + walletAddress: 'Navnîşana Cûzdan', + fillAllFields: 'Ji kerema xwe hemû qadan tije bike', + acceptConsent: 'Ji kerema xwe qutiya pejirandinê nîşan bide', + walletNotConnected: 'Cûzdan girêdayî nîne', + peopleChainNotConnected: 'People Chain girêdayî nîne', + submissionFailed: 'Serlêdan neserketî', + alreadyPending: 'Serlêdanek te ya li bendê heye', + alreadyApproved: 'Welatîbûna te berê hatiye pejirandin!', + selectLanguage: 'Ziman hilbijêre', + }, }; export default krd; diff --git a/src/i18n/translations/tr.ts b/src/i18n/translations/tr.ts index 767a7e2..bae2b02 100644 --- a/src/i18n/translations/tr.ts +++ b/src/i18n/translations/tr.ts @@ -576,6 +576,62 @@ const tr: Translations = { wrongPasswordError: 'Yanlış şifre', walletSyncFailed: 'Cüzdan adresi DB ile senkronize edilemedi', }, + + citizen: { + pageTitle: 'Vatandaş Ol', + fullName: 'Tam İsim', + fullNamePlaceholder: 'Tam isminizi girin', + fatherName: 'Baba Adı', + fatherNamePlaceholder: 'Babanızın adını girin', + grandfatherName: 'Dede Adı', + grandfatherNamePlaceholder: 'Dedenizin adını girin', + motherName: 'Anne Adı', + motherNamePlaceholder: 'Annenizin adını girin', + tribe: 'Aşiret', + tribePlaceholder: 'Aşiretinizin adını girin', + maritalStatus: 'Medeni Durum', + married: 'Evli', + single: 'Bekar', + childrenCount: 'Çocuk Sayısı', + childName: 'Çocuk {index} Adı', + childNamePlaceholder: 'Ad', + childBirthYear: 'Doğum Yılı', + addChild: 'Çocuk ekle', + removeChild: 'Kaldır', + region: 'Bölge', + regionPlaceholder: 'Bölgenizi seçin', + regionBakur: 'Bakur (Türkiye)', + regionBasur: 'Başur (Irak)', + regionRojava: 'Rojava (Suriye)', + regionRojhelat: 'Rojhelat (İran)', + regionKurdistanASor: 'Kızıl Kürdistan', + regionDiaspora: 'Diaspora', + email: 'E-posta', + emailPlaceholder: 'isim@mail.com', + profession: 'Meslek', + professionPlaceholder: 'Mesleğinizi girin', + referralCode: 'Referans Kodu (Opsiyonel)', + referralCodePlaceholder: 'Referans kodunu girin', + consentCheckbox: "Verilerim ZK-proof ile güvende, sadece hash blockchain'e kaydedilir", + submit: 'Gönder', + sign: 'İmzala', + openApp: 'Uygulamayı Aç', + preparingData: 'Veriler hazırlanıyor...', + readyToSign: 'İmzalanmaya hazır', + signingTx: 'İmzalanıyor...', + successTitle: 'Pezkuwi Vatandaşı Oldunuz!', + successSubtitle: 'Dijital vatanınıza hoş geldiniz!', + citizenId: 'Vatandaş No', + walletAddress: 'Cüzdan Adresi', + fillAllFields: 'Lütfen tüm alanları doldurun', + acceptConsent: 'Lütfen onay kutusunu işaretleyin', + walletNotConnected: 'Cüzdan bağlı değil', + peopleChainNotConnected: 'People Chain bağlı değil', + submissionFailed: 'Başvuru başarısız', + alreadyPending: 'Bekleyen bir başvurunuz var', + alreadyApproved: 'Vatandaşlığınız zaten onaylanmış!', + selectLanguage: 'Dil seçin', + }, }; export default tr; diff --git a/src/i18n/types.ts b/src/i18n/types.ts index fbd3258..68c81ca 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -580,6 +580,70 @@ export interface Translations { wrongPasswordError: string; walletSyncFailed: string; }; + + // Citizen page + citizen: { + pageTitle: string; + // Form labels + fullName: string; + fullNamePlaceholder: string; + fatherName: string; + fatherNamePlaceholder: string; + grandfatherName: string; + grandfatherNamePlaceholder: string; + motherName: string; + motherNamePlaceholder: string; + tribe: string; + tribePlaceholder: string; + maritalStatus: string; + married: string; + single: string; + childrenCount: string; + childName: string; + childNamePlaceholder: string; + childBirthYear: string; + addChild: string; + removeChild: string; + region: string; + regionPlaceholder: string; + regionBakur: string; + regionBasur: string; + regionRojava: string; + regionRojhelat: string; + regionKurdistanASor: string; + regionDiaspora: string; + email: string; + emailPlaceholder: string; + profession: string; + professionPlaceholder: string; + referralCode: string; + referralCodePlaceholder: string; + // Consent + consentCheckbox: string; + // Buttons + submit: string; + sign: string; + openApp: string; + // Processing + preparingData: string; + readyToSign: string; + signingTx: string; + // Success + successTitle: string; + successSubtitle: string; + citizenId: string; + walletAddress: string; + // Errors + fillAllFields: string; + acceptConsent: string; + walletNotConnected: string; + peopleChainNotConnected: string; + submissionFailed: string; + alreadyPending: string; + alreadyApproved: string; + // Language selector + selectLanguage: string; + }; } export type LanguageCode = 'krd' | 'en' | 'tr' | 'ckb' | 'fa' | 'ar'; diff --git a/src/lib/citizenship.ts b/src/lib/citizenship.ts new file mode 100644 index 0000000..ca540b5 --- /dev/null +++ b/src/lib/citizenship.ts @@ -0,0 +1,274 @@ +/** + * Citizenship utilities for Telegram MiniApp + * Adapted from pwap/shared/lib/citizenship-workflow.ts + * Uses native KeyringPair signing (no browser extension) + */ + +import type { ApiPromise } from '@pezkuwi/api'; +import type { KeyringPair } from '@pezkuwi/keyring/types'; + +// ── Type Definitions ──────────────────────────────────────────────── + +export type KycStatus = 'NotStarted' | 'Pending' | 'Approved' | 'Rejected'; + +export type Region = 'bakur' | 'basur' | 'rojava' | 'rojhelat' | 'diaspora' | 'kurdistan_a_sor'; + +export type MaritalStatus = 'zewici' | 'nezewici'; + +export interface ChildInfo { + name: string; + birthYear: number; +} + +export interface CitizenshipData { + fullName: string; + fatherName: string; + grandfatherName: string; + motherName: string; + tribe: string; + maritalStatus: MaritalStatus; + childrenCount?: number; + children?: ChildInfo[]; + region: Region; + email: string; + profession: string; + referralCode?: string; + walletAddress: string; + timestamp: number; +} + +export interface CitizenshipResult { + success: boolean; + error?: string; + blockHash?: string; +} + +// ── Hash Generation ───────────────────────────────────────────────── + +function simpleHash(str: string): number { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; + } + return hash; +} + +export function generateCommitmentHash(data: CitizenshipData): string { + const str = JSON.stringify(data); + return simpleHash(str).toString(16); +} + +export function generateNullifierHash(address: string, timestamp: number): string { + const str = address + timestamp.toString(); + return 'nullifier_' + simpleHash(str).toString(16); +} + +// ── Encryption & Storage ──────────────────────────────────────────── + +export function encryptCitizenshipData(data: CitizenshipData): string { + return btoa(JSON.stringify(data)); +} + +export function decryptCitizenshipData(encrypted: string): CitizenshipData | null { + try { + return JSON.parse(atob(encrypted)); + } catch { + return null; + } +} + +export function saveCitizenshipLocally(data: CitizenshipData): void { + const encrypted = encryptCitizenshipData(data); + localStorage.setItem('pezkuwi_citizenship_data', encrypted); +} + +export function getLocalCitizenshipData(): CitizenshipData | null { + const encrypted = localStorage.getItem('pezkuwi_citizenship_data'); + if (!encrypted) return null; + return decryptCitizenshipData(encrypted); +} + +// ── IPFS (Mock) ───────────────────────────────────────────────────── + +export async function uploadToIPFS(_data: CitizenshipData): Promise { + // Mock IPFS upload - same as pwap/web + const mockCID = 'Qm' + Math.random().toString(36).substring(2, 15); + // In production, use Pinata or other IPFS service + return mockCID; +} + +// ── KYC Status ────────────────────────────────────────────────────── + +export async function getKycStatus(api: ApiPromise, address: string): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (!(api?.query as any)?.identityKyc) { + return 'NotStarted'; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const status = await (api.query as any).identityKyc.kycStatuses(address); + + if (status.isEmpty) { + return 'NotStarted'; + } + + const statusStr = status.toString(); + if (statusStr === 'Approved') return 'Approved'; + if (statusStr === 'Pending') return 'Pending'; + if (statusStr === 'Rejected') return 'Rejected'; + + return 'NotStarted'; + } catch (error) { + console.error('[Citizenship] Error fetching KYC status:', error); + return 'NotStarted'; + } +} + +// ── Blockchain Submission ─────────────────────────────────────────── + +export async function submitCitizenshipApplication( + api: ApiPromise, + keypair: KeyringPair, + name: string, + email: string, + ipfsCid: string, + notes: string = 'Citizenship application via Telegram MiniApp' +): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tx = api.tx as any; + if (!tx?.identityKyc?.setIdentity || !tx?.identityKyc?.applyForKyc) { + return { success: false, error: 'Identity KYC pallet not available' }; + } + + const address = keypair.address; + + // Check for pending application + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pendingApp = await (api.query as any).identityKyc.pendingKycApplications(address); + if (!pendingApp.isEmpty) { + return { + success: false, + error: 'You already have a pending citizenship application.', + }; + } + + // Check if already approved + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const kycStatus = await (api.query as any).identityKyc.kycStatuses(address); + if (kycStatus.toString() === 'Approved') { + return { + success: false, + error: 'Your citizenship is already approved!', + }; + } + + const cidString = String(ipfsCid); + if (!cidString || cidString === 'undefined') { + return { success: false, error: 'Invalid IPFS CID' }; + } + + // Step 1: Set identity + const identityResult = await new Promise((resolve) => { + tx.identityKyc + .setIdentity(name, email) + .signAndSend( + keypair, + { nonce: -1 }, + ({ + status, + dispatchError, + }: { + status: { isInBlock: boolean; isFinalized: boolean }; + dispatchError?: { isModule: boolean; asModule: unknown; toString: () => string }; + }) => { + if (status.isInBlock || status.isFinalized) { + if (dispatchError) { + let errorMessage = 'Identity transaction 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; + } + resolve({ success: true }); + } + } + ) + .catch((error: Error) => resolve({ success: false, error: error.message })); + }); + + if (!identityResult.success) { + return identityResult; + } + + // Step 2: Apply for KYC + const kycResult = await new Promise((resolve) => { + tx.identityKyc + .applyForKyc(cidString, notes) + .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 = 'KYC application 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 kycResult; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } +} + +// ── Citizen Number Generation ─────────────────────────────────────── + +export function generateCitizenNumber( + ownerAddress: string, + collectionId: number, + itemId: number +): string { + let hash = 0; + for (let i = 0; i < ownerAddress.length; i++) { + hash = (hash << 5) - hash + ownerAddress.charCodeAt(i); + hash = hash & hash; + } + hash += collectionId * 1000 + itemId; + hash = Math.abs(hash); + return (hash % 1000000).toString().padStart(6, '0'); +} diff --git a/src/pages/CitizenPage.tsx b/src/pages/CitizenPage.tsx new file mode 100644 index 0000000..546982e --- /dev/null +++ b/src/pages/CitizenPage.tsx @@ -0,0 +1,195 @@ +/** + * Citizen Page + * Standalone page for citizenship application flow + * Accessed via ?page=citizen URL parameter + * No bottom navigation bar + */ + +import { useState, useCallback, lazy, Suspense } from 'react'; +import { Globe, Loader2 } from 'lucide-react'; +import { useWallet } from '@/contexts/WalletContext'; +import { useTranslation, LANGUAGE_NAMES, VALID_LANGS } from '@/i18n'; +import type { LanguageCode } from '@/i18n'; +import type { CitizenshipData } from '@/lib/citizenship'; + +// Lazy load sub-components +const WalletSetup = lazy(() => + import('@/components/wallet/WalletSetup').then((m) => ({ default: m.WalletSetup })) +); +const WalletCreate = lazy(() => + import('@/components/wallet/WalletCreate').then((m) => ({ default: m.WalletCreate })) +); +const WalletImport = lazy(() => + import('@/components/wallet/WalletImport').then((m) => ({ default: m.WalletImport })) +); +const WalletConnect = lazy(() => + import('@/components/wallet/WalletConnect').then((m) => ({ default: m.WalletConnect })) +); +const CitizenForm = lazy(() => + import('@/components/citizen/CitizenForm').then((m) => ({ default: m.CitizenForm })) +); +const CitizenProcessing = lazy(() => + import('@/components/citizen/CitizenProcessing').then((m) => ({ default: m.CitizenProcessing })) +); +const CitizenSuccess = lazy(() => + import('@/components/citizen/CitizenSuccess').then((m) => ({ default: m.CitizenSuccess })) +); + +type Step = + | 'wallet-setup' + | 'wallet-create' + | 'wallet-import' + | 'wallet-connect' + | 'form' + | 'processing' + | 'success'; + +function SectionLoader() { + return ( +
+ +
+ ); +} + +export function CitizenPage() { + const { hasWallet, isConnected, address, deleteWalletData } = useWallet(); + const { t, lang, setLang } = useTranslation(); + const [showLangMenu, setShowLangMenu] = useState(false); + const [citizenshipData, setCitizenshipData] = useState(null); + const [error, setError] = useState(null); + + // Determine initial step based on wallet state + const getInitialStep = (): Step => { + if (!hasWallet) return 'wallet-setup'; + if (!isConnected) return 'wallet-connect'; + return 'form'; + }; + + const [step, setStep] = useState(getInitialStep); + + // Wallet setup handlers + const handleWalletCreate = useCallback(() => setStep('wallet-create'), []); + const handleWalletImport = useCallback(() => setStep('wallet-import'), []); + const handleWalletCreated = useCallback(() => setStep('wallet-connect'), []); + const handleWalletImported = useCallback(() => setStep('wallet-connect'), []); + const handleWalletConnected = useCallback(() => setStep('form'), []); + const handleWalletDelete = useCallback(() => { + deleteWalletData(); + setStep('wallet-setup'); + }, [deleteWalletData]); + + // Form submission + const handleFormSubmit = useCallback((data: CitizenshipData) => { + setCitizenshipData(data); + setError(null); + setStep('processing'); + }, []); + + // Processing result + const handleSuccess = useCallback(() => { + setStep('success'); + }, []); + + const handleError = useCallback((err: string) => { + setError(err); + setStep('form'); + }, []); + + // Open main app + const handleOpenApp = useCallback(() => { + // Remove ?page=citizen and navigate to main app + const url = new URL(window.location.href); + url.searchParams.delete('page'); + window.location.href = url.toString(); + }, []); + + return ( +
+ {/* Header */} +
+

{t('citizen.pageTitle')}

+ + {/* Language Selector */} +
+ + + {showLangMenu && ( + <> +
setShowLangMenu(false)} /> +
+ {VALID_LANGS.map((code) => ( + + ))} +
+ + )} +
+
+ + {/* Error Banner */} + {error && ( +
+ {error} +
+ )} + + {/* Content */} +
+ }> + {step === 'wallet-setup' && ( + + )} + + {step === 'wallet-create' && ( + setStep('wallet-setup')} /> + )} + + {step === 'wallet-import' && ( + setStep('wallet-setup')} + /> + )} + + {step === 'wallet-connect' && ( + + )} + + {step === 'form' && address && ( + + )} + + {step === 'processing' && citizenshipData && ( + + )} + + {step === 'success' && address && ( + + )} + +
+
+ ); +} diff --git a/src/version.json b/src/version.json index 2c942a8..79440bc 100644 --- a/src/version.json +++ b/src/version.json @@ -1,5 +1,5 @@ { - "version": "1.0.190", - "buildTime": "2026-02-14T15:16:08.944Z", - "buildNumber": 1771082168945 + "version": "1.0.192", + "buildTime": "2026-02-14T17:44:17.743Z", + "buildNumber": 1771091057743 } diff --git a/supabase/functions/telegram-bot/index.ts b/supabase/functions/telegram-bot/index.ts index 15b466d..1b7e454 100644 --- a/supabase/functions/telegram-bot/index.ts +++ b/supabase/functions/telegram-bot/index.ts @@ -1,18 +1,38 @@ /** * PezkuwiChain Telegram Bot - Supabase Edge Function - * Handles webhook updates from Telegram + * Handles webhook updates from two separate bots: + * - @Pezkuwichain_Bot (main) → telegram.pezkuwichain.io + * - @pezkuwichainBot (krd) → telegram.pezkiwi.app */ import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'; +import { Keyring } from 'npm:@pezkuwi/api@16.5.36'; +import { cryptoWaitReady } from 'npm:@pezkuwi/util-crypto@14.0.25'; +import * as bip39 from 'https://esm.sh/@scure/bip39@1.2.1'; +import { wordlist } from 'https://esm.sh/@scure/bip39@1.2.1/wordlists/english'; -const BOT_TOKEN = Deno.env.get('TELEGRAM_BOT_TOKEN') || ''; -const MINI_APP_URL = 'https://telegram.pezkuwichain.io'; +// ── Bot configuration ─────────────────────────────────────────────── +const BOT_TOKENS: Record = { + main: Deno.env.get('TELEGRAM_BOT_TOKEN') || '', + krd: Deno.env.get('TELEGRAM_BOT_TOKEN_KRD') || '', +}; -// Welcome image URL (hosted on GitHub) +const MINI_APP_URLS: Record = { + main: 'https://telegram.pezkuwichain.io', + krd: 'https://telegram.pezkiwi.app', +}; + +function getBotId(req: Request): string { + const url = new URL(req.url); + return url.searchParams.get('bot') || 'main'; +} + +// ── Welcome image ─────────────────────────────────────────────────── const WELCOME_IMAGE_URL = 'https://raw.githubusercontent.com/pezkuwichain/pezkuwi-telegram-miniapp/main/public/images/welcome.png'; -const WELCOME_MESSAGE = ` +// ── Main bot (@Pezkuwichain_Bot) welcome ──────────────────────────── +const MAIN_WELCOME_MESSAGE = ` 🌍 Welcome to PezkuwiChain! The first blockchain platform connecting Kurds worldwide — building a digital Kurdistan where borders don't limit our unity. @@ -25,6 +45,18 @@ Join millions of Kurds in creating a decentralized digital economy. Your wallet, Together, we can. `; +// ── KRD bot (@pezkuwichainBot) welcome ────────────────────────────── +const KRD_WELCOME_MESSAGE = ` +🌐 Pezkuwî + +Bi Pezkuwî re dest bi rêwîtiya xwe ya dîjîtal bikin. +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. +`; + +// ── Types ──────────────────────────────────────────────────────────── interface TelegramUpdate { update_id: number; message?: { @@ -45,20 +77,25 @@ interface TelegramUpdate { from: { id: number; }; + message?: { + chat: { + id: number; + }; + }; data: string; }; } -// Send message via Telegram API -async function sendTelegramRequest(method: string, body: Record) { +// ── Telegram API helper ───────────────────────────────────────────── +async function sendTelegramRequest(token: string, method: string, body: Record) { console.log(`[Telegram] Calling ${method}`, JSON.stringify(body)); - if (!BOT_TOKEN) { + if (!token) { console.error('[Telegram] BOT_TOKEN is not set!'); return { ok: false, error: 'BOT_TOKEN not configured' }; } - const response = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/${method}`, { + const response = await fetch(`https://api.telegram.org/bot${token}/${method}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), @@ -66,18 +103,17 @@ async function sendTelegramRequest(method: string, body: Record const result = await response.json(); console.log(`[Telegram] Response:`, JSON.stringify(result)); - return result; } -// Send welcome message with photo -async function sendWelcomeMessage(chatId: number) { +// ── Main bot: welcome ─────────────────────────────────────────────── +async function sendMainWelcome(token: string, chatId: number) { const keyboard = { inline_keyboard: [ [ { text: '📱 Open App on Telegram', - web_app: { url: MINI_APP_URL }, + web_app: { url: MINI_APP_URLS.main }, }, ], [ @@ -89,12 +125,11 @@ async function sendWelcomeMessage(chatId: number) { ], }; - // Try to send photo with caption if (WELCOME_IMAGE_URL) { - const result = await sendTelegramRequest('sendPhoto', { + const result = await sendTelegramRequest(token, 'sendPhoto', { chat_id: chatId, photo: WELCOME_IMAGE_URL, - caption: WELCOME_MESSAGE, + caption: MAIN_WELCOME_MESSAGE, parse_mode: 'HTML', reply_markup: keyboard, }); @@ -102,19 +137,126 @@ async function sendWelcomeMessage(chatId: number) { console.log('[Bot] Photo failed, falling back to text'); } - // Send text-only message - await sendTelegramRequest('sendMessage', { + await sendTelegramRequest(token, 'sendMessage', { chat_id: chatId, - text: WELCOME_MESSAGE, + text: MAIN_WELCOME_MESSAGE, parse_mode: 'HTML', reply_markup: keyboard, }); } -// Handle callback query (button clicks) -async function handleCallbackQuery(callbackQueryId: string, data: string) { - if (data === 'playstore_coming_soon') { - await sendTelegramRequest('answerCallbackQuery', { +// ── KRD bot: welcome ──────────────────────────────────────────────── +async function sendKrdWelcome(token: string, chatId: number) { + const appUrl = MINI_APP_URLS.krd; + + const keyboard = { + inline_keyboard: [ + // Row 1: Create Wallet (callback - bot generates wallet in chat) + [ + { + text: '🔐 Create Wallet / Cûzdan Biafirîne', + callback_data: 'create_wallet', + }, + ], + // Row 2: Languages (top row) + [ + { text: 'Kurmancî', web_app: { url: `${appUrl}/krd` } }, + { text: 'English', web_app: { url: `${appUrl}/en` } }, + { text: 'Türkçe', web_app: { url: `${appUrl}/tr` } }, + ], + // Row 3: Languages (bottom row) + [ + { text: 'سۆرانی', web_app: { url: `${appUrl}/ckb` } }, + { text: 'فارسی', web_app: { url: `${appUrl}/fa` } }, + { text: 'العربية', web_app: { url: `${appUrl}/ar` } }, + ], + // Row 4: Be Citizen + [ + { + text: '🏛️ Be Citizen / Bibe Welatî', + web_app: { url: `${appUrl}?page=citizen` }, + }, + ], + ], + }; + + if (WELCOME_IMAGE_URL) { + const result = await sendTelegramRequest(token, 'sendPhoto', { + chat_id: chatId, + photo: WELCOME_IMAGE_URL, + caption: KRD_WELCOME_MESSAGE, + parse_mode: 'HTML', + reply_markup: keyboard, + }); + if (result.ok) return; + console.log('[Bot] Photo failed, falling back to text'); + } + + await sendTelegramRequest(token, 'sendMessage', { + chat_id: chatId, + text: KRD_WELCOME_MESSAGE, + parse_mode: 'HTML', + reply_markup: keyboard, + }); +} + +// ── Create wallet handler ─────────────────────────────────────────── +async function handleCreateWallet(token: string, chatId: number) { + try { + await cryptoWaitReady(); + + const mnemonic = bip39.generateMnemonic(wordlist, 128); + const keyring = new Keyring({ type: 'sr25519' }); + const pair = keyring.addFromUri(mnemonic); + const address = pair.address; + + const walletMessage = ` +🔐 Cûzdanê Te Hate Afirandin! +Your Wallet Has Been Created! + +📍 Address / Navnîşan: +${address} + +🔑 Seed Phrase (12 words): +${mnemonic} + +⚠️ GIRÎNG / IMPORTANT: +Ev 12 peyvan binivîsin û li cihekî ewle bihêlin. +Kesî re nîşan nedin! Eger winda bikin, cûzdanê xwe winda dikin. + +Write down these 12 words and keep them safe. +Never share them! If you lose them, you lose your wallet. +`; + + await sendTelegramRequest(token, 'sendMessage', { + chat_id: chatId, + text: walletMessage, + parse_mode: 'HTML', + }); + } catch (error) { + console.error('[Bot] Wallet generation error:', error); + await sendTelegramRequest(token, 'sendMessage', { + chat_id: chatId, + text: '❌ Wallet creation failed. Please try again.', + }); + } +} + +// ── Callback handler ──────────────────────────────────────────────── +async function handleCallbackQuery( + token: string, + callbackQueryId: string, + data: string, + chatId: number | undefined +) { + if (data === 'create_wallet' && chatId) { + await sendTelegramRequest(token, 'answerCallbackQuery', { + callback_query_id: callbackQueryId, + text: '🔐 Creating your wallet...', + }); + await handleCreateWallet(token, chatId); + } else if (data === 'playstore_coming_soon') { + await sendTelegramRequest(token, 'answerCallbackQuery', { callback_query_id: callbackQueryId, text: '🚀 Android app coming soon! Stay tuned.', show_alert: true, @@ -122,8 +264,9 @@ async function handleCallbackQuery(callbackQueryId: string, data: string) { } } -// Send help message -async function sendHelpMessage(chatId: number) { +// ── Help & App commands ───────────────────────────────────────────── +async function sendHelpMessage(token: string, chatId: number, botId: string) { + const appUrl = MINI_APP_URLS[botId] || MINI_APP_URLS.main; const helpText = ` PezkuwiChain Bot Commands: @@ -133,39 +276,38 @@ async function sendHelpMessage(chatId: number) { Links: 🌐 Website: pezkuwichain.io -📱 App: t.me/pezkuwichain_bot/app -🐦 Twitter: @pezkuwichain +📱 App: ${appUrl} `; - await sendTelegramRequest('sendMessage', { + await sendTelegramRequest(token, 'sendMessage', { chat_id: chatId, text: helpText, parse_mode: 'HTML', }); } -// Send app link -async function sendAppLink(chatId: number) { +async function sendAppLink(token: string, chatId: number, botId: string) { + const appUrl = MINI_APP_URLS[botId] || MINI_APP_URLS.main; const keyboard = { inline_keyboard: [ [ { text: '📱 Open PezkuwiChain App', - web_app: { url: MINI_APP_URL }, + web_app: { url: appUrl }, }, ], ], }; - await sendTelegramRequest('sendMessage', { + await sendTelegramRequest(token, 'sendMessage', { chat_id: chatId, text: 'Click below to open the app:', reply_markup: keyboard, }); } +// ── Main handler ──────────────────────────────────────────────────── serve(async (req: Request) => { - // Handle CORS preflight if (req.method === 'OPTIONS') { return new Response(null, { headers: { @@ -176,14 +318,15 @@ serve(async (req: Request) => { }); } - // Only accept POST requests if (req.method !== 'POST') { return new Response('Method not allowed', { status: 405 }); } try { + const botId = getBotId(req); + const botToken = BOT_TOKENS[botId] || BOT_TOKENS.main; const update: TelegramUpdate = await req.json(); - console.log('[Bot] Received update:', JSON.stringify(update)); + console.log(`[Bot:${botId}] Received update:`, JSON.stringify(update)); // Handle message if (update.message?.text) { @@ -191,17 +334,27 @@ serve(async (req: Request) => { const text = update.message.text; if (text === '/start' || text.startsWith('/start ')) { - await sendWelcomeMessage(chatId); + if (botId === 'krd') { + await sendKrdWelcome(botToken, chatId); + } else { + await sendMainWelcome(botToken, chatId); + } } else if (text === '/help') { - await sendHelpMessage(chatId); + await sendHelpMessage(botToken, chatId, botId); } else if (text === '/app') { - await sendAppLink(chatId); + await sendAppLink(botToken, chatId, botId); } } - // Handle callback query (button clicks) + // Handle callback query if (update.callback_query) { - await handleCallbackQuery(update.callback_query.id, update.callback_query.data); + const chatId = update.callback_query.message?.chat?.id; + await handleCallbackQuery( + botToken, + update.callback_query.id, + update.callback_query.data, + chatId + ); } return new Response(JSON.stringify({ ok: true }), {