feat: simplify Be Citizen flow - remove wallet steps, add seed phrase input

- Remove wallet setup/create/import/connect steps from CitizenPage
- Add privacy notice banner with Shield icon to form
- Add seed phrase textarea with mnemonic validation
- CitizenProcessing creates keypair directly from seed phrase
- CitizenSuccess shows 3-step next process info
- Add /citizens path support alongside ?page=citizen
- Update bot URL to /citizens
- Add 10 new i18n keys in all 6 languages
This commit is contained in:
2026-02-14 23:24:59 +03:00
parent f864ed6804
commit 8f4b9087f3
16 changed files with 221 additions and 114 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pezkuwi-telegram-miniapp", "name": "pezkuwi-telegram-miniapp",
"version": "1.0.194", "version": "1.0.196",
"type": "module", "type": "module",
"description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards", "description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards",
"author": "Pezkuwichain Team", "author": "Pezkuwichain Team",
+3 -2
View File
@@ -54,11 +54,12 @@ const NAV_ITEMS: NavItem[] = [
// P2P Web App URL - Mobile-optimized P2P // P2P Web App URL - Mobile-optimized P2P
const P2P_WEB_URL = 'https://telegram.pezkuwichain.io/p2p'; const P2P_WEB_URL = 'https://telegram.pezkuwichain.io/p2p';
// Check for standalone pages via URL query params (evaluated once at module level) // Check for standalone pages via URL query params or path (evaluated once at module level)
const PAGE_PARAM = new URLSearchParams(window.location.search).get('page'); const PAGE_PARAM = new URLSearchParams(window.location.search).get('page');
const IS_CITIZEN_PAGE = PAGE_PARAM === 'citizen' || window.location.pathname === '/citizens';
export default function App() { export default function App() {
if (PAGE_PARAM === 'citizen') { if (IS_CITIZEN_PAGE) {
return ( return (
<Suspense fallback={<SectionLoader />}> <Suspense fallback={<SectionLoader />}>
<CitizenPage /> <CitizenPage />
+63 -6
View File
@@ -1,16 +1,16 @@
/** /**
* Citizen Application Form * Citizen Application Form
* Collects citizenship data from the user * Collects citizenship data and seed phrase from the user
*/ */
import { useState } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { Plus, Trash2 } from 'lucide-react'; import { Plus, Trash2, Shield } from 'lucide-react';
import { useTranslation } from '@/i18n'; import { useTranslation } from '@/i18n';
import { useTelegram } from '@/hooks/useTelegram'; import { useTelegram } from '@/hooks/useTelegram';
import { initWalletService, validateMnemonic, getAddressFromMnemonic } from '@/lib/wallet-service';
import type { CitizenshipData, Region, MaritalStatus, ChildInfo } from '@/lib/citizenship'; import type { CitizenshipData, Region, MaritalStatus, ChildInfo } from '@/lib/citizenship';
interface Props { interface Props {
walletAddress: string;
onSubmit: (data: CitizenshipData) => void; onSubmit: (data: CitizenshipData) => void;
} }
@@ -23,7 +23,7 @@ const REGIONS: { value: Region; labelKey: string }[] = [
{ value: 'diaspora', labelKey: 'citizen.regionDiaspora' }, { value: 'diaspora', labelKey: 'citizen.regionDiaspora' },
]; ];
export function CitizenForm({ walletAddress, onSubmit }: Props) { export function CitizenForm({ onSubmit }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const { hapticImpact, hapticNotification } = useTelegram(); const { hapticImpact, hapticNotification } = useTelegram();
@@ -39,8 +39,33 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [profession, setProfession] = useState(''); const [profession, setProfession] = useState('');
const [referrerAddress, setReferrerAddress] = useState(''); const [referrerAddress, setReferrerAddress] = useState('');
const [seedPhrase, setSeedPhrase] = useState('');
const [consent, setConsent] = useState(false); const [consent, setConsent] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [cryptoReady, setCryptoReady] = useState(false);
// Initialize crypto libraries for mnemonic validation
useEffect(() => {
initWalletService().then(() => setCryptoReady(true));
}, []);
// Derive seed phrase validation error (no setState in effect)
const seedPhraseError = useMemo(() => {
const trimmed = seedPhrase.trim();
if (!trimmed) return '';
if (!cryptoReady) return '';
const words = trimmed.split(/\s+/);
if (words.length !== 12 && words.length !== 24) {
return t('citizen.invalidSeedPhrase');
}
if (!validateMnemonic(trimmed)) {
return t('citizen.invalidSeedPhrase');
}
return '';
}, [seedPhrase, cryptoReady, t]);
const handleMaritalChange = (status: MaritalStatus) => { const handleMaritalChange = (status: MaritalStatus) => {
hapticImpact('light'); hapticImpact('light');
@@ -100,6 +125,14 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
return; return;
} }
// Validate seed phrase
const trimmedSeed = seedPhrase.trim();
if (!trimmedSeed || !cryptoReady || !validateMnemonic(trimmedSeed)) {
setError(t('citizen.invalidSeedPhrase'));
hapticNotification('error');
return;
}
if (!consent) { if (!consent) {
setError(t('citizen.acceptConsent')); setError(t('citizen.acceptConsent'));
hapticNotification('error'); hapticNotification('error');
@@ -108,6 +141,9 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
hapticImpact('medium'); hapticImpact('medium');
// Derive wallet address from seed phrase
const walletAddress = getAddressFromMnemonic(trimmedSeed);
const data: CitizenshipData = { const data: CitizenshipData = {
fullName, fullName,
fatherName, fatherName,
@@ -122,17 +158,25 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
profession, profession,
referrerAddress: referrerAddress || undefined, referrerAddress: referrerAddress || undefined,
walletAddress, walletAddress,
seedPhrase: trimmedSeed,
timestamp: Date.now(), timestamp: Date.now(),
}; };
onSubmit(data); onSubmit(data);
}; };
const isSeedValid = cryptoReady && seedPhrase.trim() && !seedPhraseError;
const inputClass = 'w-full px-4 py-3 bg-muted rounded-xl text-sm'; const inputClass = 'w-full px-4 py-3 bg-muted rounded-xl text-sm';
const labelClass = 'text-sm text-muted-foreground mb-1 block'; const labelClass = 'text-sm text-muted-foreground mb-1 block';
return ( return (
<div className="p-4 space-y-4 pb-24"> <div className="p-4 space-y-4 pb-24">
{/* Privacy Notice */}
<div className="flex gap-3 p-3 bg-blue-500/10 border border-blue-500/30 rounded-xl">
<Shield className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
<p className="text-sm text-blue-300">{t('citizen.privacyNotice')}</p>
</div>
{/* Full Name */} {/* Full Name */}
<div> <div>
<label className={labelClass}>{t('citizen.fullName')}</label> <label className={labelClass}>{t('citizen.fullName')}</label>
@@ -319,6 +363,19 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
/> />
</div> </div>
{/* Seed Phrase */}
<div>
<label className={labelClass}>{t('citizen.seedPhrase')}</label>
<textarea
value={seedPhrase}
onChange={(e) => setSeedPhrase(e.target.value)}
className={`${inputClass} min-h-[80px] resize-none`}
placeholder={t('citizen.seedPhrasePlaceholder')}
rows={3}
/>
{seedPhraseError && <p className="text-xs text-red-400 mt-1">{seedPhraseError}</p>}
</div>
{/* Referrer Address */} {/* Referrer Address */}
<div> <div>
<label className={labelClass}>{t('citizen.referrerAddress')}</label> <label className={labelClass}>{t('citizen.referrerAddress')}</label>
@@ -352,7 +409,7 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
{/* Submit Button */} {/* Submit Button */}
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={!consent || !fullName || !region || !email} disabled={!consent || !fullName || !region || !email || !isSeedValid}
className="w-full py-3 bg-primary text-primary-foreground rounded-xl font-semibold disabled:opacity-50" className="w-full py-3 bg-primary text-primary-foreground rounded-xl font-semibold disabled:opacity-50"
> >
{t('citizen.submit')} {t('citizen.submit')}
+39 -24
View File
@@ -1,14 +1,15 @@
/** /**
* Citizen Processing Component * Citizen Processing Component
* Shows KurdistanSun animation while preparing data, * Shows KurdistanSun animation while preparing data,
* then enables sign button when ready * connects to People Chain and signs with seed phrase keypair
*/ */
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { KurdistanSun } from '@/components/KurdistanSun'; import { KurdistanSun } from '@/components/KurdistanSun';
import { useTranslation } from '@/i18n'; import { useTranslation } from '@/i18n';
import { useTelegram } from '@/hooks/useTelegram'; import { useTelegram } from '@/hooks/useTelegram';
import { useWallet } from '@/contexts/WalletContext'; import { initWalletService, createKeypair } from '@/lib/wallet-service';
import { initPeopleConnection } from '@/lib/rpc-manager';
import type { CitizenshipData } from '@/lib/citizenship'; import type { CitizenshipData } from '@/lib/citizenship';
import { import {
calculateIdentityHash, calculateIdentityHash,
@@ -19,24 +20,31 @@ import {
interface Props { interface Props {
citizenshipData: CitizenshipData; citizenshipData: CitizenshipData;
onSuccess: (identityHash: string, blockHash?: string) => void; onSuccess: (identityHash: string, walletAddress: string) => void;
onError: (error: string) => void; onError: (error: string) => void;
} }
type ProcessingState = 'preparing' | 'ready' | 'signing'; type ProcessingState = 'preparing' | 'connecting' | 'ready' | 'signing';
export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props) { export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const { hapticImpact, hapticNotification } = useTelegram(); const { hapticImpact, hapticNotification } = useTelegram();
const { peopleApi, keypair } = useWallet();
const [state, setState] = useState<ProcessingState>('preparing'); const [state, setState] = useState<ProcessingState>('preparing');
const [identityHash, setIdentityHash] = useState<string>(''); const [identityHash, setIdentityHash] = useState<string>('');
const seedPhraseRef = useRef<string>(citizenshipData.seedPhrase);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const peopleApiRef = useRef<any>(null);
// Prepare data on mount // Prepare data and connect to chain on mount
useEffect(() => { useEffect(() => {
let cancelled = false;
const prepare = async () => { const prepare = async () => {
try { try {
// Init crypto
await initWalletService();
// Mock IPFS upload // Mock IPFS upload
const ipfsCid = await uploadToIPFS(citizenshipData); const ipfsCid = await uploadToIPFS(citizenshipData);
@@ -44,26 +52,35 @@ export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props
const hash = calculateIdentityHash(citizenshipData.fullName, citizenshipData.email, [ const hash = calculateIdentityHash(citizenshipData.fullName, citizenshipData.email, [
ipfsCid, ipfsCid,
]); ]);
if (cancelled) return;
setIdentityHash(hash); setIdentityHash(hash);
// Save encrypted data locally // Save encrypted data locally
saveCitizenshipLocally(citizenshipData); saveCitizenshipLocally(citizenshipData);
// Small delay to show animation // Connect to People Chain
await new Promise((resolve) => setTimeout(resolve, 1500)); setState('connecting');
const peopleApi = await initPeopleConnection();
if (cancelled) return;
peopleApiRef.current = peopleApi;
setState('ready'); setState('ready');
hapticNotification('success'); hapticNotification('success');
} catch (err) { } catch (err) {
onError(err instanceof Error ? err.message : 'Preparation failed'); if (!cancelled) {
onError(err instanceof Error ? err.message : 'Preparation failed');
}
} }
}; };
prepare(); prepare();
return () => {
cancelled = true;
};
}, [citizenshipData, hapticNotification, onError]); }, [citizenshipData, hapticNotification, onError]);
const handleSign = useCallback(async () => { const handleSign = useCallback(async () => {
if (!peopleApi || !keypair) { if (!peopleApiRef.current || !seedPhraseRef.current) {
onError(t('citizen.walletNotConnected')); onError(t('citizen.walletNotConnected'));
return; return;
} }
@@ -72,35 +89,32 @@ export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props
hapticImpact('medium'); hapticImpact('medium');
try { try {
const keypair = createKeypair(seedPhraseRef.current);
const result = await applyCitizenship( const result = await applyCitizenship(
peopleApi, peopleApiRef.current,
keypair, keypair,
identityHash, identityHash,
citizenshipData.referrerAddress || null citizenshipData.referrerAddress || null
); );
// Clear seed phrase from memory
seedPhraseRef.current = '';
if (result.success) { if (result.success) {
hapticNotification('success'); hapticNotification('success');
onSuccess(identityHash, result.blockHash); onSuccess(identityHash, citizenshipData.walletAddress);
} else { } else {
hapticNotification('error'); hapticNotification('error');
onError(result.error || t('citizen.submissionFailed')); onError(result.error || t('citizen.submissionFailed'));
} }
} catch (err) { } catch (err) {
// Clear seed phrase from memory
seedPhraseRef.current = '';
hapticNotification('error'); hapticNotification('error');
onError(err instanceof Error ? err.message : t('citizen.submissionFailed')); onError(err instanceof Error ? err.message : t('citizen.submissionFailed'));
} }
}, [ }, [citizenshipData, identityHash, hapticImpact, hapticNotification, onSuccess, onError, t]);
peopleApi,
keypair,
citizenshipData,
identityHash,
hapticImpact,
hapticNotification,
onSuccess,
onError,
t,
]);
const isReady = state === 'ready'; const isReady = state === 'ready';
const isSigning = state === 'signing'; const isSigning = state === 'signing';
@@ -116,6 +130,7 @@ export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props
<div className="text-center space-y-2"> <div className="text-center space-y-2">
<p className="text-lg font-medium"> <p className="text-lg font-medium">
{state === 'preparing' && t('citizen.preparingData')} {state === 'preparing' && t('citizen.preparingData')}
{state === 'connecting' && t('citizen.connectingChain')}
{state === 'ready' && t('citizen.readyToSign')} {state === 'ready' && t('citizen.readyToSign')}
{state === 'signing' && t('citizen.signingTx')} {state === 'signing' && t('citizen.signingTx')}
</p> </p>
+25 -7
View File
@@ -1,10 +1,10 @@
/** /**
* Citizen Success Screen * Citizen Success Screen
* Shows after successful citizenship application submission * Shows after successful citizenship application submission
* Displays pending referral status instead of final approval * Displays 3-step process info for next steps
*/ */
import { CheckCircle, Clock } from 'lucide-react'; import { CheckCircle, Clock, ArrowRight } from 'lucide-react';
import { useTranslation } from '@/i18n'; import { useTranslation } from '@/i18n';
import { useTelegram } from '@/hooks/useTelegram'; import { useTelegram } from '@/hooks/useTelegram';
import { formatAddress } from '@/lib/wallet-service'; import { formatAddress } from '@/lib/wallet-service';
@@ -12,6 +12,7 @@ import { formatAddress } from '@/lib/wallet-service';
interface Props { interface Props {
address: string; address: string;
identityHash: string; identityHash: string;
hasReferrer: boolean;
onOpenApp: () => void; onOpenApp: () => void;
} }
@@ -34,9 +35,26 @@ export function CitizenSuccess({ address, identityHash, onOpenApp }: Props) {
{/* Title */} {/* Title */}
<div className="text-center space-y-2"> <div className="text-center space-y-2">
<h1 className="text-2xl font-bold">{t('citizen.applicationSubmitted')}</h1> <h1 className="text-2xl font-bold">{t('citizen.applicationSubmitted')}</h1>
<div className="flex items-center justify-center gap-2 text-yellow-500"> </div>
<Clock className="w-4 h-4" />
<p className="text-sm">{t('citizen.pendingReferral')}</p> {/* 3-Step Process */}
<div className="w-full max-w-sm space-y-3">
{/* Step 1 - Completed */}
<div className="flex items-start gap-3 p-3 bg-green-500/10 border border-green-500/30 rounded-xl">
<CheckCircle className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-green-400">{t('citizen.stepApplicationSent')}</p>
</div>
{/* Step 2 - Pending */}
<div className="flex items-start gap-3 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-xl">
<Clock className="w-5 h-5 text-yellow-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-yellow-400">{t('citizen.stepReferrerApproval')}</p>
</div>
{/* Step 3 - Future */}
<div className="flex items-start gap-3 p-3 bg-muted/50 border border-border rounded-xl">
<ArrowRight className="w-5 h-5 text-muted-foreground flex-shrink-0 mt-0.5" />
<p className="text-sm text-muted-foreground">{t('citizen.stepConfirm')}</p>
</div> </div>
</div> </div>
@@ -52,9 +70,9 @@ export function CitizenSuccess({ address, identityHash, onOpenApp }: Props) {
</div> </div>
</div> </div>
{/* Info Note */} {/* Next Steps Info */}
<p className="text-xs text-muted-foreground text-center max-w-sm"> <p className="text-xs text-muted-foreground text-center max-w-sm">
{t('citizen.applicationInfo')} {t('citizen.nextStepsInfo')}
</p> </p>
{/* Open App Button */} {/* Open App Button */}
+10
View File
@@ -623,6 +623,16 @@ const ar: Translations = {
applicationInfo: 'بعد موافقة المُحيل، يمكنك التأكيد', applicationInfo: 'بعد موافقة المُحيل، يمكنك التأكيد',
depositRequired: '١ HEZ وديعة مطلوبة', depositRequired: '١ HEZ وديعة مطلوبة',
walletAddress: 'عنوان المحفظة', walletAddress: 'عنوان المحفظة',
privacyNotice:
'لا يتم إرسال معلوماتك الشخصية إلى أي مكان. يتم فقط إنشاء رمز هاش وتسجيله على البلوكشين.',
seedPhrase: 'Seed Phrase',
seedPhrasePlaceholder: 'الصق seed phrase المكون من ١٢ أو ٢٤ كلمة هنا...',
invalidSeedPhrase: 'Seed phrase غير صالح. يرجى إدخال ١٢ أو ٢٤ كلمة صالحة.',
connectingChain: 'جاري الاتصال بـ People Chain...',
stepApplicationSent: 'تم إرسال الطلب (مكتمل)',
stepReferrerApproval: 'في انتظار موافقة المُحيل',
stepConfirm: 'بعد الموافقة، قم بالتأكيد من محفظتك',
nextStepsInfo: 'تابع الخطوات التالية من محفظتك',
fillAllFields: 'يرجى ملء جميع الحقول', fillAllFields: 'يرجى ملء جميع الحقول',
acceptConsent: 'يرجى تحديد مربع الموافقة', acceptConsent: 'يرجى تحديد مربع الموافقة',
walletNotConnected: 'المحفظة غير متصلة', walletNotConnected: 'المحفظة غير متصلة',
+10
View File
@@ -625,6 +625,16 @@ const ckb: Translations = {
applicationInfo: 'کاتێک ڕیفێڕەر پەسەند بکات، دەتوانیت confirm بکەیت', applicationInfo: 'کاتێک ڕیفێڕەر پەسەند بکات، دەتوانیت confirm بکەیت',
depositRequired: '١ HEZ ئەمانەت پێویستە', depositRequired: '١ HEZ ئەمانەت پێویستە',
walletAddress: 'ناونیشانی جزدان', walletAddress: 'ناونیشانی جزدان',
privacyNotice:
'زانیارییە کەسییەکانت بۆ هیچ شوێنێک نانێردرێت. تەنها کۆدێکی هاش دروست دەکرێت و لە بلۆکچەین تۆمار دەکرێت.',
seedPhrase: 'Seed Phrase',
seedPhrasePlaceholder: 'Seed phrase ی ١٢ یان ٢٤ وشەییت لێرە بلکێنە...',
invalidSeedPhrase: 'Seed phrase نادروستە. تکایە ١٢ یان ٢٤ وشەی دروست بنووسە.',
connectingChain: 'پەیوەندیکردن بە People Chain...',
stepApplicationSent: 'داواکاری نێردرا (تەواو)',
stepReferrerApproval: 'چاوەڕوانی پەسەندکردنی ڕیفێڕەر',
stepConfirm: 'دوای پەسەندکردن، لە جزدانەکەت confirm بکە',
nextStepsInfo: 'هەنگاوە داهاتووەکان لە جزدانەکەت بەدواداگرە',
fillAllFields: 'تکایە هەموو خانەکان پڕ بکەرەوە', fillAllFields: 'تکایە هەموو خانەکان پڕ بکەرەوە',
acceptConsent: 'تکایە خانەی ڕەزامەندی نیشان بدە', acceptConsent: 'تکایە خانەی ڕەزامەندی نیشان بدە',
walletNotConnected: 'جزدان پەیوەندی نییە', walletNotConnected: 'جزدان پەیوەندی نییە',
+10
View File
@@ -624,6 +624,16 @@ const en: Translations = {
applicationInfo: 'Once your referrer approves, you can confirm', applicationInfo: 'Once your referrer approves, you can confirm',
depositRequired: '1 HEZ deposit required', depositRequired: '1 HEZ deposit required',
walletAddress: 'Wallet Address', walletAddress: 'Wallet Address',
privacyNotice:
'Your personal information is never sent anywhere. Only a hash code is generated and recorded on the blockchain.',
seedPhrase: 'Seed Phrase',
seedPhrasePlaceholder: 'Paste your 12 or 24 word seed phrase here...',
invalidSeedPhrase: 'Invalid seed phrase. Please enter 12 or 24 valid words.',
connectingChain: 'Connecting to People Chain...',
stepApplicationSent: 'Application submitted (completed)',
stepReferrerApproval: 'Waiting for referrer approval',
stepConfirm: 'After approval, confirm from your wallet',
nextStepsInfo: 'Follow the next steps from your wallet',
fillAllFields: 'Please fill in all fields', fillAllFields: 'Please fill in all fields',
acceptConsent: 'Please accept the consent checkbox', acceptConsent: 'Please accept the consent checkbox',
walletNotConnected: 'Wallet not connected', walletNotConnected: 'Wallet not connected',
+10
View File
@@ -624,6 +624,16 @@ const fa: Translations = {
applicationInfo: 'پس از تأیید معرف، می‌توانید تأیید نهایی کنید', applicationInfo: 'پس از تأیید معرف، می‌توانید تأیید نهایی کنید',
depositRequired: '۱ HEZ سپرده لازم است', depositRequired: '۱ HEZ سپرده لازم است',
walletAddress: 'آدرس کیف پول', walletAddress: 'آدرس کیف پول',
privacyNotice:
'اطلاعات شخصی شما به هیچ جایی ارسال نمی‌شود. فقط یک کد هش تولید و در بلاکچین ثبت می‌شود.',
seedPhrase: 'Seed Phrase',
seedPhrasePlaceholder: 'seed phrase ۱۲ یا ۲۴ کلمه‌ای خود را اینجا بچسبانید...',
invalidSeedPhrase: 'Seed phrase نامعتبر. لطفاً ۱۲ یا ۲۴ کلمه معتبر وارد کنید.',
connectingChain: 'در حال اتصال به People Chain...',
stepApplicationSent: 'درخواست ارسال شد (تکمیل شده)',
stepReferrerApproval: 'در انتظار تأیید معرف',
stepConfirm: 'پس از تأیید، از کیف پول خود confirm کنید',
nextStepsInfo: 'مراحل بعدی را از کیف پول خود پیگیری کنید',
fillAllFields: 'لطفاً همه فیلدها را پر کنید', fillAllFields: 'لطفاً همه فیلدها را پر کنید',
acceptConsent: 'لطفاً کادر رضایت را علامت بزنید', acceptConsent: 'لطفاً کادر رضایت را علامت بزنید',
walletNotConnected: 'کیف پول متصل نیست', walletNotConnected: 'کیف پول متصل نیست',
+11
View File
@@ -649,6 +649,17 @@ const krd: Translations = {
applicationInfo: 'Dema referrer pejirîne, hûn dikarin confirm bikin', applicationInfo: 'Dema referrer pejirîne, hûn dikarin confirm bikin',
depositRequired: '1 HEZ depozîto pêwîst e', depositRequired: '1 HEZ depozîto pêwîst e',
walletAddress: 'Navnîşana Cûzdan', walletAddress: 'Navnîşana Cûzdan',
privacyNotice:
'Agahiyên te yên kesane tu cih nayên şandin. Tenê kodek hash tê çêkirin û li ser blockchain tê tomarkirin.',
seedPhrase: 'Seed Phrase',
seedPhrasePlaceholder: 'Seed phrase ya xwe ya 12 an 24 peyvan li vir bileqîne...',
invalidSeedPhrase:
'Seed phrase ne derbasdar e. Ji kerema xwe 12 an 24 peyvên derbasdar binivîse.',
connectingChain: 'People Chain tê girêdan...',
stepApplicationSent: 'Serlêdan hat şandin (temam)',
stepReferrerApproval: 'Li benda pejirandina referrer',
stepConfirm: 'Piştî pejirandinê, ji cûzdana xwe confirm bike',
nextStepsInfo: 'Gavên pêş ji cûzdana xwe bişopîne',
fillAllFields: 'Ji kerema xwe hemû qadan tije bike', fillAllFields: 'Ji kerema xwe hemû qadan tije bike',
acceptConsent: 'Ji kerema xwe qutiya pejirandinê nîşan bide', acceptConsent: 'Ji kerema xwe qutiya pejirandinê nîşan bide',
walletNotConnected: 'Cûzdan girêdayî nîne', walletNotConnected: 'Cûzdan girêdayî nîne',
+10
View File
@@ -625,6 +625,16 @@ const tr: Translations = {
applicationInfo: 'Referrer onayladıktan sonra confirm edebilirsiniz', applicationInfo: 'Referrer onayladıktan sonra confirm edebilirsiniz',
depositRequired: '1 HEZ depozito gerekli', depositRequired: '1 HEZ depozito gerekli',
walletAddress: 'Cüzdan Adresi', walletAddress: 'Cüzdan Adresi',
privacyNotice:
"Kişisel bilgileriniz hiçbir yere gönderilmez. Yalnızca bir hash kodu oluşturulup blockchain'e kaydedilir.",
seedPhrase: 'Seed Phrase',
seedPhrasePlaceholder: "12 veya 24 kelimelik seed phrase'inizi buraya yapıştırın...",
invalidSeedPhrase: 'Geçersiz seed phrase. Lütfen 12 veya 24 geçerli kelime girin.',
connectingChain: "People Chain'e bağlanılıyor...",
stepApplicationSent: 'Başvuru gönderildi (tamamlandı)',
stepReferrerApproval: 'Referrer onayı bekleniyor',
stepConfirm: 'Onaydan sonra cüzdanınızdan confirm yapılacak',
nextStepsInfo: 'Sonraki adımları cüzdanınızdan takip edin',
fillAllFields: 'Lütfen tüm alanları doldurun', fillAllFields: 'Lütfen tüm alanları doldurun',
acceptConsent: 'Lütfen onay kutusunu işaretleyin', acceptConsent: 'Lütfen onay kutusunu işaretleyin',
walletNotConnected: 'Cüzdan bağlı değil', walletNotConnected: 'Cüzdan bağlı değil',
+12
View File
@@ -635,6 +635,18 @@ export interface Translations {
walletAddress: string; walletAddress: string;
applicationInfo: string; applicationInfo: string;
depositRequired: string; depositRequired: string;
// Privacy & Seed phrase
privacyNotice: string;
seedPhrase: string;
seedPhrasePlaceholder: string;
invalidSeedPhrase: string;
// Processing
connectingChain: string;
// Success - next steps
stepApplicationSent: string;
stepReferrerApproval: string;
stepConfirm: string;
nextStepsInfo: string;
// Errors // Errors
fillAllFields: string; fillAllFields: string;
acceptConsent: string; acceptConsent: string;
+1
View File
@@ -35,6 +35,7 @@ export interface CitizenshipData {
profession: string; profession: string;
referrerAddress?: string; referrerAddress?: string;
walletAddress: string; walletAddress: string;
seedPhrase: string;
timestamp: number; timestamp: number;
} }
+12 -70
View File
@@ -1,30 +1,17 @@
/** /**
* Citizen Page * Citizen Page
* Standalone page for citizenship application flow * Standalone page for citizenship application flow
* Accessed via ?page=citizen URL parameter * Accessed via ?page=citizen or /citizens URL
* No bottom navigation bar * No bottom navigation bar, no wallet steps - direct form
*/ */
import { useState, useCallback, lazy, Suspense } from 'react'; import { useState, useCallback, lazy, Suspense } from 'react';
import { Globe, Loader2 } from 'lucide-react'; import { Globe, Loader2 } from 'lucide-react';
import { useWallet } from '@/contexts/WalletContext';
import { useTranslation, LANGUAGE_NAMES, VALID_LANGS } from '@/i18n'; import { useTranslation, LANGUAGE_NAMES, VALID_LANGS } from '@/i18n';
import type { LanguageCode } from '@/i18n'; import type { LanguageCode } from '@/i18n';
import type { CitizenshipData } from '@/lib/citizenship'; import type { CitizenshipData } from '@/lib/citizenship';
// Lazy load sub-components // 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(() => const CitizenForm = lazy(() =>
import('@/components/citizen/CitizenForm').then((m) => ({ default: m.CitizenForm })) import('@/components/citizen/CitizenForm').then((m) => ({ default: m.CitizenForm }))
); );
@@ -35,14 +22,7 @@ const CitizenSuccess = lazy(() =>
import('@/components/citizen/CitizenSuccess').then((m) => ({ default: m.CitizenSuccess })) import('@/components/citizen/CitizenSuccess').then((m) => ({ default: m.CitizenSuccess }))
); );
type Step = type Step = 'form' | 'processing' | 'success';
| 'wallet-setup'
| 'wallet-create'
| 'wallet-import'
| 'wallet-connect'
| 'form'
| 'processing'
| 'success';
function SectionLoader() { function SectionLoader() {
return ( return (
@@ -53,32 +33,13 @@ function SectionLoader() {
} }
export function CitizenPage() { export function CitizenPage() {
const { hasWallet, isConnected, address, deleteWalletData } = useWallet();
const { t, lang, setLang } = useTranslation(); const { t, lang, setLang } = useTranslation();
const [showLangMenu, setShowLangMenu] = useState(false); const [showLangMenu, setShowLangMenu] = useState(false);
const [citizenshipData, setCitizenshipData] = useState<CitizenshipData | null>(null); const [citizenshipData, setCitizenshipData] = useState<CitizenshipData | null>(null);
const [identityHash, setIdentityHash] = useState<string>(''); const [identityHash, setIdentityHash] = useState<string>('');
const [walletAddress, setWalletAddress] = useState<string>('');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [step, setStep] = useState<Step>('form');
// 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 // Form submission
const handleFormSubmit = useCallback((data: CitizenshipData) => { const handleFormSubmit = useCallback((data: CitizenshipData) => {
@@ -88,8 +49,9 @@ export function CitizenPage() {
}, []); }, []);
// Processing result // Processing result
const handleSuccess = useCallback((hash: string) => { const handleSuccess = useCallback((hash: string, address: string) => {
setIdentityHash(hash); setIdentityHash(hash);
setWalletAddress(address);
setStep('success'); setStep('success');
}, []); }, []);
@@ -100,9 +62,9 @@ export function CitizenPage() {
// Open main app // Open main app
const handleOpenApp = useCallback(() => { const handleOpenApp = useCallback(() => {
// Remove ?page=citizen and navigate to main app
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.delete('page'); url.searchParams.delete('page');
url.pathname = '/';
window.location.href = url.toString(); window.location.href = url.toString();
}, []); }, []);
@@ -156,28 +118,7 @@ export function CitizenPage() {
{/* Content */} {/* Content */}
<main className="flex-1 overflow-y-auto"> <main className="flex-1 overflow-y-auto">
<Suspense fallback={<SectionLoader />}> <Suspense fallback={<SectionLoader />}>
{step === 'wallet-setup' && ( {step === 'form' && <CitizenForm onSubmit={handleFormSubmit} />}
<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 && ( {step === 'processing' && citizenshipData && (
<CitizenProcessing <CitizenProcessing
@@ -187,10 +128,11 @@ export function CitizenPage() {
/> />
)} )}
{step === 'success' && address && ( {step === 'success' && (
<CitizenSuccess <CitizenSuccess
address={address} address={walletAddress}
identityHash={identityHash} identityHash={identityHash}
hasReferrer={!!citizenshipData?.referrerAddress}
onOpenApp={handleOpenApp} onOpenApp={handleOpenApp}
/> />
)} )}
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"version": "1.0.194", "version": "1.0.196",
"buildTime": "2026-02-14T19:00:32.936Z", "buildTime": "2026-02-14T20:24:59.270Z",
"buildNumber": 1771095632937 "buildNumber": 1771100699271
} }
+1 -1
View File
@@ -174,7 +174,7 @@ async function sendKrdWelcome(token: string, chatId: number) {
[ [
{ {
text: '🏛️ Be Citizen / Bibe Welatî', text: '🏛️ Be Citizen / Bibe Welatî',
web_app: { url: `${appUrl}?page=citizen` }, web_app: { url: `${appUrl}/citizens` },
}, },
], ],
], ],