refactor(mobile): Remove i18n, expand core screens, update plan

BREAKING: Removed multi-language support (i18n) - will be re-added later

Changes:
- Removed i18n system (6 language files, LanguageContext)
- Expanded WalletScreen, SettingsScreen, SwapScreen with more features
- Added KurdistanSun component, HEZ/PEZ token icons
- Added EditProfileScreen, WalletSetupScreen
- Added button e2e tests (Profile, Settings, Wallet)
- Updated plan: honest assessment - 42 nav buttons with mock data
- Fixed terminology: Polkadot→Pezkuwi, Substrate→Bizinikiwi

Reality check: UI complete with mock data, converting to production one-by-one
This commit is contained in:
2026-01-15 05:08:21 +03:00
parent 5d293cc954
commit f2e70a8150
110 changed files with 11157 additions and 3260 deletions
+820
View File
@@ -0,0 +1,820 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
SafeAreaView,
ScrollView,
TextInput,
ActivityIndicator,
Alert,
Platform,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { LinearGradient } from 'expo-linear-gradient';
import { KurdistanColors } from '../theme/colors';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { mnemonicGenerate, mnemonicValidate } from '@pezkuwi/util-crypto';
// Cross-platform alert helper
const showAlert = (title: string, message: string, buttons?: Array<{text: string; onPress?: () => void; style?: string}>) => {
if (Platform.OS === 'web') {
window.alert(`${title}\n\n${message}`);
if (buttons?.[0]?.onPress) buttons[0].onPress();
} else {
Alert.alert(title, message, buttons as any);
}
};
type SetupStep = 'choice' | 'create-show' | 'create-verify' | 'import' | 'wallet-name' | 'success';
const WalletSetupScreen: React.FC = () => {
const navigation = useNavigation<any>();
const { createWallet, importWallet, connectWallet, isReady } = usePezkuwi();
const [step, setStep] = useState<SetupStep>('choice');
const [mnemonic, setMnemonic] = useState<string[]>([]);
const [walletName, setWalletName] = useState('');
const [importMnemonic, setImportMnemonic] = useState('');
const [verificationIndices, setVerificationIndices] = useState<number[]>([]);
const [selectedWords, setSelectedWords] = useState<{[key: number]: string}>({});
const [isLoading, setIsLoading] = useState(false);
const [createdAddress, setCreatedAddress] = useState('');
const [isCreateFlow, setIsCreateFlow] = useState(true);
// Generate mnemonic when entering create flow
const handleCreateNew = () => {
const generatedMnemonic = mnemonicGenerate(12);
setMnemonic(generatedMnemonic.split(' '));
setIsCreateFlow(true);
setStep('create-show');
};
// Go to import flow
const handleImport = () => {
setIsCreateFlow(false);
setStep('import');
};
// After showing mnemonic, go to verification
const handleMnemonicConfirmed = () => {
// Select 3 random indices for verification
const indices: number[] = [];
while (indices.length < 3) {
const randomIndex = Math.floor(Math.random() * 12);
if (!indices.includes(randomIndex)) {
indices.push(randomIndex);
}
}
indices.sort((a, b) => a - b);
setVerificationIndices(indices);
setSelectedWords({});
setStep('create-verify');
};
// Verify selected words
const handleVerifyWord = (index: number, word: string) => {
setSelectedWords(prev => ({ ...prev, [index]: word }));
};
// Check if verification is complete and correct
const isVerificationComplete = () => {
return verificationIndices.every(idx => selectedWords[idx] === mnemonic[idx]);
};
// After verification, go to wallet name
const handleVerificationComplete = () => {
if (!isVerificationComplete()) {
showAlert('Incorrect', 'The words you selected do not match. Please try again.');
setSelectedWords({});
return;
}
setStep('wallet-name');
};
// Create wallet with name
const handleCreateWallet = async () => {
if (!walletName.trim()) {
showAlert('Error', 'Please enter a wallet name');
return;
}
if (!isReady) {
showAlert('Error', 'Crypto libraries are still loading. Please wait a moment and try again.');
return;
}
setIsLoading(true);
try {
const { address } = await createWallet(walletName.trim(), mnemonic.join(' '));
setCreatedAddress(address);
await connectWallet();
setStep('success');
} catch (error: any) {
console.error('[WalletSetup] Create wallet error:', error);
showAlert('Error', error.message || 'Failed to create wallet');
} finally {
setIsLoading(false);
}
};
// Import wallet with mnemonic or dev URI (like //Alice)
const handleImportWallet = async () => {
const trimmedInput = importMnemonic.trim();
// Check if it's a dev URI (starts with //)
if (trimmedInput.startsWith('//')) {
// Dev URI like //Alice, //Bob, etc.
setMnemonic([trimmedInput]); // Store as single-element array to indicate URI
setStep('wallet-name');
return;
}
// Otherwise treat as mnemonic
const words = trimmedInput.toLowerCase().split(/\s+/);
if (words.length !== 12 && words.length !== 24) {
showAlert('Invalid Input', 'Please enter a valid 12 or 24 word recovery phrase, or a dev URI like //Alice');
return;
}
if (!mnemonicValidate(trimmedInput.toLowerCase())) {
showAlert('Invalid Mnemonic', 'The recovery phrase is invalid. Please check and try again.');
return;
}
setMnemonic(words);
setStep('wallet-name');
};
// After naming imported wallet
const handleImportComplete = async () => {
if (!walletName.trim()) {
showAlert('Error', 'Please enter a wallet name');
return;
}
if (!isReady) {
showAlert('Error', 'Crypto libraries are still loading. Please wait a moment and try again.');
return;
}
setIsLoading(true);
try {
const { address } = await importWallet(walletName.trim(), mnemonic.join(' '));
setCreatedAddress(address);
await connectWallet();
setStep('success');
} catch (error: any) {
console.error('[WalletSetup] Import wallet error:', error);
showAlert('Error', error.message || 'Failed to import wallet');
} finally {
setIsLoading(false);
}
};
// Finish setup and go to wallet
const handleFinish = () => {
navigation.replace('Wallet');
};
// Go back to previous step
const handleBack = () => {
switch (step) {
case 'create-show':
case 'import':
setStep('choice');
break;
case 'create-verify':
setStep('create-show');
break;
case 'wallet-name':
if (isCreateFlow) {
setStep('create-verify');
} else {
setStep('import');
}
break;
default:
navigation.goBack();
}
};
// Generate shuffled words for verification options
const getShuffledOptions = (correctWord: string): string[] => {
const allWords = [...mnemonic];
const options = [correctWord];
while (options.length < 4) {
const randomWord = allWords[Math.floor(Math.random() * allWords.length)];
if (!options.includes(randomWord)) {
options.push(randomWord);
}
}
// Shuffle options
return options.sort(() => Math.random() - 0.5);
};
// ============================================================
// RENDER FUNCTIONS
// ============================================================
const renderChoiceStep = () => (
<View style={styles.stepContainer} testID="wallet-setup-choice">
<View style={styles.iconContainer}>
<Text style={styles.mainIcon}>👛</Text>
</View>
<Text style={styles.title}>Set Up Your Wallet</Text>
<Text style={styles.subtitle}>
Create a new wallet or import an existing one using your recovery phrase
</Text>
<View style={styles.choiceButtons}>
<TouchableOpacity
style={styles.choiceButton}
onPress={handleCreateNew}
testID="wallet-setup-create-button"
>
<LinearGradient
colors={[KurdistanColors.kesk, '#008f43']}
style={styles.choiceButtonGradient}
>
<Text style={styles.choiceButtonIcon}></Text>
<Text style={styles.choiceButtonTitle}>Create New Wallet</Text>
<Text style={styles.choiceButtonSubtitle}>
Generate a new recovery phrase
</Text>
</LinearGradient>
</TouchableOpacity>
<TouchableOpacity
style={styles.choiceButton}
onPress={handleImport}
testID="wallet-setup-import-button"
>
<View style={styles.choiceButtonOutline}>
<Text style={styles.choiceButtonIcon}>📥</Text>
<Text style={[styles.choiceButtonTitle, { color: KurdistanColors.reş }]}>
Import Existing Wallet
</Text>
<Text style={[styles.choiceButtonSubtitle, { color: '#666' }]}>
Use your 12 or 24 word phrase
</Text>
</View>
</TouchableOpacity>
</View>
</View>
);
const renderCreateShowStep = () => (
<View style={styles.stepContainer} testID="wallet-setup-show-seed">
<Text style={styles.title}>Your Recovery Phrase</Text>
<Text style={styles.subtitle}>
Write down these 12 words in order and keep them safe. This is the only way to recover your wallet.
</Text>
<View style={styles.warningBox}>
<Text style={styles.warningIcon}></Text>
<Text style={styles.warningText}>
Never share your recovery phrase with anyone. Anyone with these words can access your funds.
</Text>
</View>
<View style={styles.mnemonicGrid} testID="mnemonic-grid">
{mnemonic.map((word, index) => (
<View key={index} style={styles.wordCard}>
<Text style={styles.wordNumber}>{index + 1}</Text>
<Text style={styles.wordText}>{word}</Text>
</View>
))}
</View>
<TouchableOpacity
style={styles.primaryButton}
onPress={handleMnemonicConfirmed}
testID="wallet-setup-continue-button"
>
<Text style={styles.primaryButtonText}>I've Written It Down</Text>
</TouchableOpacity>
</View>
);
const renderCreateVerifyStep = () => (
<View style={styles.stepContainer} testID="wallet-setup-verify-seed">
<Text style={styles.title}>Verify Your Phrase</Text>
<Text style={styles.subtitle}>
Select the correct words to verify you've saved your recovery phrase
</Text>
<View style={styles.verificationContainer}>
{verificationIndices.map((wordIndex) => (
<View key={wordIndex} style={styles.verificationItem}>
<Text style={styles.verificationLabel}>Word #{wordIndex + 1}</Text>
<View style={styles.verificationOptions}>
{getShuffledOptions(mnemonic[wordIndex]).map((option, optIdx) => (
<TouchableOpacity
key={optIdx}
style={[
styles.verificationOption,
selectedWords[wordIndex] === option && styles.verificationOptionSelected,
selectedWords[wordIndex] === option &&
selectedWords[wordIndex] === mnemonic[wordIndex] && styles.verificationOptionCorrect,
]}
onPress={() => handleVerifyWord(wordIndex, option)}
testID={`verify-option-${wordIndex}-${optIdx}`}
>
<Text style={[
styles.verificationOptionText,
selectedWords[wordIndex] === option && styles.verificationOptionTextSelected,
]}>
{option}
</Text>
</TouchableOpacity>
))}
</View>
</View>
))}
</View>
<TouchableOpacity
style={[
styles.primaryButton,
!Object.keys(selectedWords).length && styles.primaryButtonDisabled
]}
onPress={handleVerificationComplete}
disabled={Object.keys(selectedWords).length !== 3}
testID="wallet-setup-verify-button"
>
<Text style={styles.primaryButtonText}>Verify & Continue</Text>
</TouchableOpacity>
</View>
);
const renderImportStep = () => (
<View style={styles.stepContainer} testID="wallet-setup-import">
<Text style={styles.title}>Import Wallet</Text>
<Text style={styles.subtitle}>
Enter your 12 or 24 word recovery phrase, or a dev URI like //Alice
</Text>
<View style={styles.importInputContainer}>
<TextInput
style={styles.importInput}
placeholder="Enter recovery phrase or //Alice..."
placeholderTextColor="#999"
multiline
numberOfLines={4}
value={importMnemonic}
onChangeText={setImportMnemonic}
autoCapitalize="none"
autoCorrect={false}
testID="wallet-import-input"
/>
<Text style={styles.importHint}>
Mnemonic: separate words with space | Dev URI: //Alice, //Bob, etc.
</Text>
</View>
<TouchableOpacity
style={[
styles.primaryButton,
!importMnemonic.trim() && styles.primaryButtonDisabled
]}
onPress={handleImportWallet}
disabled={!importMnemonic.trim()}
testID="wallet-import-continue-button"
>
<Text style={styles.primaryButtonText}>Continue</Text>
</TouchableOpacity>
</View>
);
const renderWalletNameStep = () => (
<View style={styles.stepContainer} testID="wallet-setup-name">
<Text style={styles.title}>Name Your Wallet</Text>
<Text style={styles.subtitle}>
Give your wallet a name to easily identify it
</Text>
<View style={styles.nameInputContainer}>
<TextInput
style={styles.nameInput}
placeholder="e.g., My Main Wallet"
placeholderTextColor="#999"
value={walletName}
onChangeText={setWalletName}
autoCapitalize="words"
maxLength={30}
testID="wallet-name-input"
/>
</View>
<TouchableOpacity
style={[
styles.primaryButton,
(!walletName.trim() || isLoading) && styles.primaryButtonDisabled
]}
onPress={isCreateFlow ? handleCreateWallet : handleImportComplete}
disabled={!walletName.trim() || isLoading}
testID="wallet-setup-finish-button"
>
{isLoading ? (
<ActivityIndicator color="#FFFFFF" />
) : (
<Text style={styles.primaryButtonText}>
{isCreateFlow ? 'Create Wallet' : 'Import Wallet'}
</Text>
)}
</TouchableOpacity>
</View>
);
const renderSuccessStep = () => (
<View style={styles.stepContainer} testID="wallet-setup-success">
<View style={styles.successIconContainer}>
<Text style={styles.successIcon}></Text>
</View>
<Text style={styles.title}>Wallet Created!</Text>
<Text style={styles.subtitle}>
Your wallet is ready to use. You can now send and receive tokens.
</Text>
<View style={styles.addressBox}>
<Text style={styles.addressLabel}>Your Wallet Address</Text>
<Text style={styles.addressText} numberOfLines={1} ellipsizeMode="middle">
{createdAddress}
</Text>
</View>
<TouchableOpacity
style={styles.primaryButton}
onPress={handleFinish}
testID="wallet-setup-done-button"
>
<Text style={styles.primaryButtonText}>Go to Wallet</Text>
</TouchableOpacity>
</View>
);
const renderStep = () => {
switch (step) {
case 'choice':
return renderChoiceStep();
case 'create-show':
return renderCreateShowStep();
case 'create-verify':
return renderCreateVerifyStep();
case 'import':
return renderImportStep();
case 'wallet-name':
return renderWalletNameStep();
case 'success':
return renderSuccessStep();
default:
return renderChoiceStep();
}
};
return (
<SafeAreaView style={styles.container} testID="wallet-setup-screen">
{/* Header */}
{step !== 'choice' && step !== 'success' && (
<View style={styles.header}>
<TouchableOpacity onPress={handleBack} style={styles.backButton} testID="wallet-setup-back">
<Text style={styles.backButtonText}> Back</Text>
</TouchableOpacity>
<View style={styles.progressContainer}>
{['create-show', 'create-verify', 'wallet-name'].includes(step) && isCreateFlow && (
<>
<View style={[styles.progressDot, step === 'create-show' && styles.progressDotActive]} />
<View style={[styles.progressDot, step === 'create-verify' && styles.progressDotActive]} />
<View style={[styles.progressDot, step === 'wallet-name' && styles.progressDotActive]} />
</>
)}
{['import', 'wallet-name'].includes(step) && !isCreateFlow && (
<>
<View style={[styles.progressDot, step === 'import' && styles.progressDotActive]} />
<View style={[styles.progressDot, step === 'wallet-name' && styles.progressDotActive]} />
</>
)}
</View>
<View style={styles.headerSpacer} />
</View>
)}
{/* Close button on choice screen */}
{step === 'choice' && (
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.closeButton} testID="wallet-setup-close">
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
</View>
)}
<ScrollView
style={styles.content}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{renderStep()}
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
},
backButton: {
paddingVertical: 4,
paddingRight: 16,
},
backButtonText: {
fontSize: 16,
color: KurdistanColors.kesk,
fontWeight: '500',
},
closeButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#F5F5F5',
justifyContent: 'center',
alignItems: 'center',
},
closeButtonText: {
fontSize: 18,
color: '#666',
},
progressContainer: {
flexDirection: 'row',
gap: 8,
},
progressDot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: '#E0E0E0',
},
progressDotActive: {
backgroundColor: KurdistanColors.kesk,
},
headerSpacer: {
width: 60,
},
content: {
flex: 1,
},
contentContainer: {
padding: 24,
paddingBottom: 40,
},
stepContainer: {
flex: 1,
},
iconContainer: {
alignItems: 'center',
marginBottom: 24,
marginTop: 20,
},
mainIcon: {
fontSize: 80,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: KurdistanColors.reş,
textAlign: 'center',
marginBottom: 12,
},
subtitle: {
fontSize: 16,
color: '#666',
textAlign: 'center',
lineHeight: 24,
marginBottom: 32,
},
// Choice buttons
choiceButtons: {
gap: 16,
},
choiceButton: {
borderRadius: 16,
overflow: 'hidden',
},
choiceButtonGradient: {
padding: 24,
alignItems: 'center',
},
choiceButtonOutline: {
padding: 24,
alignItems: 'center',
borderWidth: 2,
borderColor: '#E0E0E0',
borderRadius: 16,
},
choiceButtonIcon: {
fontSize: 40,
marginBottom: 12,
},
choiceButtonTitle: {
fontSize: 18,
fontWeight: '600',
color: '#FFFFFF',
marginBottom: 4,
},
choiceButtonSubtitle: {
fontSize: 14,
color: 'rgba(255,255,255,0.8)',
},
// Warning box
warningBox: {
flexDirection: 'row',
backgroundColor: '#FFF3CD',
borderRadius: 12,
padding: 16,
marginBottom: 24,
alignItems: 'flex-start',
},
warningIcon: {
fontSize: 20,
marginRight: 12,
},
warningText: {
flex: 1,
fontSize: 14,
color: '#856404',
lineHeight: 20,
},
// Mnemonic grid
mnemonicGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
marginBottom: 32,
},
wordCard: {
width: '31%',
backgroundColor: '#F8F9FA',
borderRadius: 12,
padding: 12,
marginBottom: 12,
flexDirection: 'row',
alignItems: 'center',
},
wordNumber: {
fontSize: 12,
color: '#999',
marginRight: 8,
minWidth: 20,
},
wordText: {
fontSize: 14,
fontWeight: '600',
color: KurdistanColors.reş,
},
// Verification
verificationContainer: {
marginBottom: 32,
},
verificationItem: {
marginBottom: 24,
},
verificationLabel: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.reş,
marginBottom: 12,
},
verificationOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 10,
},
verificationOption: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
backgroundColor: '#F5F5F5',
borderWidth: 2,
borderColor: 'transparent',
},
verificationOptionSelected: {
borderColor: KurdistanColors.kesk,
backgroundColor: 'rgba(0, 143, 67, 0.1)',
},
verificationOptionCorrect: {
borderColor: KurdistanColors.kesk,
backgroundColor: 'rgba(0, 143, 67, 0.15)',
},
verificationOptionText: {
fontSize: 14,
color: '#333',
},
verificationOptionTextSelected: {
color: KurdistanColors.kesk,
fontWeight: '600',
},
// Import
importInputContainer: {
marginBottom: 32,
},
importInput: {
backgroundColor: '#F8F9FA',
borderRadius: 16,
padding: 16,
fontSize: 16,
color: KurdistanColors.reş,
minHeight: 120,
textAlignVertical: 'top',
borderWidth: 1,
borderColor: '#E0E0E0',
},
importHint: {
fontSize: 12,
color: '#999',
marginTop: 8,
marginLeft: 4,
},
// Wallet name
nameInputContainer: {
marginBottom: 32,
},
nameInput: {
backgroundColor: '#F8F9FA',
borderRadius: 16,
padding: 16,
fontSize: 18,
color: KurdistanColors.reş,
borderWidth: 1,
borderColor: '#E0E0E0',
textAlign: 'center',
},
// Primary button
primaryButton: {
backgroundColor: KurdistanColors.kesk,
borderRadius: 16,
padding: 18,
alignItems: 'center',
},
primaryButtonDisabled: {
opacity: 0.5,
},
primaryButtonText: {
fontSize: 18,
fontWeight: '600',
color: '#FFFFFF',
},
// Success
successIconContainer: {
alignItems: 'center',
marginBottom: 24,
marginTop: 40,
},
successIcon: {
fontSize: 80,
},
addressBox: {
backgroundColor: '#F8F9FA',
borderRadius: 16,
padding: 20,
marginBottom: 32,
alignItems: 'center',
},
addressLabel: {
fontSize: 12,
color: '#999',
marginBottom: 8,
},
addressText: {
fontSize: 14,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
color: KurdistanColors.reş,
},
});
export default WalletSetupScreen;