mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-04-22 03:07:55 +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
-1
@@ -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
@@ -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 />
|
||||||
|
|||||||
@@ -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')}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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: 'المحفظة غير متصلة',
|
||||||
|
|||||||
@@ -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: 'جزدان پەیوەندی نییە',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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: 'کیف پول متصل نیست',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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` },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user