mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-06-15 20:51:10 +00:00
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:
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user