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 92e5831f7c
commit 24d6a942f8
110 changed files with 11157 additions and 3260 deletions
+30 -32
View File
@@ -14,12 +14,10 @@ import {
Image,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../contexts/AuthContext';
import { KurdistanColors } from '../theme/colors';
const AuthScreen: React.FC = () => {
const { t } = useTranslation();
const { signIn, signUp } = useAuth();
// Tab state
@@ -47,7 +45,7 @@ const AuthScreen: React.FC = () => {
setError('');
if (!loginEmail || !loginPassword) {
setError(t('auth.fillAllFields', 'Please fill in all fields'));
setError('Please fill in all fields');
return;
}
@@ -58,13 +56,13 @@ const AuthScreen: React.FC = () => {
if (signInError) {
if (signInError.message?.includes('Invalid login credentials')) {
setError(t('auth.invalidCredentials', 'Email or password is incorrect'));
setError('Email or password is incorrect');
} else {
setError(signInError.message || t('auth.loginFailed', 'Login failed'));
setError(signInError.message || 'Login failed');
}
}
} catch (err) {
setError(t('auth.loginFailed', 'Login failed. Please try again.'));
setError('Login failed. Please try again.');
if (__DEV__) console.error('Sign in error:', err);
} finally {
setLoading(false);
@@ -75,17 +73,17 @@ const AuthScreen: React.FC = () => {
setError('');
if (!signupName || !signupEmail || !signupPassword || !signupConfirmPassword) {
setError(t('auth.fillAllFields', 'Please fill in all required fields'));
setError('Please fill in all required fields');
return;
}
if (signupPassword !== signupConfirmPassword) {
setError(t('auth.passwordsDoNotMatch', 'Passwords do not match'));
setError('Passwords do not match');
return;
}
if (signupPassword.length < 8) {
setError(t('auth.passwordTooShort', 'Password must be at least 8 characters'));
setError('Password must be at least 8 characters');
return;
}
@@ -100,10 +98,10 @@ const AuthScreen: React.FC = () => {
);
if (signUpError) {
setError(signUpError.message || t('auth.signupFailed', 'Sign up failed'));
setError(signUpError.message || 'Sign up failed');
}
} catch (err) {
setError(t('auth.signupFailed', 'Sign up failed. Please try again.'));
setError('Sign up failed. Please try again.');
if (__DEV__) console.error('Sign up error:', err);
} finally {
setLoading(false);
@@ -144,7 +142,7 @@ const AuthScreen: React.FC = () => {
</View>
<Text style={styles.brandTitle}>PezkuwiChain</Text>
<Text style={styles.subtitle}>
{t('login.subtitle', 'Access your governance account')}
Access your governance account
</Text>
</View>
@@ -158,7 +156,7 @@ const AuthScreen: React.FC = () => {
}}
>
<Text style={[styles.tabText, activeTab === 'signin' && styles.tabTextActive]}>
{t('login.signin', 'Sign In')}
Sign In
</Text>
</TouchableOpacity>
<TouchableOpacity
@@ -169,7 +167,7 @@ const AuthScreen: React.FC = () => {
}}
>
<Text style={[styles.tabText, activeTab === 'signup' && styles.tabTextActive]}>
{t('login.signup', 'Sign Up')}
Sign Up
</Text>
</TouchableOpacity>
</View>
@@ -178,7 +176,7 @@ const AuthScreen: React.FC = () => {
{activeTab === 'signin' && (
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('login.email', 'Email')}</Text>
<Text style={styles.label}>Email</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}></Text>
<TextInput
@@ -195,7 +193,7 @@ const AuthScreen: React.FC = () => {
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('login.password', 'Password')}</Text>
<Text style={styles.label}>Password</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}>🔒</Text>
<TextInput
@@ -225,12 +223,12 @@ const AuthScreen: React.FC = () => {
{rememberMe && <Text style={styles.checkmark}></Text>}
</View>
<Text style={styles.checkboxLabel}>
{t('login.rememberMe', 'Remember me')}
Remember me
</Text>
</TouchableOpacity>
<TouchableOpacity>
<Text style={styles.linkText}>
{t('login.forgotPassword', 'Forgot password?')}
Forgot password?
</Text>
</TouchableOpacity>
</View>
@@ -251,7 +249,7 @@ const AuthScreen: React.FC = () => {
<ActivityIndicator color="#FFFFFF" />
) : (
<Text style={styles.primaryButtonText}>
{t('login.signin', 'Sign In')}
Sign In
</Text>
)}
</TouchableOpacity>
@@ -262,7 +260,7 @@ const AuthScreen: React.FC = () => {
{activeTab === 'signup' && (
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('login.fullName', 'Full Name')}</Text>
<Text style={styles.label}>Full Name</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}>👤</Text>
<TextInput
@@ -277,7 +275,7 @@ const AuthScreen: React.FC = () => {
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('login.email', 'Email')}</Text>
<Text style={styles.label}>Email</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}></Text>
<TextInput
@@ -294,7 +292,7 @@ const AuthScreen: React.FC = () => {
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('login.password', 'Password')}</Text>
<Text style={styles.label}>Password</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}>🔒</Text>
<TextInput
@@ -316,7 +314,7 @@ const AuthScreen: React.FC = () => {
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('login.confirmPassword', 'Confirm Password')}</Text>
<Text style={styles.label}>Confirm Password</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}>🔒</Text>
<TextInput
@@ -333,16 +331,16 @@ const AuthScreen: React.FC = () => {
<View style={styles.inputGroup}>
<Text style={styles.label}>
{t('login.referralCode', 'Referral Code')}{' '}
Referral Code{' '}
<Text style={styles.optionalText}>
({t('login.optional', 'Optional')})
(Optional)
</Text>
</Text>
<View style={styles.inputContainer}>
<Text style={styles.inputIcon}>👥</Text>
<TextInput
style={styles.input}
placeholder={t('login.enterReferralCode', 'Referral code (optional)')}
placeholder="Referral code (optional)"
placeholderTextColor="#9CA3AF"
value={signupReferralCode}
onChangeText={setSignupReferralCode}
@@ -350,7 +348,7 @@ const AuthScreen: React.FC = () => {
/>
</View>
<Text style={styles.hintText}>
{t('login.referralDescription', 'If someone referred you, enter their code here')}
If someone referred you, enter their code here
</Text>
</View>
@@ -370,7 +368,7 @@ const AuthScreen: React.FC = () => {
<ActivityIndicator color="#FFFFFF" />
) : (
<Text style={styles.primaryButtonText}>
{t('login.createAccount', 'Create Account')}
Create Account
</Text>
)}
</TouchableOpacity>
@@ -380,15 +378,15 @@ const AuthScreen: React.FC = () => {
{/* Footer */}
<View style={styles.footer}>
<Text style={styles.footerText}>
{t('login.terms', 'By continuing, you agree to our')}{' '}
By continuing, you agree to our{' '}
</Text>
<View style={styles.footerLinks}>
<Text style={styles.footerLink}>
{t('login.termsOfService', 'Terms of Service')}
Terms of Service
</Text>
<Text style={styles.footerText}> {t('login.and', 'and')} </Text>
<Text style={styles.footerText}> and </Text>
<Text style={styles.footerLink}>
{t('login.privacyPolicy', 'Privacy Policy')}
Privacy Policy
</Text>
</View>
</View>
@@ -12,7 +12,6 @@ import {
ActivityIndicator,
Modal,
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import {
@@ -97,7 +96,6 @@ const CustomPicker: React.FC<{
};
const BeCitizenApplyScreen: React.FC = () => {
const { t } = useTranslation();
const navigation = useNavigation();
const { api, selectedAccount } = usePezkuwi();
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -9,7 +9,6 @@ import {
StatusBar,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import { KurdistanColors } from '../theme/colors';
import type { NavigationProp } from '@react-navigation/native';
@@ -21,7 +20,6 @@ type RootStackParamList = {
};
const BeCitizenChoiceScreen: React.FC = () => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
return (
@@ -10,14 +10,12 @@ import {
Alert,
ActivityIndicator,
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { getCitizenshipStatus } from '@pezkuwi/lib/citizenship-workflow';
import { KurdistanColors } from '../theme/colors';
const BeCitizenClaimScreen: React.FC = () => {
const { t } = useTranslation();
const navigation = useNavigation();
const { api, selectedAccount } = usePezkuwi();
const [isSubmitting, setIsSubmitting] = useState(false);
-2
View File
@@ -12,7 +12,6 @@ import {
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import {
submitKycApplication,
@@ -22,7 +21,6 @@ import {
import { KurdistanColors } from '../theme/colors';
const BeCitizenScreen: React.FC = () => {
const { t: _t } = useTranslation();
const { api, selectedAccount } = usePezkuwi();
const [_isExistingCitizen, _setIsExistingCitizen] = useState(false);
const [currentStep, setCurrentStep] = useState<'choice' | 'new' | 'existing'>('choice');
+4 -19
View File
@@ -14,7 +14,6 @@ import {
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import type { NavigationProp } from '@react-navigation/native';
import type { BottomTabParamList } from '../navigation/BottomTabNavigator';
@@ -82,7 +81,6 @@ const getEmojiFromAvatarId = (avatarId: string): string => {
interface DashboardScreenProps {}
const DashboardScreen: React.FC<DashboardScreenProps> = () => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<BottomTabParamList & RootStackParamList>>();
const { user } = useAuth();
const { api, isApiReady, selectedAccount } = usePezkuwi();
@@ -171,8 +169,8 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
const showComingSoon = (featureName: string) => {
Alert.alert(
t('settingsScreen.comingSoon'),
`${featureName} ${t('settingsScreen.comingSoonMessage')}`,
'Coming Soon',
`${featureName} will be available soon!`,
[{ text: 'OK' }]
);
};
@@ -431,21 +429,8 @@ const DashboardScreen: React.FC<DashboardScreenProps> = () => {
</TouchableOpacity>
</View>
<View style={styles.appsGrid}>
{/* Wallet Visitors - Everyone can use */}
{renderAppIcon('Wallet Visitors', '👁️', () => showComingSoon('Wallet Visitors'), true)}
{/* Wallet Welati - Only Citizens can use */}
{renderAppIcon('Wallet Welati', '🏛️', () => {
if (tikis.includes('Citizen') || tikis.includes('Welati')) {
showComingSoon('Wallet Welati');
} else {
Alert.alert(
'Citizens Only',
'Wallet Welati is only available to Pezkuwi citizens. Please apply for citizenship first.',
[{ text: 'OK' }]
);
}
}, true, !tikis.includes('Citizen') && !tikis.includes('Welati'))}
{/* Wallet - Navigate to WalletScreen */}
{renderAppIcon('Wallet', '👛', () => navigation.navigate('Wallet'), true)}
{renderAppIcon('Bank', qaBank, () => showComingSoon('Bank'), false, true)}
{renderAppIcon('Exchange', qaExchange, () => showComingSoon('Swap'), false)}
+450
View File
@@ -0,0 +1,450 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
SafeAreaView,
ScrollView,
TextInput,
ActivityIndicator,
Alert,
Platform,
KeyboardAvoidingView,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useAuth } from '../contexts/AuthContext';
import { useTheme } from '../contexts/ThemeContext';
import { KurdistanColors } from '../theme/colors';
import { supabase } from '../lib/supabase';
import AvatarPickerModal from '../components/AvatarPickerModal';
// Cross-platform alert helper
const showAlert = (title: string, message: string, buttons?: Array<{text: string; onPress?: () => void; style?: string}>) => {
if (Platform.OS === 'web') {
if (buttons && buttons.length > 1) {
const result = window.confirm(`${title}\n\n${message}`);
if (result && buttons[1]?.onPress) {
buttons[1].onPress();
} else if (!result && buttons[0]?.onPress) {
buttons[0].onPress();
}
} else {
window.alert(`${title}\n\n${message}`);
if (buttons?.[0]?.onPress) buttons[0].onPress();
}
} else {
Alert.alert(title, message, buttons as any);
}
};
// Avatar pool matching AvatarPickerModal
const AVATAR_POOL = [
{ id: 'avatar1', emoji: '👨🏻' },
{ id: 'avatar2', emoji: '👨🏼' },
{ id: 'avatar3', emoji: '👨🏽' },
{ id: 'avatar4', emoji: '👨🏾' },
{ id: 'avatar5', emoji: '👩🏻' },
{ id: 'avatar6', emoji: '👩🏼' },
{ id: 'avatar7', emoji: '👩🏽' },
{ id: 'avatar8', emoji: '👩🏾' },
{ id: 'avatar9', emoji: '🧔🏻' },
{ id: 'avatar10', emoji: '🧔🏼' },
{ id: 'avatar11', emoji: '🧔🏽' },
{ id: 'avatar12', emoji: '🧔🏾' },
{ id: 'avatar13', emoji: '👳🏻‍♂️' },
{ id: 'avatar14', emoji: '👳🏼‍♂️' },
{ id: 'avatar15', emoji: '👳🏽‍♂️' },
{ id: 'avatar16', emoji: '🧕🏻' },
{ id: 'avatar17', emoji: '🧕🏼' },
{ id: 'avatar18', emoji: '🧕🏽' },
{ id: 'avatar19', emoji: '👴🏻' },
{ id: 'avatar20', emoji: '👴🏼' },
{ id: 'avatar21', emoji: '👵🏻' },
{ id: 'avatar22', emoji: '👵🏼' },
{ id: 'avatar23', emoji: '👦🏻' },
{ id: 'avatar24', emoji: '👦🏼' },
{ id: 'avatar25', emoji: '👧🏻' },
{ id: 'avatar26', emoji: '👧🏼' },
];
const getEmojiFromAvatarId = (avatarId: string): string => {
const avatar = AVATAR_POOL.find(a => a.id === avatarId);
return avatar ? avatar.emoji : '👤';
};
const EditProfileScreen: React.FC = () => {
const navigation = useNavigation();
const { user } = useAuth();
const { isDarkMode, colors, fontScale } = useTheme();
const [fullName, setFullName] = useState('');
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const [originalName, setOriginalName] = useState('');
const [originalAvatar, setOriginalAvatar] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [avatarModalVisible, setAvatarModalVisible] = useState(false);
useEffect(() => {
fetchProfile();
}, []);
const fetchProfile = async () => {
if (!user) {
setLoading(false);
return;
}
try {
const { data, error } = await supabase
.from('profiles')
.select('full_name, avatar_url')
.eq('id', user.id)
.single();
if (error) throw error;
setFullName(data?.full_name || '');
setAvatarUrl(data?.avatar_url || null);
setOriginalName(data?.full_name || '');
setOriginalAvatar(data?.avatar_url || null);
} catch (error) {
if (__DEV__) console.error('Error fetching profile:', error);
showAlert('Error', 'Failed to load profile data');
} finally {
setLoading(false);
}
};
const hasChanges = () => {
return fullName !== originalName || avatarUrl !== originalAvatar;
};
const handleSave = async () => {
if (!user) return;
if (!hasChanges()) {
navigation.goBack();
return;
}
setSaving(true);
try {
const updates: { full_name?: string | null; avatar_url?: string | null } = {};
if (fullName !== originalName) {
updates.full_name = fullName.trim() || null;
}
if (avatarUrl !== originalAvatar) {
updates.avatar_url = avatarUrl;
}
const { error } = await supabase
.from('profiles')
.update(updates)
.eq('id', user.id);
if (error) throw error;
showAlert('Success', 'Profile updated successfully', [
{ text: 'OK', onPress: () => navigation.goBack() }
]);
} catch (error) {
if (__DEV__) console.error('Error saving profile:', error);
showAlert('Error', 'Failed to save profile. Please try again.');
} finally {
setSaving(false);
}
};
const handleCancel = () => {
if (hasChanges()) {
showAlert(
'Discard Changes?',
'You have unsaved changes. Are you sure you want to go back?',
[
{ text: 'Keep Editing', style: 'cancel' },
{ text: 'Discard', style: 'destructive', onPress: () => navigation.goBack() }
]
);
} else {
navigation.goBack();
}
};
const handleAvatarSelected = (newAvatarUrl: string) => {
setAvatarUrl(newAvatarUrl);
};
if (loading) {
return (
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} testID="edit-profile-loading">
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
<Text style={[styles.loadingText, { color: colors.textSecondary, fontSize: 14 * fontScale }]}>
Loading profile...
</Text>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} testID="edit-profile-screen">
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardAvoid}
>
{/* Header */}
<View style={[styles.header, { borderBottomColor: colors.border }]} testID="edit-profile-header">
<TouchableOpacity onPress={handleCancel} testID="edit-profile-cancel-button">
<Text style={[styles.headerButton, { color: colors.textSecondary, fontSize: 16 * fontScale }]}>
Cancel
</Text>
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: colors.text, fontSize: 18 * fontScale }]}>
Edit Profile
</Text>
<TouchableOpacity
onPress={handleSave}
disabled={saving || !hasChanges()}
testID="edit-profile-save-button"
>
{saving ? (
<ActivityIndicator size="small" color={KurdistanColors.kesk} />
) : (
<Text style={[
styles.headerButton,
styles.saveButton,
{ fontSize: 16 * fontScale },
!hasChanges() && styles.saveButtonDisabled
]}>
Save
</Text>
)}
</TouchableOpacity>
</View>
<ScrollView
style={styles.content}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
testID="edit-profile-scroll"
>
{/* Avatar Section */}
<View style={styles.avatarSection} testID="edit-profile-avatar-section">
<TouchableOpacity
onPress={() => setAvatarModalVisible(true)}
style={styles.avatarButton}
testID="edit-profile-avatar-button"
>
<View style={[styles.avatarCircle, { backgroundColor: colors.surface }]}>
{avatarUrl ? (
<Text style={styles.avatarEmoji}>{getEmojiFromAvatarId(avatarUrl)}</Text>
) : (
<Text style={[styles.avatarInitial, { color: colors.textSecondary }]}>
{fullName?.charAt(0)?.toUpperCase() || user?.email?.charAt(0)?.toUpperCase() || '?'}
</Text>
)}
</View>
<View style={styles.editAvatarBadge}>
<Text style={styles.editAvatarIcon}>📷</Text>
</View>
</TouchableOpacity>
<Text style={[styles.changePhotoText, { color: KurdistanColors.kesk, fontSize: 14 * fontScale }]}>
Change Avatar
</Text>
</View>
{/* Form Section */}
<View style={styles.formSection}>
{/* Display Name */}
<View style={styles.inputGroup} testID="edit-profile-name-group">
<Text style={[styles.inputLabel, { color: colors.textSecondary, fontSize: 14 * fontScale }]}>
Display Name
</Text>
<TextInput
style={[styles.textInput, {
backgroundColor: colors.surface,
color: colors.text,
borderColor: colors.border,
fontSize: 16 * fontScale
}]}
value={fullName}
onChangeText={setFullName}
placeholder="Enter your display name"
placeholderTextColor={colors.textSecondary}
autoCapitalize="words"
autoCorrect={false}
testID="edit-profile-name-input"
/>
<Text style={[styles.inputHint, { color: colors.textSecondary, fontSize: 12 * fontScale }]}>
This is how other users will see you
</Text>
</View>
{/* Email (Read-only) */}
<View style={styles.inputGroup} testID="edit-profile-email-group">
<Text style={[styles.inputLabel, { color: colors.textSecondary, fontSize: 14 * fontScale }]}>
Email
</Text>
<View style={[styles.readOnlyField, { backgroundColor: colors.background, borderColor: colors.border }]}>
<Text style={[styles.readOnlyText, { color: colors.textSecondary, fontSize: 16 * fontScale }]}>
{user?.email || 'N/A'}
</Text>
<Text style={styles.lockIcon}>🔒</Text>
</View>
<Text style={[styles.inputHint, { color: colors.textSecondary, fontSize: 12 * fontScale }]}>
Email cannot be changed
</Text>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
{/* Avatar Picker Modal */}
<AvatarPickerModal
visible={avatarModalVisible}
onClose={() => setAvatarModalVisible(false)}
currentAvatar={avatarUrl || undefined}
onAvatarSelected={handleAvatarSelected}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
keyboardAvoid: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: 12,
},
loadingText: {
fontSize: 14,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
},
headerButton: {
fontSize: 16,
fontWeight: '500',
},
headerTitle: {
fontSize: 18,
fontWeight: '600',
},
saveButton: {
color: KurdistanColors.kesk,
fontWeight: '600',
},
saveButtonDisabled: {
opacity: 0.4,
},
content: {
flex: 1,
},
contentContainer: {
padding: 24,
},
avatarSection: {
alignItems: 'center',
marginBottom: 32,
},
avatarButton: {
position: 'relative',
},
avatarCircle: {
width: 120,
height: 120,
borderRadius: 60,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 4,
},
avatarEmoji: {
fontSize: 70,
},
avatarInitial: {
fontSize: 48,
fontWeight: 'bold',
},
editAvatarBadge: {
position: 'absolute',
bottom: 4,
right: 4,
backgroundColor: '#FFFFFF',
width: 36,
height: 36,
borderRadius: 18,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 4,
},
editAvatarIcon: {
fontSize: 18,
},
changePhotoText: {
marginTop: 12,
fontWeight: '500',
},
formSection: {
gap: 24,
},
inputGroup: {
gap: 8,
},
inputLabel: {
fontSize: 14,
fontWeight: '600',
marginLeft: 4,
},
textInput: {
borderWidth: 1,
borderRadius: 12,
padding: 16,
fontSize: 16,
},
inputHint: {
fontSize: 12,
marginLeft: 4,
},
readOnlyField: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderWidth: 1,
borderRadius: 12,
padding: 16,
},
readOnlyText: {
fontSize: 16,
flex: 1,
},
lockIcon: {
fontSize: 16,
marginLeft: 8,
},
});
export default EditProfileScreen;
+77 -37
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
@@ -10,15 +10,33 @@ import {
Image,
ActivityIndicator,
Alert,
Platform,
} from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import { useAuth } from '../contexts/AuthContext';
import { useTheme } from '../contexts/ThemeContext';
import { KurdistanColors } from '../theme/colors';
import { supabase } from '../lib/supabase';
import AvatarPickerModal from '../components/AvatarPickerModal';
// Cross-platform alert helper
const showAlert = (title: string, message: string, buttons?: Array<{text: string; onPress?: () => void; style?: string}>) => {
if (Platform.OS === 'web') {
if (buttons && buttons.length > 1) {
const result = window.confirm(`${title}\n\n${message}`);
if (result && buttons[1]?.onPress) {
buttons[1].onPress();
}
} else {
window.alert(`${title}\n\n${message}`);
}
} else {
Alert.alert(title, message, buttons as any);
}
};
// Avatar pool matching AvatarPickerModal
const AVATAR_POOL = [
{ id: 'avatar1', emoji: '👨🏻' },
@@ -65,16 +83,19 @@ interface ProfileData {
}
const ProfileScreen: React.FC = () => {
const { t } = useTranslation();
const navigation = useNavigation();
const navigation = useNavigation<any>();
const { user, signOut } = useAuth();
const { isDarkMode, colors, fontScale } = useTheme();
const [profileData, setProfileData] = useState<ProfileData | null>(null);
const [loading, setLoading] = useState(true);
const [avatarModalVisible, setAvatarModalVisible] = useState(false);
useEffect(() => {
fetchProfileData();
}, [user]);
// Refresh profile data when screen is focused (e.g., after EditProfile)
useFocusEffect(
useCallback(() => {
fetchProfileData();
}, [user])
);
const fetchProfileData = async () => {
if (!user) {
@@ -100,7 +121,7 @@ const ProfileScreen: React.FC = () => {
};
const handleLogout = () => {
Alert.alert(
showAlert(
'Logout',
'Are you sure you want to logout?',
[
@@ -120,12 +141,22 @@ const ProfileScreen: React.FC = () => {
setProfileData(prev => prev ? { ...prev, avatar_url: avatarUrl } : null);
};
const ProfileCard = ({ icon, title, value, onPress }: { icon: string; title: string; value: string; onPress?: () => void }) => (
<TouchableOpacity style={styles.profileCard} onPress={onPress} disabled={!onPress} activeOpacity={onPress ? 0.7 : 1}>
const handleEditProfile = () => {
navigation.navigate('EditProfile');
};
const ProfileCard = ({ icon, title, value, onPress, testID }: { icon: string; title: string; value: string; onPress?: () => void; testID?: string }) => (
<TouchableOpacity
style={[styles.profileCard, { backgroundColor: colors.surface }]}
onPress={onPress}
disabled={!onPress}
activeOpacity={onPress ? 0.7 : 1}
testID={testID}
>
<Text style={styles.cardIcon}>{icon}</Text>
<View style={styles.cardContent}>
<Text style={styles.cardTitle}>{title}</Text>
<Text style={styles.cardValue} numberOfLines={1}>{value}</Text>
<Text style={[styles.cardTitle, { fontSize: 12 * fontScale }]}>{title}</Text>
<Text style={[styles.cardValue, { color: colors.text, fontSize: 16 * fontScale }]} numberOfLines={1}>{value}</Text>
</View>
{onPress && <Text style={styles.cardArrow}></Text>}
</TouchableOpacity>
@@ -133,41 +164,42 @@ const ProfileScreen: React.FC = () => {
if (loading) {
return (
<SafeAreaView style={styles.container}>
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} testID="profile-loading-container">
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
<ActivityIndicator size="large" color={KurdistanColors.kesk} testID="profile-loading-indicator" />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} testID="profile-screen">
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<ScrollView showsVerticalScrollIndicator={false}>
<ScrollView showsVerticalScrollIndicator={false} testID="profile-scroll-view">
{/* Header with Gradient */}
<LinearGradient
colors={[KurdistanColors.kesk, '#008f43']}
style={styles.header}
testID="profile-header-gradient"
>
<View style={styles.avatarContainer}>
<TouchableOpacity onPress={() => setAvatarModalVisible(true)} style={styles.avatarWrapper}>
<TouchableOpacity onPress={() => setAvatarModalVisible(true)} style={styles.avatarWrapper} testID="profile-avatar-button">
{profileData?.avatar_url ? (
// Check if avatar_url is a URL (starts with http) or an emoji ID
profileData.avatar_url.startsWith('http') ? (
<Image source={{ uri: profileData.avatar_url }} style={styles.avatar} />
<Image source={{ uri: profileData.avatar_url }} style={styles.avatar} testID="profile-avatar-image" />
) : (
// It's an emoji ID, render as emoji text
<View style={styles.avatarPlaceholder}>
<Text style={styles.avatarEmojiLarge}>
<View style={styles.avatarPlaceholder} testID="profile-avatar-emoji-container">
<Text style={styles.avatarEmojiLarge} testID="profile-avatar-emoji">
{getEmojiFromAvatarId(profileData.avatar_url)}
</Text>
</View>
)
) : (
<View style={styles.avatarPlaceholder}>
<Text style={styles.avatarText}>
<View style={styles.avatarPlaceholder} testID="profile-avatar-placeholder">
<Text style={styles.avatarText} testID="profile-avatar-initial">
{profileData?.full_name?.charAt(0)?.toUpperCase() || user?.email?.charAt(0)?.toUpperCase() || '?'}
</Text>
</View>
@@ -176,25 +208,27 @@ const ProfileScreen: React.FC = () => {
<Text style={styles.editAvatarIcon}>📷</Text>
</View>
</TouchableOpacity>
<Text style={styles.name}>
<Text style={[styles.name, { fontSize: 24 * fontScale }]} testID="profile-name">
{profileData?.full_name || user?.email?.split('@')[0] || 'User'}
</Text>
<Text style={styles.email}>{user?.email}</Text>
<Text style={[styles.email, { fontSize: 14 * fontScale }]} testID="profile-email">{user?.email}</Text>
</View>
</LinearGradient>
{/* Profile Info Cards */}
<View style={styles.cardsContainer}>
<View style={styles.cardsContainer} testID="profile-cards-container">
<ProfileCard
icon="📧"
title="Email"
value={user?.email || 'N/A'}
testID="profile-card-email"
/>
<ProfileCard
icon="📅"
title="Member Since"
value={profileData?.created_at ? new Date(profileData.created_at).toLocaleDateString() : 'N/A'}
testID="profile-card-member-since"
/>
<ProfileCard
@@ -202,6 +236,7 @@ const ProfileScreen: React.FC = () => {
title="Referrals"
value={`${profileData?.referral_count || 0} people`}
onPress={() => (navigation as any).navigate('Referral')}
testID="profile-card-referrals"
/>
{profileData?.referral_code && (
@@ -209,6 +244,7 @@ const ProfileScreen: React.FC = () => {
icon="🎁"
title="Your Referral Code"
value={profileData.referral_code}
testID="profile-card-referral-code"
/>
)}
@@ -217,31 +253,34 @@ const ProfileScreen: React.FC = () => {
icon="👛"
title="Wallet Address"
value={`${profileData.wallet_address.slice(0, 10)}...${profileData.wallet_address.slice(-8)}`}
testID="profile-card-wallet"
/>
)}
</View>
{/* Action Buttons */}
<View style={styles.actionsContainer}>
<View style={styles.actionsContainer} testID="profile-actions-container">
<TouchableOpacity
style={styles.actionButton}
onPress={() => Alert.alert('Coming Soon', 'Edit profile feature will be available soon')}
style={[styles.actionButton, { backgroundColor: colors.surface }]}
onPress={handleEditProfile}
testID="profile-edit-button"
>
<Text style={styles.actionIcon}></Text>
<Text style={styles.actionText}>Edit Profile</Text>
<Text style={[styles.actionText, { color: colors.text, fontSize: 16 * fontScale }]}>Edit Profile</Text>
<Text style={styles.actionArrow}></Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
onPress={() => Alert.alert(
style={[styles.actionButton, { backgroundColor: colors.surface }]}
onPress={() => showAlert(
'About Pezkuwi',
'Pezkuwi is a decentralized blockchain platform for Digital Kurdistan.\n\nVersion: 1.0.0\n\n© 2026 Digital Kurdistan',
[{ text: 'OK' }]
)}
testID="profile-about-button"
>
<Text style={styles.actionIcon}></Text>
<Text style={styles.actionText}>About Pezkuwi</Text>
<Text style={[styles.actionText, { color: colors.text, fontSize: 16 * fontScale }]}>About Pezkuwi</Text>
<Text style={styles.actionArrow}></Text>
</TouchableOpacity>
</View>
@@ -251,15 +290,16 @@ const ProfileScreen: React.FC = () => {
style={styles.logoutButton}
onPress={handleLogout}
activeOpacity={0.8}
testID="profile-logout-button"
>
<Text style={styles.logoutButtonText}>Logout</Text>
<Text style={[styles.logoutButtonText, { fontSize: 16 * fontScale }]}>Logout</Text>
</TouchableOpacity>
<View style={styles.footer}>
<Text style={styles.footerText}>
<View style={styles.footer} testID="profile-footer">
<Text style={[styles.footerText, { color: colors.textSecondary, fontSize: 12 * fontScale }]}>
Pezkuwi Blockchain {new Date().getFullYear()}
</Text>
<Text style={styles.footerVersion}>Version 1.0.0</Text>
<Text style={[styles.footerVersion, { fontSize: 10 * fontScale }]}>Version 1.0.0</Text>
</View>
</ScrollView>
-2
View File
@@ -13,7 +13,6 @@ import {
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { KurdistanColors } from '../theme/colors';
import {
@@ -41,7 +40,6 @@ interface Referral {
}
const ReferralScreen: React.FC = () => {
const { t: _t } = useTranslation();
const { selectedAccount, api, connectWallet, isApiReady } = usePezkuwi();
const isConnected = !!selectedAccount;
File diff suppressed because it is too large Load Diff
+10 -12
View File
@@ -14,7 +14,6 @@ import {
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../contexts/AuthContext';
import { KurdistanColors } from '../theme/colors';
@@ -24,7 +23,6 @@ interface SignInScreenProps {
}
const SignInScreen: React.FC<SignInScreenProps> = ({ onSignIn, onNavigateToSignUp }) => {
const { t } = useTranslation();
const { signIn } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
@@ -78,17 +76,17 @@ const SignInScreen: React.FC<SignInScreenProps> = ({ onSignIn, onNavigateToSignU
<View style={styles.logoContainer}>
<Text style={styles.logoText}>PZK</Text>
</View>
<Text style={styles.title}>{t('auth.welcomeBack')}</Text>
<Text style={styles.subtitle}>{t('auth.signIn')}</Text>
<Text style={styles.title}>Welcome Back!</Text>
<Text style={styles.subtitle}>Sign In</Text>
</View>
{/* Form */}
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('auth.email')}</Text>
<Text style={styles.label}>Email</Text>
<TextInput
style={styles.input}
placeholder={t('auth.email')}
placeholder="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
@@ -98,10 +96,10 @@ const SignInScreen: React.FC<SignInScreenProps> = ({ onSignIn, onNavigateToSignU
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('auth.password')}</Text>
<Text style={styles.label}>Password</Text>
<TextInput
style={styles.input}
placeholder={t('auth.password')}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
@@ -111,7 +109,7 @@ const SignInScreen: React.FC<SignInScreenProps> = ({ onSignIn, onNavigateToSignU
<TouchableOpacity style={styles.forgotPassword}>
<Text style={styles.forgotPasswordText}>
{t('auth.forgotPassword')}
Forgot Password?
</Text>
</TouchableOpacity>
@@ -124,7 +122,7 @@ const SignInScreen: React.FC<SignInScreenProps> = ({ onSignIn, onNavigateToSignU
{isLoading ? (
<ActivityIndicator color={KurdistanColors.spi} />
) : (
<Text style={styles.signInButtonText}>{t('auth.signIn')}</Text>
<Text style={styles.signInButtonText}>Sign In</Text>
)}
</TouchableOpacity>
@@ -139,8 +137,8 @@ const SignInScreen: React.FC<SignInScreenProps> = ({ onSignIn, onNavigateToSignU
onPress={onNavigateToSignUp}
>
<Text style={styles.signUpPromptText}>
{t('auth.noAccount')}{' '}
<Text style={styles.signUpLink}>{t('auth.signUp')}</Text>
Don't have an account?{' '}
<Text style={styles.signUpLink}>Sign Up</Text>
</Text>
</TouchableOpacity>
</View>
+13 -15
View File
@@ -14,7 +14,6 @@ import {
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../contexts/AuthContext';
import { KurdistanColors } from '../theme/colors';
@@ -24,7 +23,6 @@ interface SignUpScreenProps {
}
const SignUpScreen: React.FC<SignUpScreenProps> = ({ onSignUp, onNavigateToSignIn }) => {
const { t } = useTranslation();
const { signUp } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
@@ -85,17 +83,17 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ onSignUp, onNavigateToSignI
<View style={styles.logoContainer}>
<Text style={styles.logoText}>PZK</Text>
</View>
<Text style={styles.title}>{t('auth.getStarted')}</Text>
<Text style={styles.subtitle}>{t('auth.createAccount')}</Text>
<Text style={styles.title}>Get Started</Text>
<Text style={styles.subtitle}>Create Account</Text>
</View>
{/* Form */}
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('auth.email')}</Text>
<Text style={styles.label}>Email</Text>
<TextInput
style={styles.input}
placeholder={t('auth.email')}
placeholder="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
@@ -105,10 +103,10 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ onSignUp, onNavigateToSignI
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('auth.username')}</Text>
<Text style={styles.label}>Username</Text>
<TextInput
style={styles.input}
placeholder={t('auth.username')}
placeholder="Username"
value={username}
onChangeText={setUsername}
autoCapitalize="none"
@@ -117,10 +115,10 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ onSignUp, onNavigateToSignI
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('auth.password')}</Text>
<Text style={styles.label}>Password</Text>
<TextInput
style={styles.input}
placeholder={t('auth.password')}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
@@ -129,10 +127,10 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ onSignUp, onNavigateToSignI
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>{t('auth.confirmPassword')}</Text>
<Text style={styles.label}>Confirm Password</Text>
<TextInput
style={styles.input}
placeholder={t('auth.confirmPassword')}
placeholder="Confirm Password"
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry
@@ -149,7 +147,7 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ onSignUp, onNavigateToSignI
{isLoading ? (
<ActivityIndicator color={KurdistanColors.spi} />
) : (
<Text style={styles.signUpButtonText}>{t('auth.signUp')}</Text>
<Text style={styles.signUpButtonText}>Sign Up</Text>
)}
</TouchableOpacity>
@@ -164,8 +162,8 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ onSignUp, onNavigateToSignI
onPress={onNavigateToSignIn}
>
<Text style={styles.signInPromptText}>
{t('auth.haveAccount')}{' '}
<Text style={styles.signInLink}>{t('auth.signIn')}</Text>
Already have an account?{' '}
<Text style={styles.signInLink}>Sign In</Text>
</Text>
</TouchableOpacity>
</View>
+35 -10
View File
@@ -42,7 +42,7 @@ const SCORE_WEIGHTS = {
};
export default function StakingScreen() {
const { api, selectedAccount, isApiReady } = usePezkuwi();
const { api, selectedAccount, isApiReady, getKeyPair } = usePezkuwi();
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
@@ -137,17 +137,20 @@ export default function StakingScreen() {
setProcessing(true);
if (!api || !selectedAccount) return;
// Get keypair for signing
const keyPair = await getKeyPair(selectedAccount.address);
if (!keyPair) {
Alert.alert('Error', 'Could not retrieve wallet keypair for signing');
return;
}
// Convert amount to planck
const amountPlanck = BigInt(Math.floor(parseFloat(stakeAmount) * 1e12));
// Bond tokens (or bond_extra if already bonding)
// For simplicity, using bond_extra if already bonded, otherwise bond
// But UI should handle controller/stash logic. Assuming simple setup.
// This part is simplified.
const tx = api.tx.staking.bondExtra(amountPlanck);
await tx.signAndSend(selectedAccount.address, ({ status }) => {
await tx.signAndSend(keyPair, ({ status }) => {
if (status.isInBlock) {
Alert.alert('Success', `Successfully staked ${stakeAmount} HEZ!`);
setStakeSheetVisible(false);
@@ -173,10 +176,17 @@ export default function StakingScreen() {
setProcessing(true);
if (!api || !selectedAccount) return;
// Get keypair for signing
const keyPair = await getKeyPair(selectedAccount.address);
if (!keyPair) {
Alert.alert('Error', 'Could not retrieve wallet keypair for signing');
return;
}
const amountPlanck = BigInt(Math.floor(parseFloat(unstakeAmount) * 1e12));
const tx = api.tx.staking.unbond(amountPlanck);
await tx.signAndSend(selectedAccount.address, ({ status }) => {
await tx.signAndSend(keyPair, ({ status }) => {
if (status.isInBlock) {
Alert.alert(
'Success',
@@ -200,10 +210,17 @@ export default function StakingScreen() {
setProcessing(true);
if (!api || !selectedAccount) return;
// Get keypair for signing
const keyPair = await getKeyPair(selectedAccount.address);
if (!keyPair) {
Alert.alert('Error', 'Could not retrieve wallet keypair for signing');
return;
}
// Withdraw all available unbonded funds
// num_slashing_spans is usually 0 for simple stakers
const tx = api.tx.staking.withdrawUnbonded(0);
await tx.signAndSend(selectedAccount.address, ({ status }) => {
await tx.signAndSend(keyPair, ({ status }) => {
if (status.isInBlock) {
Alert.alert('Success', 'Successfully withdrawn unbonded tokens!');
fetchStakingData();
@@ -226,8 +243,16 @@ export default function StakingScreen() {
setProcessing(true);
try {
// Get keypair for signing
const keyPair = await getKeyPair(selectedAccount.address);
if (!keyPair) {
Alert.alert('Error', 'Could not retrieve wallet keypair for signing');
setProcessing(false);
return;
}
const tx = api.tx.staking.nominate(validators);
await tx.signAndSend(selectedAccount.address, ({ status }) => {
await tx.signAndSend(keyPair, ({ status }) => {
if (status.isInBlock) {
Alert.alert('Success', 'Nomination transaction sent!');
setValidatorSheetVisible(false);
+233 -29
View File
@@ -13,8 +13,10 @@ import {
Platform,
Image,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { KurdistanColors } from '../theme/colors';
import { usePezkuwi } from '../contexts/PezkuwiContext';
import { KurdistanSun } from '../components/KurdistanSun';
// Token Images
const hezLogo = require('../../../shared/images/hez_logo.png');
@@ -30,14 +32,15 @@ interface TokenInfo {
}
const TOKENS: TokenInfo[] = [
{ symbol: 'HEZ', name: 'Hemuwelet', assetId: 0, decimals: 12, logo: hezLogo },
{ symbol: 'PEZ', name: 'Pezkunel', assetId: 1, decimals: 12, logo: pezLogo },
{ symbol: 'HEZ', name: 'Welati Coin', assetId: 0, decimals: 12, logo: hezLogo },
{ symbol: 'PEZ', name: 'Pezkuwichain Token', assetId: 1, decimals: 12, logo: pezLogo },
{ symbol: 'USDT', name: 'Tether USD', assetId: 1000, decimals: 6, logo: usdtLogo },
];
type TransactionStatus = 'idle' | 'signing' | 'submitting' | 'success' | 'error';
const SwapScreen: React.FC = () => {
const navigation = useNavigation<any>();
const { api, isApiReady, selectedAccount, getKeyPair } = usePezkuwi();
const [fromToken, setFromToken] = useState<TokenInfo>(TOKENS[0]);
@@ -49,6 +52,17 @@ const SwapScreen: React.FC = () => {
const [fromBalance, setFromBalance] = useState('0');
const [toBalance, setToBalance] = useState('0');
// Pool reserves for AMM calculation
const [poolReserves, setPoolReserves] = useState<{
reserve0: number;
reserve1: number;
asset0: number;
asset1: number;
} | null>(null);
const [exchangeRate, setExchangeRate] = useState(0);
const [isLoadingRate, setIsLoadingRate] = useState(false);
const [isDexAvailable, setIsDexAvailable] = useState(false);
const [txStatus, setTxStatus] = useState<TransactionStatus>('idle');
const [errorMessage, setErrorMessage] = useState('');
@@ -93,18 +107,184 @@ const SwapScreen: React.FC = () => {
fetchBalances();
}, [api, isApiReady, selectedAccount, fromToken, toToken]);
// Calculate output amount (simple 1:1 for now - should use pool reserves)
// Check if AssetConversion pallet is available
useEffect(() => {
if (api && isApiReady) {
const hasAssetConversion = api.tx.assetConversion !== undefined;
setIsDexAvailable(hasAssetConversion);
if (__DEV__ && !hasAssetConversion) {
console.warn('AssetConversion pallet not available in runtime');
}
}
}, [api, isApiReady]);
// Fetch exchange rate from AssetConversion pool
useEffect(() => {
const fetchExchangeRate = async () => {
if (!api || !isApiReady || !isDexAvailable) {
return;
}
setIsLoadingRate(true);
try {
// Map user-selected tokens to actual pool assets
// HEZ → wHEZ (Asset 0) behind the scenes
const getPoolAssetId = (token: TokenInfo) => {
if (token.symbol === 'HEZ') return 0; // wHEZ
return token.assetId;
};
const fromAssetId = getPoolAssetId(fromToken);
const toAssetId = getPoolAssetId(toToken);
// Pool ID must be sorted (smaller asset ID first)
const [asset1, asset2] = fromAssetId < toAssetId
? [fromAssetId, toAssetId]
: [toAssetId, fromAssetId];
// Create pool asset tuple [asset1, asset2] - must be sorted!
const poolAssets = [
{ NativeOrAsset: { Asset: asset1 } },
{ NativeOrAsset: { Asset: asset2 } }
];
// Query pool from AssetConversion pallet
const poolInfo = await api.query.assetConversion.pools(poolAssets);
if (poolInfo && !poolInfo.isEmpty) {
try {
// Derive pool account using AccountIdConverter
// blake2_256(&Encode::encode(&(PalletId, PoolId))[..])
const { stringToU8a } = await import('@pezkuwi/util');
const { blake2AsU8a } = await import('@pezkuwi/util-crypto');
// PalletId for AssetConversion: "py/ascon" (8 bytes)
const PALLET_ID = stringToU8a('py/ascon');
// Create PoolId tuple (u32, u32)
const poolId = api.createType('(u32, u32)', [asset1, asset2]);
// Create (PalletId, PoolId) tuple: ([u8; 8], (u32, u32))
const palletIdType = api.createType('[u8; 8]', PALLET_ID);
const fullTuple = api.createType('([u8; 8], (u32, u32))', [palletIdType, poolId]);
// Hash the SCALE-encoded tuple
const accountHash = blake2AsU8a(fullTuple.toU8a(), 256);
const poolAccountId = api.createType('AccountId32', accountHash);
// Query pool account's asset balances
const reserve0Query = await api.query.assets.account(asset1, poolAccountId);
const reserve1Query = await api.query.assets.account(asset2, poolAccountId);
const reserve0Data = reserve0Query.toJSON() as { balance?: string } | null;
const reserve1Data = reserve1Query.toJSON() as { balance?: string } | null;
if (reserve0Data?.balance && reserve1Data?.balance) {
// Parse hex string balances to BigInt, then to number
const balance0Hex = reserve0Data.balance.toString();
const balance1Hex = reserve1Data.balance.toString();
// Use correct decimals for each asset
const decimals0 = asset1 === 1000 ? 6 : 12;
const decimals1 = asset2 === 1000 ? 6 : 12;
const reserve0 = Number(BigInt(balance0Hex)) / (10 ** decimals0);
const reserve1 = Number(BigInt(balance1Hex)) / (10 ** decimals1);
if (__DEV__) {
console.log('Pool reserves found:', { reserve0, reserve1, asset1, asset2 });
}
// Store pool reserves for AMM calculation
setPoolReserves({
reserve0,
reserve1,
asset0: asset1,
asset1: asset2
});
// Calculate simple exchange rate for display
const rate = fromAssetId === asset1
? reserve1 / reserve0 // from asset1 to asset2
: reserve0 / reserve1; // from asset2 to asset1
setExchangeRate(rate);
} else {
if (__DEV__) console.warn('Pool has no reserves');
setExchangeRate(0);
setPoolReserves(null);
}
} catch (err) {
if (__DEV__) console.error('Error deriving pool account:', err);
setExchangeRate(0);
setPoolReserves(null);
}
} else {
if (__DEV__) console.warn('No liquidity pool found for this pair');
setExchangeRate(0);
setPoolReserves(null);
}
} catch (error) {
if (__DEV__) console.error('Failed to fetch exchange rate:', error);
setExchangeRate(0);
setPoolReserves(null);
} finally {
setIsLoadingRate(false);
}
};
fetchExchangeRate();
}, [api, isApiReady, isDexAvailable, fromToken, toToken]);
// Calculate output amount using Uniswap V2 AMM formula
useEffect(() => {
if (!fromAmount || parseFloat(fromAmount) <= 0) {
setToAmount('');
return;
}
// TODO: Implement proper AMM calculation using pool reserves
// For now, simple 1:1 conversion (placeholder)
const calculatedAmount = (parseFloat(fromAmount) * 0.97).toFixed(6); // 3% fee simulation
setToAmount(calculatedAmount);
}, [fromAmount, fromToken, toToken]);
// If no pool reserves available, cannot calculate
if (!poolReserves) {
setToAmount('');
return;
}
const amountIn = parseFloat(fromAmount);
const { reserve0, reserve1, asset0 } = poolReserves;
// Determine which reserve is input and which is output
const getPoolAssetId = (token: TokenInfo) => {
if (token.symbol === 'HEZ') return 0; // wHEZ
return token.assetId;
};
const fromAssetId = getPoolAssetId(fromToken);
const isAsset0ToAsset1 = fromAssetId === asset0;
const reserveIn = isAsset0ToAsset1 ? reserve0 : reserve1;
const reserveOut = isAsset0ToAsset1 ? reserve1 : reserve0;
// Uniswap V2 AMM formula (matches Substrate runtime exactly)
// Runtime: amount_in_with_fee = amount_in * (1000 - LPFee) = amount_in * 970
// LPFee = 30 (3% fee)
// Formula: amountOut = (amountIn * 970 * reserveOut) / (reserveIn * 1000 + amountIn * 970)
const LP_FEE = 30; // 3% fee
const amountInWithFee = amountIn * (1000 - LP_FEE);
const numerator = amountInWithFee * reserveOut;
const denominator = reserveIn * 1000 + amountInWithFee;
const amountOut = numerator / denominator;
if (__DEV__) {
console.log('AMM calculation:', {
amountIn,
reserveIn,
reserveOut,
amountOut,
lpFee: `${LP_FEE / 10}%`
});
}
setToAmount(amountOut.toFixed(6));
}, [fromAmount, fromToken, toToken, poolReserves]);
// Calculate formatted balances
const fromBalanceFormatted = useMemo(() => {
@@ -161,6 +341,11 @@ const SwapScreen: React.FC = () => {
return;
}
if (!exchangeRate || exchangeRate === 0) {
Alert.alert('Error', 'No liquidity pool available for this pair');
return;
}
setTxStatus('signing');
setShowConfirm(false);
setErrorMessage('');
@@ -276,21 +461,22 @@ const SwapScreen: React.FC = () => {
return (
<SafeAreaView style={styles.container}>
{/* Transaction Loading Overlay */}
{/* Kurdistan Sun Loading Overlay */}
{(txStatus === 'signing' || txStatus === 'submitting') && (
<View style={styles.loadingOverlay}>
<View style={styles.loadingCard}>
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
<Text style={styles.loadingText}>
{txStatus === 'signing' ? 'Waiting for signature...' : 'Processing swap...'}
</Text>
</View>
<KurdistanSun size={250} />
<Text style={styles.loadingText}>
{txStatus === 'signing' ? 'Waiting for signature...' : 'Processing your swap...'}
</Text>
</View>
)}
<ScrollView style={styles.scrollContent} contentContainerStyle={styles.scrollContentContainer}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
<Text style={styles.backButtonText}></Text>
</TouchableOpacity>
<Text style={styles.headerTitle}>Swap Tokens</Text>
<TouchableOpacity onPress={() => setShowSettings(true)} style={styles.settingsButton}>
<Text style={styles.settingsIcon}></Text>
@@ -369,7 +555,13 @@ const SwapScreen: React.FC = () => {
<View style={styles.detailsCard}>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}> Exchange Rate</Text>
<Text style={styles.detailValue}>1 {fromToken.symbol} 1 {toToken.symbol}</Text>
<Text style={styles.detailValue}>
{isLoadingRate
? 'Loading...'
: exchangeRate > 0
? `1 ${fromToken.symbol}${exchangeRate.toFixed(4)} ${toToken.symbol}`
: 'No pool available'}
</Text>
</View>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Slippage Tolerance</Text>
@@ -389,14 +581,16 @@ const SwapScreen: React.FC = () => {
<TouchableOpacity
style={[
styles.swapButton,
(!fromAmount || hasInsufficientBalance || txStatus !== 'idle') && styles.swapButtonDisabled
(!fromAmount || hasInsufficientBalance || txStatus !== 'idle' || exchangeRate === 0) && styles.swapButtonDisabled
]}
onPress={() => setShowConfirm(true)}
disabled={!fromAmount || hasInsufficientBalance || txStatus !== 'idle'}
disabled={!fromAmount || hasInsufficientBalance || txStatus !== 'idle' || exchangeRate === 0}
>
<Text style={styles.swapButtonText}>
{hasInsufficientBalance
? `Insufficient ${fromToken.symbol} Balance`
: exchangeRate === 0
? 'No Pool Available'
: 'Swap Tokens'}
</Text>
</TouchableOpacity>
@@ -468,6 +662,10 @@ const SwapScreen: React.FC = () => {
<Text style={styles.confirmValue}>{toAmount} {toToken.symbol}</Text>
</View>
<View style={[styles.confirmRow, styles.confirmRowBorder]}>
<Text style={styles.confirmLabelSmall}>Exchange Rate</Text>
<Text style={styles.confirmValueSmall}>1 {fromToken.symbol} = {exchangeRate.toFixed(4)} {toToken.symbol}</Text>
</View>
<View style={styles.confirmRow}>
<Text style={styles.confirmLabelSmall}>Slippage</Text>
<Text style={styles.confirmValueSmall}>{slippage}%</Text>
</View>
@@ -519,8 +717,20 @@ const styles = StyleSheet.create({
alignItems: 'center',
marginBottom: 20,
},
headerTitle: {
backButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#F5F5F5',
justifyContent: 'center',
alignItems: 'center',
},
backButtonText: {
fontSize: 24,
color: '#333',
},
headerTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#333',
},
@@ -693,22 +903,16 @@ const styles = StyleSheet.create({
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.7)',
backgroundColor: 'rgba(0,0,0,0.85)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
},
loadingCard: {
backgroundColor: '#FFFFFF',
borderRadius: 20,
padding: 32,
alignItems: 'center',
gap: 16,
},
loadingText: {
fontSize: 16,
marginTop: 24,
fontSize: 18,
fontWeight: '600',
color: '#333',
color: '#FFFFFF',
},
modalOverlay: {
flex: 1,
+6 -8
View File
@@ -9,7 +9,6 @@ import {
Animated,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import { KurdistanColors } from '../theme/colors';
import AsyncStorage from '@react-native-async-storage/async-storage';
@@ -20,7 +19,6 @@ interface VerifyHumanScreenProps {
}
const VerifyHumanScreen: React.FC<VerifyHumanScreenProps> = ({ onVerified }) => {
const { t } = useTranslation();
const [isChecked, setIsChecked] = useState(false);
const [scaleValue] = useState(new Animated.Value(1));
@@ -71,9 +69,9 @@ const VerifyHumanScreen: React.FC<VerifyHumanScreenProps> = ({ onVerified }) =>
</View>
{/* Title */}
<Text style={styles.title}>{t('verify.title', 'Security Verification')}</Text>
<Text style={styles.title}>Security Verification</Text>
<Text style={styles.subtitle}>
{t('verify.subtitle', 'Please confirm you are human to continue')}
Please confirm you are human to continue
</Text>
{/* Verification Box */}
@@ -86,13 +84,13 @@ const VerifyHumanScreen: React.FC<VerifyHumanScreenProps> = ({ onVerified }) =>
{isChecked && <Text style={styles.checkmark}></Text>}
</View>
<Text style={styles.verificationText}>
{t('verify.checkbox', "I'm not a robot")}
I'm not a robot
</Text>
</TouchableOpacity>
{/* Info Text */}
<Text style={styles.infoText}>
{t('verify.info', 'This helps protect the Pezkuwi network from automated attacks')}
This helps protect the Pezkuwi network from automated attacks
</Text>
{/* Continue Button */}
@@ -109,7 +107,7 @@ const VerifyHumanScreen: React.FC<VerifyHumanScreenProps> = ({ onVerified }) =>
!isChecked && styles.continueButtonTextDisabled,
]}
>
{t('verify.continue', 'Continue')}
Continue
</Text>
</Animated.View>
</TouchableOpacity>
@@ -117,7 +115,7 @@ const VerifyHumanScreen: React.FC<VerifyHumanScreenProps> = ({ onVerified }) =>
{/* Footer */}
<View style={styles.footer}>
<Text style={styles.footerText}>
🔒 {t('verify.secure', 'Secure & Private')}
Secure & Private
</Text>
</View>
</View>
+425 -159
View File
@@ -18,12 +18,44 @@ import {
Share,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import QRCode from 'react-native-qrcode-svg';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as SecureStore from 'expo-secure-store';
import { KurdistanColors } from '../theme/colors';
import { usePezkuwi, NetworkType, NETWORKS } from '../contexts/PezkuwiContext';
import { AddTokenModal } from '../components/wallet/AddTokenModal';
import { HezTokenLogo, PezTokenLogo } from '../components/icons';
// Secure storage helper - same as in PezkuwiContext
const secureStorage = {
getItem: async (key: string): Promise<string | null> => {
if (Platform.OS === 'web') {
return await AsyncStorage.getItem(key);
} else {
return await SecureStore.getItemAsync(key);
}
},
};
// Cross-platform alert helper
const showAlert = (title: string, message: string, buttons?: Array<{text: string; onPress?: () => void; style?: string}>) => {
if (Platform.OS === 'web') {
if (buttons && buttons.length > 1) {
const result = window.confirm(`${title}\n\n${message}`);
if (result && buttons[1]?.onPress) {
buttons[1].onPress();
} else if (!result && buttons[0]?.onPress) {
buttons[0].onPress();
}
} else {
window.alert(`${title}\n\n${message}`);
if (buttons?.[0]?.onPress) buttons[0].onPress();
}
} else {
showAlert(title, message, buttons as any);
}
};
// Token Images - From shared/images
const hezLogo = require('../../../shared/images/hez_logo.png');
@@ -59,16 +91,17 @@ interface Transaction {
}
const WalletScreen: React.FC = () => {
const { t } = useTranslation();
const navigation = useNavigation<any>();
const {
api,
isApiReady,
accounts,
selectedAccount,
setSelectedAccount,
connectWallet,
disconnectWallet,
createWallet,
deleteWallet,
getKeyPair,
currentNetwork,
switchNetwork,
@@ -82,6 +115,7 @@ const WalletScreen: React.FC = () => {
const [importWalletModalVisible, setImportWalletModalVisible] = useState(false);
const [backupModalVisible, setBackupModalVisible] = useState(false);
const [networkSelectorVisible, setNetworkSelectorVisible] = useState(false);
const [walletSelectorVisible, setWalletSelectorVisible] = useState(false);
const [addTokenModalVisible, setAddTokenModalVisible] = useState(false);
const [recipientAddress, setRecipientAddress] = useState('');
const [sendAmount, setSendAmount] = useState('');
@@ -225,7 +259,7 @@ const WalletScreen: React.FC = () => {
const handleConfirmSend = async () => {
if (!recipientAddress || !sendAmount || !selectedToken || !selectedAccount || !api) {
Alert.alert('Error', 'Please enter recipient address and amount');
showAlert('Error', 'Please enter recipient address and amount');
return;
}
@@ -251,13 +285,13 @@ const WalletScreen: React.FC = () => {
if (status.isFinalized) {
setSendModalVisible(false);
setIsSending(false);
Alert.alert('Success', 'Transaction Sent!');
showAlert('Success', 'Transaction Sent!');
fetchData();
}
});
} catch (e: any) {
setIsSending(false);
Alert.alert('Error', e.message);
showAlert('Error', e.message);
}
};
@@ -272,21 +306,21 @@ const WalletScreen: React.FC = () => {
const { address, mnemonic } = await createWallet(walletName);
setUserMnemonic(mnemonic); // Save for backup
setCreateWalletModalVisible(false);
Alert.alert('Wallet Created', `Save this mnemonic:\n${mnemonic}`, [{ text: 'OK', onPress: () => connectWallet() }]);
} catch (e) { Alert.alert('Error', 'Failed'); }
showAlert('Wallet Created', `Save this mnemonic:\n${mnemonic}`, [{ text: 'OK', onPress: () => connectWallet() }]);
} catch (e) { showAlert('Error', 'Failed'); }
};
// Copy Address Handler
const handleCopyAddress = () => {
if (!selectedAccount) return;
Clipboard.setString(selectedAccount.address);
Alert.alert('Copied!', 'Address copied to clipboard');
showAlert('Copied!', 'Address copied to clipboard');
};
// Import Wallet Handler
const handleImportWallet = async () => {
if (!importMnemonic.trim()) {
Alert.alert('Error', 'Please enter a valid mnemonic');
showAlert('Error', 'Please enter a valid mnemonic');
return;
}
try {
@@ -297,23 +331,36 @@ const WalletScreen: React.FC = () => {
const pair = keyring.addFromMnemonic(importMnemonic.trim());
// Store in AsyncStorage (via context method ideally)
Alert.alert('Success', `Wallet imported: ${pair.address.slice(0,8)}...`);
showAlert('Success', `Wallet imported: ${pair.address.slice(0,8)}...`);
setImportWalletModalVisible(false);
setImportMnemonic('');
connectWallet();
} catch (e: any) {
Alert.alert('Error', e.message || 'Invalid mnemonic');
showAlert('Error', e.message || 'Invalid mnemonic');
}
};
// Backup Mnemonic Handler
const handleBackupMnemonic = async () => {
// Retrieve mnemonic from secure storage
// For demo, we show the saved one or prompt user
if (userMnemonic) {
setBackupModalVisible(true);
} else {
Alert.alert('No Backup', 'Mnemonic not available. Create a new wallet or import existing one.');
if (!selectedAccount) {
showAlert('No Wallet', 'Please create or import a wallet first.');
return;
}
try {
// Retrieve mnemonic from secure storage
const seedKey = `pezkuwi_seed_${selectedAccount.address}`;
const storedMnemonic = await secureStorage.getItem(seedKey);
if (storedMnemonic) {
setUserMnemonic(storedMnemonic);
setBackupModalVisible(true);
} else {
showAlert('No Backup', 'Mnemonic not found in secure storage. It may have been imported from another device.');
}
} catch (error) {
console.error('Error retrieving mnemonic:', error);
showAlert('Error', 'Failed to retrieve mnemonic from secure storage.');
}
};
@@ -334,156 +381,110 @@ const WalletScreen: React.FC = () => {
try {
await switchNetwork(network);
setNetworkSelectorVisible(false);
Alert.alert('Success', `Switched to ${NETWORKS[network].displayName}`);
showAlert('Success', `Switched to ${NETWORKS[network].displayName}`);
} catch (e: any) {
Alert.alert('Error', e.message || 'Failed to switch network');
showAlert('Error', e.message || 'Failed to switch network');
}
};
if (!selectedAccount) {
return (
<SafeAreaView style={styles.container}>
<LinearGradient
colors={['#00693E', '#008f43', '#00A651']}
style={styles.welcomeGradient}
>
<View style={styles.welcomeContent}>
<Text style={styles.welcomeEmoji}>🔐</Text>
<Text style={styles.welcomeTitle}>Welcome to</Text>
<Text style={styles.welcomeBrand}>Pezkuwichain Wallet</Text>
<Text style={styles.welcomeSubtitle}>
Secure, Fast & Decentralized
</Text>
</View>
</LinearGradient>
// Redirect to WalletSetupScreen if no wallet exists
useEffect(() => {
if (!selectedAccount && accounts.length === 0) {
navigation.replace('WalletSetup');
}
}, [selectedAccount, accounts, navigation]);
<View style={styles.buttonContainer}>
<TouchableOpacity
style={styles.primaryWalletButton}
onPress={handleConnectWallet}
activeOpacity={0.8}
>
<LinearGradient
colors={[KurdistanColors.kesk, '#00A651']}
style={styles.buttonGradient}
start={{x: 0, y: 0}}
end={{x: 1, y: 0}}
>
<View style={styles.buttonIcon}>
<Text style={styles.buttonIconText}></Text>
</View>
<View style={styles.buttonTextContainer}>
<Text style={styles.primaryButtonText}>Create New Wallet</Text>
<Text style={styles.primaryButtonSubtext}>
Get started in seconds
</Text>
</View>
</LinearGradient>
</TouchableOpacity>
<TouchableOpacity
style={styles.secondaryWalletButton}
onPress={() => setImportWalletModalVisible(true)}
activeOpacity={0.8}
>
<View style={styles.secondaryButtonContent}>
<View style={[styles.buttonIcon, {backgroundColor: 'rgba(0,105,62,0.1)'}]}>
<Text style={[styles.buttonIconText, {color: KurdistanColors.kesk}]}>📥</Text>
</View>
<View style={styles.buttonTextContainer}>
<Text style={styles.secondaryButtonText}>Import Existing Wallet</Text>
<Text style={styles.secondaryButtonSubtext}>
Use your seed phrase
</Text>
</View>
</View>
</TouchableOpacity>
<View style={styles.securityNotice}>
<Text style={styles.securityIcon}>🛡</Text>
<Text style={styles.securityText}>
Your keys are encrypted and stored locally on your device
</Text>
</View>
</View>
{/* Create Wallet Modal */}
<Modal visible={createWalletModalVisible} transparent animationType="slide" onRequestClose={() => setCreateWalletModalVisible(false)}>
<View style={styles.modalOverlay}>
<View style={styles.modalCard}>
<Text style={styles.modalHeader}>Create New Wallet</Text>
<TextInput style={styles.inputField} placeholder="Wallet Name" value={walletName} onChangeText={setWalletName} />
<View style={styles.modalActions}>
<TouchableOpacity style={styles.btnCancel} onPress={() => setCreateWalletModalVisible(false)}><Text>Cancel</Text></TouchableOpacity>
<TouchableOpacity style={styles.btnConfirm} onPress={handleCreateWallet}><Text style={{color:'white'}}>Create</Text></TouchableOpacity>
</View>
</View>
</View>
</Modal>
{/* Import Wallet Modal */}
<Modal visible={importWalletModalVisible} transparent animationType="slide" onRequestClose={() => setImportWalletModalVisible(false)}>
<View style={styles.modalOverlay}>
<View style={styles.modalCard}>
<Text style={styles.modalHeader}>Import Wallet</Text>
<Text style={{color: '#666', fontSize: 12, marginBottom: 12}}>Enter your 12 or 24 word mnemonic phrase</Text>
<TextInput
style={[styles.inputField, {height: 100, textAlignVertical: 'top'}]}
placeholder="word1 word2 word3..."
multiline
value={importMnemonic}
onChangeText={setImportMnemonic}
/>
<View style={styles.modalActions}>
<TouchableOpacity style={styles.btnCancel} onPress={() => setImportWalletModalVisible(false)}><Text>Cancel</Text></TouchableOpacity>
<TouchableOpacity style={styles.btnConfirm} onPress={handleImportWallet}><Text style={{color:'white'}}>Import</Text></TouchableOpacity>
</View>
</View>
</View>
</Modal>
</SafeAreaView>
);
// Show loading while checking wallet state or redirecting
if (!selectedAccount && accounts.length === 0) {
return (
<SafeAreaView style={styles.container} testID="wallet-redirecting">
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
<Text style={styles.loadingText}>Loading wallet...</Text>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<SafeAreaView style={styles.container} testID="wallet-screen">
<StatusBar barStyle="dark-content" />
{/* Top Header with Back Button */}
<View style={styles.topHeader} testID="wallet-top-header">
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton} testID="wallet-back-button">
<Text style={styles.backButtonText}> Back</Text>
</TouchableOpacity>
<Text style={styles.topHeaderTitle}>Wallet</Text>
<TouchableOpacity onPress={() => setNetworkSelectorVisible(true)} testID="wallet-network-button">
<Text style={styles.networkBadge}>🌐 {NETWORKS[currentNetwork].displayName}</Text>
</TouchableOpacity>
</View>
<ScrollView
style={styles.scrollContent}
refreshControl={<RefreshControl refreshing={isLoadingBalances} onRefresh={fetchData} />}
showsVerticalScrollIndicator={false}
testID="wallet-scroll-view"
>
{/* Header */}
<View style={styles.headerContainer}>
<Text style={styles.walletTitle}>pezkuwi wallet</Text>
<TouchableOpacity onPress={() => setNetworkSelectorVisible(true)}>
<Text style={styles.networkBadge}>🌐 {NETWORKS[currentNetwork].displayName}</Text>
{/* Wallet Selector Row */}
<View style={styles.walletSelectorRow}>
<TouchableOpacity
style={styles.walletSelector}
onPress={() => setWalletSelectorVisible(true)}
testID="wallet-selector-button"
>
<View style={styles.walletSelectorInfo}>
<Text style={styles.walletSelectorName}>{selectedAccount?.name || 'Wallet'}</Text>
<Text style={styles.walletSelectorAddress} numberOfLines={1}>
{selectedAccount?.address ? `${selectedAccount.address.slice(0, 8)}...${selectedAccount.address.slice(-6)}` : ''}
</Text>
</View>
<Text style={styles.walletSelectorArrow}></Text>
</TouchableOpacity>
<View style={styles.walletHeaderButtons}>
<TouchableOpacity
style={styles.addWalletButton}
onPress={() => navigation.navigate('WalletSetup')}
testID="add-wallet-button"
>
<Text style={styles.addWalletIcon}>+</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.scanButton}
onPress={() => showAlert('Scan', 'QR Scanner coming soon')}
testID="wallet-scan-button"
>
<Text style={styles.scanIcon}></Text>
</TouchableOpacity>
</View>
</View>
{/* Main Token Cards - HEZ and PEZ side by side */}
<View style={styles.mainTokensRow}>
{/* HEZ Card */}
<TouchableOpacity style={styles.mainTokenCard} onPress={() => handleTokenPress(tokens[0])}>
<Image source={hezLogo} style={styles.mainTokenLogo} resizeMode="contain" />
<View style={styles.mainTokenLogoContainer}>
<HezTokenLogo size={56} />
</View>
<Text style={styles.mainTokenSymbol}>HEZ</Text>
<Text style={styles.mainTokenBalance}>{balances.HEZ}</Text>
<Text style={styles.mainTokenSubtitle}>Hemuwelet Token</Text>
<Text style={styles.mainTokenSubtitle}>Welati Coin</Text>
</TouchableOpacity>
{/* PEZ Card */}
<TouchableOpacity style={styles.mainTokenCard} onPress={() => handleTokenPress(tokens[1])}>
<Image source={pezLogo} style={styles.mainTokenLogo} resizeMode="contain" />
<View style={styles.mainTokenLogoContainer}>
<PezTokenLogo size={56} />
</View>
<Text style={styles.mainTokenSymbol}>PEZ</Text>
<Text style={styles.mainTokenBalance}>{balances.PEZ}</Text>
<Text style={styles.mainTokenSubtitle}>Pezkunel Token</Text>
<Text style={styles.mainTokenSubtitle}>Pezkuwichain Token</Text>
</TouchableOpacity>
</View>
{/* Action Buttons Grid - 2x4 */}
{/* Action Buttons Grid - 1x4 */}
<View style={styles.actionsGrid}>
{/* Row 1 */}
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#22C55E'}]} onPress={handleSend}>
<Text style={styles.actionIcon}></Text>
<Text style={styles.actionLabel}>Send</Text>
@@ -494,36 +495,15 @@ const WalletScreen: React.FC = () => {
<Text style={styles.actionLabel}>Receive</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#A855F7'}]} onPress={() => Alert.alert('Scan', 'QR Scanner coming soon')}>
<Text style={styles.actionIcon}></Text>
<Text style={styles.actionLabel}>Scan</Text>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#6B7280'}]} onPress={() => navigation.navigate('Swap')}>
<Text style={styles.actionIcon}>🔄</Text>
<Text style={styles.actionLabel}>Swap</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#6B7280'}]} onPress={() => Alert.alert('P2P', 'Navigate to P2P Platform')}>
<Text style={styles.actionIcon}>👥</Text>
<Text style={styles.actionLabel}>P2P</Text>
</TouchableOpacity>
{/* Row 2 */}
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#EF4444'}]} onPress={() => Alert.alert('Vote', 'Navigate to Governance')}>
<Text style={styles.actionIcon}>🗳</Text>
<Text style={styles.actionLabel}>Vote</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#F59E0B'}]} onPress={() => Alert.alert('Dapps', 'Navigate to Apps')}>
<Text style={styles.actionIcon}></Text>
<Text style={styles.actionLabel}>Dapps</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#10B981'}]} onPress={() => Alert.alert('Staking', 'Navigate to Staking')}>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#10B981'}]} onPress={() => showAlert('Staking', 'Navigate to Staking')}>
<Text style={styles.actionIcon}>🥩</Text>
<Text style={styles.actionLabel}>Staking</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, {backgroundColor: '#8B5CF6'}]} onPress={() => setNetworkSelectorVisible(true)}>
<Text style={styles.actionIcon}>🔗</Text>
<Text style={styles.actionLabel}>Connect</Text>
</TouchableOpacity>
</View>
{/* Tokens List */}
@@ -630,7 +610,7 @@ const WalletScreen: React.FC = () => {
<View style={styles.modalActions}>
<TouchableOpacity style={styles.btnCancel} onPress={() => {
Clipboard.setString(userMnemonic);
Alert.alert('Copied', 'Mnemonic copied to clipboard');
showAlert('Copied', 'Mnemonic copied to clipboard');
}}>
<Text>📋 Copy</Text>
</TouchableOpacity>
@@ -642,6 +622,103 @@ const WalletScreen: React.FC = () => {
</View>
</Modal>
{/* Wallet Selector Modal */}
<Modal visible={walletSelectorVisible} transparent animationType="slide" onRequestClose={() => setWalletSelectorVisible(false)}>
<View style={styles.modalOverlay}>
<View style={styles.modalCard}>
<Text style={styles.modalHeader}>👛 My Wallets</Text>
<Text style={{color: '#666', fontSize: 12, marginBottom: 16, textAlign: 'center'}}>
Select a wallet or create a new one
</Text>
{/* Wallet List */}
{accounts.map((account) => {
const isSelected = account.address === selectedAccount?.address;
return (
<View key={account.address} style={styles.walletOptionRow}>
<TouchableOpacity
style={[
styles.walletOption,
isSelected && styles.walletOptionSelected,
{flex: 1, marginBottom: 0}
]}
onPress={() => {
setSelectedAccount(account);
setWalletSelectorVisible(false);
}}
>
<View style={styles.walletOptionIcon}>
<Text style={{fontSize: 24}}>👛</Text>
</View>
<View style={{flex: 1}}>
<Text style={[styles.walletOptionName, isSelected && {color: KurdistanColors.kesk}]}>
{account.name}
</Text>
<Text style={styles.walletOptionAddress} numberOfLines={1}>
{account.address.slice(0, 12)}...{account.address.slice(-8)}
</Text>
</View>
{isSelected && <Text style={{fontSize: 20, color: KurdistanColors.kesk}}></Text>}
</TouchableOpacity>
<TouchableOpacity
style={styles.deleteWalletButton}
onPress={async () => {
const confirmDelete = Platform.OS === 'web'
? window.confirm(`Delete "${account.name}"?\n\nThis action cannot be undone. Make sure you have backed up your recovery phrase.`)
: await new Promise<boolean>((resolve) => {
Alert.alert(
'Delete Wallet',
`Are you sure you want to delete "${account.name}"?\n\nThis action cannot be undone. Make sure you have backed up your recovery phrase.`,
[
{ text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
{ text: 'Delete', style: 'destructive', onPress: () => resolve(true) }
]
);
});
if (confirmDelete) {
try {
await deleteWallet(account.address);
if (accounts.length <= 1) {
setWalletSelectorVisible(false);
}
} catch (err) {
if (Platform.OS === 'web') {
window.alert('Failed to delete wallet');
} else {
Alert.alert('Error', 'Failed to delete wallet');
}
}
}
}}
>
<Text style={styles.deleteWalletIcon}>🗑</Text>
</TouchableOpacity>
</View>
);
})}
{/* Add New Wallet Button */}
<TouchableOpacity
style={styles.addNewWalletOption}
onPress={() => {
setWalletSelectorVisible(false);
navigation.navigate('WalletSetup');
}}
>
<View style={styles.addNewWalletIcon}>
<Text style={{fontSize: 24, color: KurdistanColors.kesk}}>+</Text>
</View>
<Text style={styles.addNewWalletText}>Add New Wallet</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btnConfirm} onPress={() => setWalletSelectorVisible(false)}>
<Text style={{color:'white'}}>Close</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
{/* Network Selector Modal */}
<Modal visible={networkSelectorVisible} transparent animationType="slide" onRequestClose={() => setNetworkSelectorVisible(false)}>
<View style={styles.modalOverlay}>
@@ -700,6 +777,42 @@ const styles = StyleSheet.create({
scrollContent: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 16,
fontSize: 16,
color: '#666',
},
// Top Header with Back Button
topHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: '#FFFFFF',
borderBottomWidth: 1,
borderBottomColor: '#E5E5E5',
},
backButton: {
paddingVertical: 4,
paddingRight: 8,
},
backButtonText: {
fontSize: 16,
color: KurdistanColors.kesk,
fontWeight: '500',
},
topHeaderTitle: {
fontSize: 18,
fontWeight: '600',
color: '#333',
},
// Header Styles (New Design)
headerContainer: {
@@ -725,6 +838,18 @@ const styles = StyleSheet.create({
borderRadius: 16,
overflow: 'hidden',
},
scanButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#F5F5F5',
justifyContent: 'center',
alignItems: 'center',
},
scanIcon: {
fontSize: 20,
color: '#333',
},
// Main Token Cards (HEZ & PEZ) - New Design
mainTokensRow: {
@@ -747,6 +872,13 @@ const styles = StyleSheet.create({
height: 56,
marginBottom: 12,
},
mainTokenLogoContainer: {
width: 56,
height: 56,
marginBottom: 12,
justifyContent: 'center',
alignItems: 'center',
},
mainTokenSymbol: {
fontSize: 18,
fontWeight: 'bold',
@@ -1071,6 +1203,140 @@ const styles = StyleSheet.create({
color: '#666',
lineHeight: 18,
},
// Wallet Selector Row
walletSelectorRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
marginBottom: 8,
},
walletSelector: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFFFFF',
borderRadius: 12,
padding: 12,
marginRight: 12,
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.05)',
elevation: 2,
},
walletSelectorInfo: {
flex: 1,
},
walletSelectorName: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.reş,
},
walletSelectorAddress: {
fontSize: 12,
color: '#999',
marginTop: 2,
},
walletSelectorArrow: {
fontSize: 12,
color: '#666',
marginLeft: 8,
},
walletHeaderButtons: {
flexDirection: 'row',
gap: 8,
},
addWalletButton: {
width: 44,
height: 44,
borderRadius: 12,
backgroundColor: '#FFFFFF',
justifyContent: 'center',
alignItems: 'center',
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.05)',
elevation: 2,
},
addWalletIcon: {
fontSize: 24,
color: KurdistanColors.kesk,
fontWeight: '300',
},
// Wallet Selector Modal
walletOption: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
backgroundColor: '#F8F9FA',
borderRadius: 12,
marginBottom: 8,
borderWidth: 2,
borderColor: 'transparent',
},
walletOptionSelected: {
borderColor: KurdistanColors.kesk,
backgroundColor: 'rgba(0, 143, 67, 0.05)',
},
walletOptionIcon: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: '#FFFFFF',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
walletOptionName: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.reş,
},
walletOptionAddress: {
fontSize: 12,
color: '#999',
marginTop: 2,
},
addNewWalletOption: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
backgroundColor: 'rgba(0, 143, 67, 0.05)',
borderRadius: 12,
marginBottom: 16,
borderWidth: 2,
borderColor: KurdistanColors.kesk,
borderStyle: 'dashed',
},
addNewWalletIcon: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: '#FFFFFF',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
addNewWalletText: {
fontSize: 16,
fontWeight: '600',
color: KurdistanColors.kesk,
},
// Delete wallet
walletOptionRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
gap: 8,
},
deleteWalletButton: {
width: 44,
height: 44,
borderRadius: 12,
backgroundColor: 'rgba(239, 68, 68, 0.1)',
justifyContent: 'center',
alignItems: 'center',
},
deleteWalletIcon: {
fontSize: 18,
},
});
export default WalletScreen;
+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;
-2
View File
@@ -10,7 +10,6 @@ import {
Image,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { KurdistanColors } from '../theme/colors';
import PrivacyPolicyModal from '../components/PrivacyPolicyModal';
@@ -21,7 +20,6 @@ interface WelcomeScreenProps {
}
const WelcomeScreen: React.FC<WelcomeScreenProps> = ({ onContinue }) => {
const { t } = useTranslation();
const [agreed, setAgreed] = useState(false);
const [privacyModalVisible, setPrivacyModalVisible] = useState(false);
const [termsModalVisible, setTermsModalVisible] = useState(false);
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import BeCitizenScreen from '../BeCitizenScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const BeCitizenScreenWrapper = () => (
<LanguageProvider>
<PezkuwiProvider>
<BeCitizenScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<BeCitizenScreen />
</PezkuwiProvider>
);
describe('BeCitizenScreen', () => {
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import EducationScreen from '../EducationScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const EducationScreenWrapper = () => (
<LanguageProvider>
<PezkuwiProvider>
<EducationScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<EducationScreen />
</PezkuwiProvider>
);
describe('EducationScreen', () => {
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import { AuthProvider } from '../../contexts/AuthContext';
import ForumScreen from '../ForumScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const ForumScreenWrapper = () => (
<LanguageProvider>
<AuthProvider>
<ForumScreen />
</AuthProvider>
</LanguageProvider>
<AuthProvider>
<ForumScreen />
</AuthProvider>
);
describe('ForumScreen', () => {
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import GovernanceScreen from '../GovernanceScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const GovernanceScreenWrapper = () => (
<LanguageProvider>
<PezkuwiProvider>
<GovernanceScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<GovernanceScreen />
</PezkuwiProvider>
);
describe('GovernanceScreen', () => {
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import { BiometricAuthProvider } from '../../contexts/BiometricAuthContext';
import LockScreen from '../LockScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const LockScreenWrapper = () => (
<LanguageProvider>
<BiometricAuthProvider>
<LockScreen />
</BiometricAuthProvider>
</LanguageProvider>
<BiometricAuthProvider>
<LockScreen />
</BiometricAuthProvider>
);
describe('LockScreen', () => {
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import NFTGalleryScreen from '../NFTGalleryScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const NFTGalleryScreenWrapper = () => (
<LanguageProvider>
<PezkuwiProvider>
<NFTGalleryScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<NFTGalleryScreen />
</PezkuwiProvider>
);
describe('NFTGalleryScreen', () => {
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import P2PScreen from '../P2PScreen';
@@ -11,11 +10,9 @@ jest.mock('@react-navigation/native', () => ({
// Wrapper with required providers
const P2PScreenWrapper = () => (
<LanguageProvider>
<PezkuwiProvider>
<P2PScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<P2PScreen />
</PezkuwiProvider>
);
describe('P2PScreen', () => {
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import ProfileScreen from '../ProfileScreen';
jest.mock('@react-navigation/native', () => ({
@@ -9,9 +8,7 @@ jest.mock('@react-navigation/native', () => ({
}));
const ProfileScreenWrapper = () => (
<LanguageProvider>
<ProfileScreen />
</LanguageProvider>
<ProfileScreen />
);
describe('ProfileScreen', () => {
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import ReferralScreen from '../ReferralScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const ReferralScreenWrapper = () => (
<LanguageProvider>
<PezkuwiProvider>
<ReferralScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<ReferralScreen />
</PezkuwiProvider>
);
describe('ReferralScreen', () => {
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import { BiometricAuthProvider } from '../../contexts/BiometricAuthContext';
import SecurityScreen from '../SecurityScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const SecurityScreenWrapper = () => (
<LanguageProvider>
<BiometricAuthProvider>
<SecurityScreen />
</BiometricAuthProvider>
</LanguageProvider>
<BiometricAuthProvider>
<SecurityScreen />
</BiometricAuthProvider>
);
describe('SecurityScreen', () => {
@@ -1,7 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { AuthProvider } from '../../contexts/AuthContext';
import { LanguageProvider } from '../../contexts/LanguageContext';
import SignInScreen from '../SignInScreen';
jest.mock('@react-navigation/native', () => ({
@@ -11,11 +10,9 @@ jest.mock('@react-navigation/native', () => ({
// Wrapper with required providers
const SignInScreenWrapper = () => (
<LanguageProvider>
<AuthProvider>
<SignInScreen />
</AuthProvider>
</LanguageProvider>
<AuthProvider>
<SignInScreen />
</AuthProvider>
);
describe('SignInScreen', () => {
@@ -1,7 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { AuthProvider } from '../../contexts/AuthContext';
import { LanguageProvider } from '../../contexts/LanguageContext';
import SignUpScreen from '../SignUpScreen';
jest.mock('@react-navigation/native', () => ({
@@ -11,11 +10,9 @@ jest.mock('@react-navigation/native', () => ({
// Wrapper with required providers
const SignUpScreenWrapper = () => (
<LanguageProvider>
<AuthProvider>
<SignUpScreen />
</AuthProvider>
</LanguageProvider>
<AuthProvider>
<SignUpScreen />
</AuthProvider>
);
describe('SignUpScreen', () => {
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import StakingScreen from '../StakingScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const StakingScreenWrapper = () => (
<LanguageProvider>
<PezkuwiProvider>
<StakingScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<StakingScreen />
</PezkuwiProvider>
);
describe('StakingScreen', () => {
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import SwapScreen from '../SwapScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const SwapScreenWrapper = () => (
<LanguageProvider>
<PezkuwiProvider>
<SwapScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<SwapScreen />
</PezkuwiProvider>
);
describe('SwapScreen', () => {
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import { PezkuwiProvider } from '../contexts/PezkuwiContext';
import WalletScreen from '../WalletScreen';
@@ -10,11 +9,9 @@ jest.mock('@react-navigation/native', () => ({
}));
const WalletScreenWrapper = () => (
<LanguageProvider>
<PezkuwiProvider>
<WalletScreen />
</PezkuwiProvider>
</LanguageProvider>
<PezkuwiProvider>
<WalletScreen />
</PezkuwiProvider>
);
describe('WalletScreen', () => {
@@ -1,6 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import { LanguageProvider } from '../../contexts/LanguageContext';
import WelcomeScreen from '../WelcomeScreen';
// Mock navigation
@@ -13,9 +12,7 @@ jest.mock('@react-navigation/native', () => ({
// Wrapper with required providers
const WelcomeScreenWrapper = () => (
<LanguageProvider>
<WelcomeScreen />
</LanguageProvider>
<WelcomeScreen />
);
describe('WelcomeScreen', () => {