mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-12 19:01:03 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user