mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-04-22 03:07:55 +00:00
feat: add Be Citizen page with 6-language support
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
+18
@@ -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 (
|
||||
<Suspense fallback={<SectionLoader />}>
|
||||
<CitizenPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <MainApp />;
|
||||
}
|
||||
|
||||
function MainApp() {
|
||||
const [activeSection, setActiveSection] = useState<Section>('announcements');
|
||||
const [showP2PModal, setShowP2PModal] = useState(false);
|
||||
const { sessionToken } = useAuth();
|
||||
|
||||
@@ -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<MaritalStatus>('nezewici');
|
||||
const [childrenCount, setChildrenCount] = useState(0);
|
||||
const [children, setChildren] = useState<ChildInfo[]>([]);
|
||||
const [region, setRegion] = useState<Region | ''>('');
|
||||
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 (
|
||||
<div className="p-4 space-y-4 pb-24">
|
||||
{/* Full Name */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.fullName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fullName}
|
||||
onChange={(e) => setFullName(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.fullNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Father's Name */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.fatherName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fatherName}
|
||||
onChange={(e) => setFatherName(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.fatherNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Grandfather's Name */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.grandfatherName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={grandfatherName}
|
||||
onChange={(e) => setGrandfatherName(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.grandfatherNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mother's Name */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.motherName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={motherName}
|
||||
onChange={(e) => setMotherName(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.motherNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tribe */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.tribe')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tribe}
|
||||
onChange={(e) => setTribe(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.tribePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Marital Status */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.maritalStatus')}</label>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMaritalChange('nezewici')}
|
||||
className={`flex-1 py-3 rounded-xl text-sm font-medium transition-colors ${
|
||||
maritalStatus === 'nezewici'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{t('citizen.single')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMaritalChange('zewici')}
|
||||
className={`flex-1 py-3 rounded-xl text-sm font-medium transition-colors ${
|
||||
maritalStatus === 'zewici'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{t('citizen.married')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Children (if married) */}
|
||||
{maritalStatus === 'zewici' && (
|
||||
<div className="space-y-3">
|
||||
<label className={labelClass}>{t('citizen.childrenCount')}</label>
|
||||
{children.map((child, index) => (
|
||||
<div key={index} className="flex gap-2 items-end">
|
||||
<div className="flex-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t('citizen.childName', { index: String(index + 1) })}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={child.name}
|
||||
onChange={(e) => updateChild(index, 'name', e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.childNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t('citizen.childBirthYear')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={child.birthYear}
|
||||
onChange={(e) =>
|
||||
updateChild(index, 'birthYear', parseInt(e.target.value) || 2000)
|
||||
}
|
||||
className={inputClass}
|
||||
min={1950}
|
||||
max={2026}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeChild(index)}
|
||||
className="p-3 text-red-400 hover:text-red-300"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={addChild}
|
||||
className="flex items-center gap-2 text-sm text-primary"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('citizen.addChild')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Region */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.region')}</label>
|
||||
<select
|
||||
value={region}
|
||||
onChange={(e) => {
|
||||
setRegion(e.target.value as Region);
|
||||
hapticImpact('light');
|
||||
}}
|
||||
className={`${inputClass} appearance-none`}
|
||||
>
|
||||
<option value="">{t('citizen.regionPlaceholder')}</option>
|
||||
{REGIONS.map((r) => (
|
||||
<option key={r.value} value={r.value}>
|
||||
{t(r.labelKey)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.email')}</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.emailPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Profession */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.profession')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={profession}
|
||||
onChange={(e) => setProfession(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.professionPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Referral Code */}
|
||||
<div>
|
||||
<label className={labelClass}>{t('citizen.referralCode')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={referralCode}
|
||||
onChange={(e) => setReferralCode(e.target.value)}
|
||||
className={inputClass}
|
||||
placeholder={t('citizen.referralCodePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Consent */}
|
||||
<label className="flex items-start gap-3 p-3 bg-muted/50 rounded-xl cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={consent}
|
||||
onChange={(e) => setConsent(e.target.checked)}
|
||||
className="mt-1 w-5 h-5 rounded accent-primary flex-shrink-0"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">{t('citizen.consentCheckbox')}</span>
|
||||
</label>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!consent || !fullName || !region || !email}
|
||||
className="w-full py-3 bg-primary text-primary-foreground rounded-xl font-semibold disabled:opacity-50"
|
||||
>
|
||||
{t('citizen.submit')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<ProcessingState>('preparing');
|
||||
const [ipfsCid, setIpfsCid] = useState<string>('');
|
||||
|
||||
// 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 (
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] p-6 space-y-8">
|
||||
{/* Kurdistan Sun Animation */}
|
||||
<div className={state === 'ready' ? 'opacity-80' : ''}>
|
||||
<KurdistanSun size={100} />
|
||||
</div>
|
||||
|
||||
{/* Status Message */}
|
||||
<div className="text-center space-y-2">
|
||||
<p className="text-lg font-medium">
|
||||
{state === 'preparing' && t('citizen.preparingData')}
|
||||
{state === 'ready' && t('citizen.readyToSign')}
|
||||
{state === 'signing' && t('citizen.signingTx')}
|
||||
</p>
|
||||
{state === 'preparing' && (
|
||||
<p className="text-sm text-muted-foreground">{citizenshipData.fullName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sign Button */}
|
||||
<button
|
||||
onClick={handleSign}
|
||||
disabled={!isReady || isSigning}
|
||||
className={`w-full max-w-xs py-4 rounded-xl font-bold text-lg transition-all ${
|
||||
isReady
|
||||
? 'bg-green-600 text-white hover:bg-green-500 active:scale-95'
|
||||
: 'bg-muted text-muted-foreground cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{isSigning ? t('citizen.signingTx') : t('citizen.sign')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex flex-col items-center justify-center min-h-[70vh] p-6 space-y-6">
|
||||
{/* Success Icon */}
|
||||
<div className="w-20 h-20 bg-green-500/20 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="w-12 h-12 text-green-500" />
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-2xl font-bold">{t('citizen.successTitle')}</h1>
|
||||
<p className="text-muted-foreground">{t('citizen.successSubtitle')}</p>
|
||||
</div>
|
||||
|
||||
{/* Citizen ID Card */}
|
||||
<div className="w-full max-w-sm bg-muted/50 rounded-2xl p-5 space-y-4 border border-border">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t('citizen.citizenId')}</p>
|
||||
<p className="text-xl font-mono font-bold text-primary">{citizenId}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t('citizen.walletAddress')}</p>
|
||||
<p className="text-sm font-mono">{formatAddress(address)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Open App Button */}
|
||||
<button
|
||||
onClick={handleOpenApp}
|
||||
className="w-full max-w-sm py-3 bg-primary text-primary-foreground rounded-xl font-semibold"
|
||||
>
|
||||
{t('citizen.openApp')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<string> {
|
||||
// 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<KycStatus> {
|
||||
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<CitizenshipResult> {
|
||||
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<CitizenshipResult>((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<typeof api.registry.findMetaError>[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<CitizenshipResult>((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<typeof api.registry.findMetaError>[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');
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="w-8 h-8 text-primary animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CitizenPage() {
|
||||
const { hasWallet, isConnected, address, deleteWalletData } = useWallet();
|
||||
const { t, lang, setLang } = useTranslation();
|
||||
const [showLangMenu, setShowLangMenu] = useState(false);
|
||||
const [citizenshipData, setCitizenshipData] = useState<CitizenshipData | null>(null);
|
||||
const [error, setError] = useState<string | null>(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<Step>(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 (
|
||||
<div className="flex flex-col h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="flex-shrink-0 flex items-center justify-between px-4 py-3 border-b border-border">
|
||||
<h1 className="text-lg font-bold">{t('citizen.pageTitle')}</h1>
|
||||
|
||||
{/* Language Selector */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowLangMenu(!showLangMenu)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-muted rounded-lg text-sm"
|
||||
>
|
||||
<Globe className="w-4 h-4" />
|
||||
<span>{LANGUAGE_NAMES[lang]}</span>
|
||||
</button>
|
||||
|
||||
{showLangMenu && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-10" onClick={() => setShowLangMenu(false)} />
|
||||
<div className="absolute end-0 top-full mt-1 z-20 bg-secondary border border-border rounded-xl shadow-lg py-1 min-w-[140px]">
|
||||
{VALID_LANGS.map((code) => (
|
||||
<button
|
||||
key={code}
|
||||
onClick={() => {
|
||||
setLang(code as LanguageCode);
|
||||
setShowLangMenu(false);
|
||||
}}
|
||||
className={`w-full px-4 py-2 text-start text-sm hover:bg-muted transition-colors ${
|
||||
lang === code ? 'text-primary font-medium' : ''
|
||||
}`}
|
||||
>
|
||||
{LANGUAGE_NAMES[code as LanguageCode]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Error Banner */}
|
||||
{error && (
|
||||
<div className="mx-4 mt-3 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<Suspense fallback={<SectionLoader />}>
|
||||
{step === 'wallet-setup' && (
|
||||
<WalletSetup onCreate={handleWalletCreate} onImport={handleWalletImport} />
|
||||
)}
|
||||
|
||||
{step === 'wallet-create' && (
|
||||
<WalletCreate onComplete={handleWalletCreated} onBack={() => setStep('wallet-setup')} />
|
||||
)}
|
||||
|
||||
{step === 'wallet-import' && (
|
||||
<WalletImport
|
||||
onComplete={handleWalletImported}
|
||||
onBack={() => setStep('wallet-setup')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 'wallet-connect' && (
|
||||
<WalletConnect onConnected={handleWalletConnected} onDelete={handleWalletDelete} />
|
||||
)}
|
||||
|
||||
{step === 'form' && address && (
|
||||
<CitizenForm walletAddress={address} onSubmit={handleFormSubmit} />
|
||||
)}
|
||||
|
||||
{step === 'processing' && citizenshipData && (
|
||||
<CitizenProcessing
|
||||
citizenshipData={citizenshipData}
|
||||
onSuccess={handleSuccess}
|
||||
onError={handleError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 'success' && address && (
|
||||
<CitizenSuccess address={address} onOpenApp={handleOpenApp} />
|
||||
)}
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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<string, string> = {
|
||||
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 = `
|
||||
🌍 <b>Welcome to PezkuwiChain!</b>
|
||||
|
||||
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,
|
||||
<i>Together, we can.</i>
|
||||
`;
|
||||
|
||||
// ── KRD bot (@pezkuwichainBot) welcome ──────────────────────────────
|
||||
const KRD_WELCOME_MESSAGE = `
|
||||
🌐 <b>Pezkuwî</b>
|
||||
|
||||
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.
|
||||
|
||||
<i>Start your digital journey with Pezkuwi.
|
||||
Create your wallet, choose your language and become a citizen.</i>
|
||||
`;
|
||||
|
||||
// ── 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<string, unknown>) {
|
||||
// ── Telegram API helper ─────────────────────────────────────────────
|
||||
async function sendTelegramRequest(token: string, method: string, body: Record<string, unknown>) {
|
||||
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<string, unknown>
|
||||
|
||||
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 = `
|
||||
🔐 <b>Cûzdanê Te Hate Afirandin!</b>
|
||||
<b>Your Wallet Has Been Created!</b>
|
||||
|
||||
📍 <b>Address / Navnîşan:</b>
|
||||
<code>${address}</code>
|
||||
|
||||
🔑 <b>Seed Phrase (12 words):</b>
|
||||
<tg-spoiler>${mnemonic}</tg-spoiler>
|
||||
|
||||
⚠️ <b>GIRÎNG / IMPORTANT:</b>
|
||||
<i>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.</i>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<b>PezkuwiChain Bot Commands:</b>
|
||||
|
||||
@@ -133,39 +276,38 @@ async function sendHelpMessage(chatId: number) {
|
||||
|
||||
<b>Links:</b>
|
||||
🌐 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 }), {
|
||||
|
||||
Reference in New Issue
Block a user