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",
"version": "1.0.194",
"version": "1.0.196",
"type": "module",
"description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards",
"author": "Pezkuwichain Team",
+3 -2
View File
@@ -54,11 +54,12 @@ 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)
// 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 IS_CITIZEN_PAGE = PAGE_PARAM === 'citizen' || window.location.pathname === '/citizens';
export default function App() {
if (PAGE_PARAM === 'citizen') {
if (IS_CITIZEN_PAGE) {
return (
<Suspense fallback={<SectionLoader />}>
<CitizenPage />
+63 -6
View File
@@ -1,16 +1,16 @@
/**
* Citizen Application Form
* Collects citizenship data from the user
* Collects citizenship data and seed phrase from the user
*/
import { useState } from 'react';
import { Plus, Trash2 } from 'lucide-react';
import { useState, useEffect, useMemo } from 'react';
import { Plus, Trash2, Shield } from 'lucide-react';
import { useTranslation } from '@/i18n';
import { useTelegram } from '@/hooks/useTelegram';
import { initWalletService, validateMnemonic, getAddressFromMnemonic } from '@/lib/wallet-service';
import type { CitizenshipData, Region, MaritalStatus, ChildInfo } from '@/lib/citizenship';
interface Props {
walletAddress: string;
onSubmit: (data: CitizenshipData) => void;
}
@@ -23,7 +23,7 @@ const REGIONS: { value: Region; labelKey: string }[] = [
{ value: 'diaspora', labelKey: 'citizen.regionDiaspora' },
];
export function CitizenForm({ walletAddress, onSubmit }: Props) {
export function CitizenForm({ onSubmit }: Props) {
const { t } = useTranslation();
const { hapticImpact, hapticNotification } = useTelegram();
@@ -39,8 +39,33 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
const [email, setEmail] = useState('');
const [profession, setProfession] = useState('');
const [referrerAddress, setReferrerAddress] = useState('');
const [seedPhrase, setSeedPhrase] = useState('');
const [consent, setConsent] = useState(false);
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) => {
hapticImpact('light');
@@ -100,6 +125,14 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
return;
}
// Validate seed phrase
const trimmedSeed = seedPhrase.trim();
if (!trimmedSeed || !cryptoReady || !validateMnemonic(trimmedSeed)) {
setError(t('citizen.invalidSeedPhrase'));
hapticNotification('error');
return;
}
if (!consent) {
setError(t('citizen.acceptConsent'));
hapticNotification('error');
@@ -108,6 +141,9 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
hapticImpact('medium');
// Derive wallet address from seed phrase
const walletAddress = getAddressFromMnemonic(trimmedSeed);
const data: CitizenshipData = {
fullName,
fatherName,
@@ -122,17 +158,25 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
profession,
referrerAddress: referrerAddress || undefined,
walletAddress,
seedPhrase: trimmedSeed,
timestamp: Date.now(),
};
onSubmit(data);
};
const isSeedValid = cryptoReady && seedPhrase.trim() && !seedPhraseError;
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">
{/* 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 */}
<div>
<label className={labelClass}>{t('citizen.fullName')}</label>
@@ -319,6 +363,19 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
/>
</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 */}
<div>
<label className={labelClass}>{t('citizen.referrerAddress')}</label>
@@ -352,7 +409,7 @@ export function CitizenForm({ walletAddress, onSubmit }: Props) {
{/* Submit Button */}
<button
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"
>
{t('citizen.submit')}
+39 -24
View File
@@ -1,14 +1,15 @@
/**
* Citizen Processing Component
* 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 { useTranslation } from '@/i18n';
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 {
calculateIdentityHash,
@@ -19,24 +20,31 @@ import {
interface Props {
citizenshipData: CitizenshipData;
onSuccess: (identityHash: string, blockHash?: string) => void;
onSuccess: (identityHash: string, walletAddress: string) => void;
onError: (error: string) => void;
}
type ProcessingState = 'preparing' | 'ready' | 'signing';
type ProcessingState = 'preparing' | 'connecting' | '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 [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(() => {
let cancelled = false;
const prepare = async () => {
try {
// Init crypto
await initWalletService();
// Mock IPFS upload
const ipfsCid = await uploadToIPFS(citizenshipData);
@@ -44,26 +52,35 @@ export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props
const hash = calculateIdentityHash(citizenshipData.fullName, citizenshipData.email, [
ipfsCid,
]);
if (cancelled) return;
setIdentityHash(hash);
// Save encrypted data locally
saveCitizenshipLocally(citizenshipData);
// Small delay to show animation
await new Promise((resolve) => setTimeout(resolve, 1500));
// Connect to People Chain
setState('connecting');
const peopleApi = await initPeopleConnection();
if (cancelled) return;
peopleApiRef.current = peopleApi;
setState('ready');
hapticNotification('success');
} catch (err) {
onError(err instanceof Error ? err.message : 'Preparation failed');
if (!cancelled) {
onError(err instanceof Error ? err.message : 'Preparation failed');
}
}
};
prepare();
return () => {
cancelled = true;
};
}, [citizenshipData, hapticNotification, onError]);
const handleSign = useCallback(async () => {
if (!peopleApi || !keypair) {
if (!peopleApiRef.current || !seedPhraseRef.current) {
onError(t('citizen.walletNotConnected'));
return;
}
@@ -72,35 +89,32 @@ export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props
hapticImpact('medium');
try {
const keypair = createKeypair(seedPhraseRef.current);
const result = await applyCitizenship(
peopleApi,
peopleApiRef.current,
keypair,
identityHash,
citizenshipData.referrerAddress || null
);
// Clear seed phrase from memory
seedPhraseRef.current = '';
if (result.success) {
hapticNotification('success');
onSuccess(identityHash, result.blockHash);
onSuccess(identityHash, citizenshipData.walletAddress);
} else {
hapticNotification('error');
onError(result.error || t('citizen.submissionFailed'));
}
} catch (err) {
// Clear seed phrase from memory
seedPhraseRef.current = '';
hapticNotification('error');
onError(err instanceof Error ? err.message : t('citizen.submissionFailed'));
}
}, [
peopleApi,
keypair,
citizenshipData,
identityHash,
hapticImpact,
hapticNotification,
onSuccess,
onError,
t,
]);
}, [citizenshipData, identityHash, hapticImpact, hapticNotification, onSuccess, onError, t]);
const isReady = state === 'ready';
const isSigning = state === 'signing';
@@ -116,6 +130,7 @@ export function CitizenProcessing({ citizenshipData, onSuccess, onError }: Props
<div className="text-center space-y-2">
<p className="text-lg font-medium">
{state === 'preparing' && t('citizen.preparingData')}
{state === 'connecting' && t('citizen.connectingChain')}
{state === 'ready' && t('citizen.readyToSign')}
{state === 'signing' && t('citizen.signingTx')}
</p>
+25 -7
View File
@@ -1,10 +1,10 @@
/**
* Citizen Success Screen
* 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 { useTelegram } from '@/hooks/useTelegram';
import { formatAddress } from '@/lib/wallet-service';
@@ -12,6 +12,7 @@ import { formatAddress } from '@/lib/wallet-service';
interface Props {
address: string;
identityHash: string;
hasReferrer: boolean;
onOpenApp: () => void;
}
@@ -34,9 +35,26 @@ export function CitizenSuccess({ address, identityHash, onOpenApp }: Props) {
{/* Title */}
<div className="text-center space-y-2">
<h1 className="text-2xl font-bold">{t('citizen.applicationSubmitted')}</h1>
<div className="flex items-center justify-center gap-2 text-yellow-500">
<Clock className="w-4 h-4" />
<p className="text-sm">{t('citizen.pendingReferral')}</p>
</div>
{/* 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>
@@ -52,9 +70,9 @@ export function CitizenSuccess({ address, identityHash, onOpenApp }: Props) {
</div>
</div>
{/* Info Note */}
{/* Next Steps Info */}
<p className="text-xs text-muted-foreground text-center max-w-sm">
{t('citizen.applicationInfo')}
{t('citizen.nextStepsInfo')}
</p>
{/* Open App Button */}
+10
View File
@@ -623,6 +623,16 @@ const ar: Translations = {
applicationInfo: 'بعد موافقة المُحيل، يمكنك التأكيد',
depositRequired: '١ HEZ وديعة مطلوبة',
walletAddress: 'عنوان المحفظة',
privacyNotice:
'لا يتم إرسال معلوماتك الشخصية إلى أي مكان. يتم فقط إنشاء رمز هاش وتسجيله على البلوكشين.',
seedPhrase: 'Seed Phrase',
seedPhrasePlaceholder: 'الصق seed phrase المكون من ١٢ أو ٢٤ كلمة هنا...',
invalidSeedPhrase: 'Seed phrase غير صالح. يرجى إدخال ١٢ أو ٢٤ كلمة صالحة.',
connectingChain: 'جاري الاتصال بـ People Chain...',
stepApplicationSent: 'تم إرسال الطلب (مكتمل)',
stepReferrerApproval: 'في انتظار موافقة المُحيل',
stepConfirm: 'بعد الموافقة، قم بالتأكيد من محفظتك',
nextStepsInfo: 'تابع الخطوات التالية من محفظتك',
fillAllFields: 'يرجى ملء جميع الحقول',
acceptConsent: 'يرجى تحديد مربع الموافقة',
walletNotConnected: 'المحفظة غير متصلة',
+10
View File
@@ -625,6 +625,16 @@ const ckb: Translations = {
applicationInfo: 'کاتێک ڕیفێڕەر پەسەند بکات، دەتوانیت confirm بکەیت',
depositRequired: '١ HEZ ئەمانەت پێویستە',
walletAddress: 'ناونیشانی جزدان',
privacyNotice:
'زانیارییە کەسییەکانت بۆ هیچ شوێنێک نانێردرێت. تەنها کۆدێکی هاش دروست دەکرێت و لە بلۆکچەین تۆمار دەکرێت.',
seedPhrase: 'Seed Phrase',
seedPhrasePlaceholder: 'Seed phrase ی ١٢ یان ٢٤ وشەییت لێرە بلکێنە...',
invalidSeedPhrase: 'Seed phrase نادروستە. تکایە ١٢ یان ٢٤ وشەی دروست بنووسە.',
connectingChain: 'پەیوەندیکردن بە People Chain...',
stepApplicationSent: 'داواکاری نێردرا (تەواو)',
stepReferrerApproval: 'چاوەڕوانی پەسەندکردنی ڕیفێڕەر',
stepConfirm: 'دوای پەسەندکردن، لە جزدانەکەت confirm بکە',
nextStepsInfo: 'هەنگاوە داهاتووەکان لە جزدانەکەت بەدواداگرە',
fillAllFields: 'تکایە هەموو خانەکان پڕ بکەرەوە',
acceptConsent: 'تکایە خانەی ڕەزامەندی نیشان بدە',
walletNotConnected: 'جزدان پەیوەندی نییە',
+10
View File
@@ -624,6 +624,16 @@ const en: Translations = {
applicationInfo: 'Once your referrer approves, you can confirm',
depositRequired: '1 HEZ deposit required',
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',
acceptConsent: 'Please accept the consent checkbox',
walletNotConnected: 'Wallet not connected',
+10
View File
@@ -624,6 +624,16 @@ const fa: Translations = {
applicationInfo: 'پس از تأیید معرف، می‌توانید تأیید نهایی کنید',
depositRequired: '۱ HEZ سپرده لازم است',
walletAddress: 'آدرس کیف پول',
privacyNotice:
'اطلاعات شخصی شما به هیچ جایی ارسال نمی‌شود. فقط یک کد هش تولید و در بلاکچین ثبت می‌شود.',
seedPhrase: 'Seed Phrase',
seedPhrasePlaceholder: 'seed phrase ۱۲ یا ۲۴ کلمه‌ای خود را اینجا بچسبانید...',
invalidSeedPhrase: 'Seed phrase نامعتبر. لطفاً ۱۲ یا ۲۴ کلمه معتبر وارد کنید.',
connectingChain: 'در حال اتصال به People Chain...',
stepApplicationSent: 'درخواست ارسال شد (تکمیل شده)',
stepReferrerApproval: 'در انتظار تأیید معرف',
stepConfirm: 'پس از تأیید، از کیف پول خود confirm کنید',
nextStepsInfo: 'مراحل بعدی را از کیف پول خود پیگیری کنید',
fillAllFields: 'لطفاً همه فیلدها را پر کنید',
acceptConsent: 'لطفاً کادر رضایت را علامت بزنید',
walletNotConnected: 'کیف پول متصل نیست',
+11
View File
@@ -649,6 +649,17 @@ const krd: Translations = {
applicationInfo: 'Dema referrer pejirîne, hûn dikarin confirm bikin',
depositRequired: '1 HEZ depozîto pêwîst e',
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',
acceptConsent: 'Ji kerema xwe qutiya pejirandinê nîşan bide',
walletNotConnected: 'Cûzdan girêdayî nîne',
+10
View File
@@ -625,6 +625,16 @@ const tr: Translations = {
applicationInfo: 'Referrer onayladıktan sonra confirm edebilirsiniz',
depositRequired: '1 HEZ depozito gerekli',
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',
acceptConsent: 'Lütfen onay kutusunu işaretleyin',
walletNotConnected: 'Cüzdan bağlı değil',
+12
View File
@@ -635,6 +635,18 @@ export interface Translations {
walletAddress: string;
applicationInfo: 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
fillAllFields: string;
acceptConsent: string;
+1
View File
@@ -35,6 +35,7 @@ export interface CitizenshipData {
profession: string;
referrerAddress?: string;
walletAddress: string;
seedPhrase: string;
timestamp: number;
}
+12 -70
View File
@@ -1,30 +1,17 @@
/**
* Citizen Page
* Standalone page for citizenship application flow
* Accessed via ?page=citizen URL parameter
* No bottom navigation bar
* Accessed via ?page=citizen or /citizens URL
* No bottom navigation bar, no wallet steps - direct form
*/
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 }))
);
@@ -35,14 +22,7 @@ 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';
type Step = 'form' | 'processing' | 'success';
function SectionLoader() {
return (
@@ -53,32 +33,13 @@ function SectionLoader() {
}
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 [identityHash, setIdentityHash] = useState<string>('');
const [walletAddress, setWalletAddress] = useState<string>('');
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]);
const [step, setStep] = useState<Step>('form');
// Form submission
const handleFormSubmit = useCallback((data: CitizenshipData) => {
@@ -88,8 +49,9 @@ export function CitizenPage() {
}, []);
// Processing result
const handleSuccess = useCallback((hash: string) => {
const handleSuccess = useCallback((hash: string, address: string) => {
setIdentityHash(hash);
setWalletAddress(address);
setStep('success');
}, []);
@@ -100,9 +62,9 @@ export function CitizenPage() {
// 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');
url.pathname = '/';
window.location.href = url.toString();
}, []);
@@ -156,28 +118,7 @@ export function CitizenPage() {
{/* 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 === 'form' && <CitizenForm onSubmit={handleFormSubmit} />}
{step === 'processing' && citizenshipData && (
<CitizenProcessing
@@ -187,10 +128,11 @@ export function CitizenPage() {
/>
)}
{step === 'success' && address && (
{step === 'success' && (
<CitizenSuccess
address={address}
address={walletAddress}
identityHash={identityHash}
hasReferrer={!!citizenshipData?.referrerAddress}
onOpenApp={handleOpenApp}
/>
)}
+3 -3
View File
@@ -1,5 +1,5 @@
{
"version": "1.0.194",
"buildTime": "2026-02-14T19:00:32.936Z",
"buildNumber": 1771095632937
"version": "1.0.196",
"buildTime": "2026-02-14T20:24:59.270Z",
"buildNumber": 1771100699271
}
+1 -1
View File
@@ -174,7 +174,7 @@ async function sendKrdWelcome(token: string, chatId: number) {
[
{
text: '🏛️ Be Citizen / Bibe Welatî',
web_app: { url: `${appUrl}?page=citizen` },
web_app: { url: `${appUrl}/citizens` },
},
],
],