Files
pwap/mobile/src/screens/EditProfileScreen.tsx
T
pezkuwichain f2e70a8150 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
2026-01-15 05:08:21 +03:00

451 lines
13 KiB
TypeScript

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;