Improved Localization in Account Manage Screen

This commit is contained in:
albyalex96 2026-03-05 15:45:47 +01:00
parent 18c40ced29
commit 821a6b864f
3 changed files with 1973 additions and 1820 deletions

File diff suppressed because it is too large Load diff

View file

@ -1535,5 +1535,12 @@
"provider_logs": "Log Provider", "provider_logs": "Log Provider",
"no_logs_captured": "Nessun log catturato." "no_logs_captured": "Nessun log catturato."
} }
} },
"account_manager":{
"sign_out":"Esci",
"sign_out_desc":"",
"user_id":"ID Utente",
"display_name":"Nickname",
"display_name_placeholder":"Aggiungi un nickname"
}
} }

View file

@ -1,5 +1,16 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, StatusBar, Platform, Animated, Easing, TextInput, ActivityIndicator } from 'react-native'; import {
View,
Text,
StyleSheet,
TouchableOpacity,
StatusBar,
Platform,
Animated,
Easing,
TextInput,
ActivityIndicator,
} from 'react-native';
import FastImage from '@d11/react-native-fast-image'; import FastImage from '@d11/react-native-fast-image';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
@ -7,331 +18,458 @@ import { MaterialIcons } from '@expo/vector-icons';
import { useAccount } from '../contexts/AccountContext'; import { useAccount } from '../contexts/AccountContext';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import CustomAlert from '../components/CustomAlert'; import CustomAlert from '../components/CustomAlert';
const AccountManageScreen: React.FC = () => { const AccountManageScreen: React.FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { user, signOut, updateProfile } = useAccount(); const { user, signOut, updateProfile } = useAccount();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { t } = useTranslation();
const headerOpacity = useRef(new Animated.Value(0)).current;
const headerTranslateY = useRef(new Animated.Value(8)).current;
const contentOpacity = useRef(new Animated.Value(0)).current;
const contentTranslateY = useRef(new Animated.Value(8)).current;
const headerOpacity = useRef(new Animated.Value(0)).current; useEffect(() => {
const headerTranslateY = useRef(new Animated.Value(8)).current; Animated.parallel([
const contentOpacity = useRef(new Animated.Value(0)).current; Animated.timing(headerOpacity, {
const contentTranslateY = useRef(new Animated.Value(8)).current; toValue: 1,
duration: 260,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}),
Animated.timing(headerTranslateY, {
toValue: 0,
duration: 260,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}),
Animated.timing(contentOpacity, {
toValue: 1,
duration: 320,
delay: 80,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}),
Animated.timing(contentTranslateY, {
toValue: 0,
duration: 320,
delay: 80,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}),
]).start();
}, [headerOpacity, headerTranslateY, contentOpacity, contentTranslateY]);
useEffect(() => { const initial = useMemo(
Animated.parallel([ () => user?.email?.[0]?.toUpperCase() || 'U',
Animated.timing(headerOpacity, { toValue: 1, duration: 260, easing: Easing.out(Easing.cubic), useNativeDriver: true }), [user?.email],
Animated.timing(headerTranslateY, { toValue: 0, duration: 260, easing: Easing.out(Easing.cubic), useNativeDriver: true }), );
Animated.timing(contentOpacity, { toValue: 1, duration: 320, delay: 80, easing: Easing.out(Easing.cubic), useNativeDriver: true }), const [displayName, setDisplayName] = useState(user?.displayName || '');
Animated.timing(contentTranslateY, { toValue: 0, duration: 320, delay: 80, easing: Easing.out(Easing.cubic), useNativeDriver: true }), const [avatarUrl, setAvatarUrl] = useState(user?.avatarUrl || '');
]).start(); const [saving, setSaving] = useState(false);
}, [headerOpacity, headerTranslateY, contentOpacity, contentTranslateY]); const [avatarError, setAvatarError] = useState(false);
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<any[]>([]);
useEffect(() => {
// Reset image error state when URL changes
setAvatarError(false);
}, [avatarUrl]);
const initial = useMemo(() => (user?.email?.[0]?.toUpperCase() || 'U'), [user?.email]); const handleSave = async () => {
const [displayName, setDisplayName] = useState(user?.displayName || ''); if (saving) return;
const [avatarUrl, setAvatarUrl] = useState(user?.avatarUrl || ''); setSaving(true);
const [saving, setSaving] = useState(false); const err = await updateProfile({
const [avatarError, setAvatarError] = useState(false); displayName: displayName.trim() || undefined,
const [alertVisible, setAlertVisible] = useState(false); avatarUrl: avatarUrl.trim() || undefined,
const [alertTitle, setAlertTitle] = useState(''); });
const [alertMessage, setAlertMessage] = useState(''); if (err) {
const [alertActions, setAlertActions] = useState<any[]>([]); setAlertTitle(t('common.error'));
setAlertMessage(err);
setAlertActions([{ label: 'OK', onPress: () => {} }]);
setAlertVisible(true);
}
setSaving(false);
};
useEffect(() => { const handleSignOut = () => {
// Reset image error state when URL changes setAlertTitle(t('account_manager.sign_out'));
setAvatarError(false); setAlertMessage(t('account_manager.sign_out_desc'));
}, [avatarUrl]); setAlertActions([
{ label: t('common.cancel'), onPress: () => {} },
{
label: t('account_manager.sign_out'),
onPress: async () => {
try {
await signOut();
// @ts-ignore
navigation.goBack();
} catch (_) {}
},
style: { opacity: 1 },
},
]);
setAlertVisible(true);
};
const handleSave = async () => { return (
if (saving) return; <View
setSaving(true); style={[
const err = await updateProfile({ displayName: displayName.trim() || undefined, avatarUrl: avatarUrl.trim() || undefined }); styles.container,
if (err) { { backgroundColor: currentTheme.colors.darkBackground },
setAlertTitle('Error'); ]}
setAlertMessage(err); >
setAlertActions([{ label: 'OK', onPress: () => {} }]); <StatusBar
setAlertVisible(true); translucent
} barStyle="light-content"
setSaving(false); backgroundColor="transparent"
}; />
const handleSignOut = () => { {/* Header */}
setAlertTitle('Sign out'); <Animated.View
setAlertMessage('Are you sure you want to sign out?'); style={[
setAlertActions([ styles.header,
{ label: 'Cancel', onPress: () => {} }, {
{ paddingTop:
label: 'Sign out', (Platform.OS === 'android' ? StatusBar.currentHeight || 0 : insets.top) +
onPress: async () => { 12,
try { opacity: headerOpacity,
await signOut(); transform: [{ translateY: headerTranslateY }],
// @ts-ignore },
navigation.goBack(); ]}
} catch (_) {} >
}, <LinearGradient
style: { opacity: 1 }, colors={[currentTheme.colors.darkBackground, '#111318']}
}, style={StyleSheet.absoluteFill}
]); />
setAlertVisible(true); <TouchableOpacity
}; onPress={() => navigation.goBack()}
style={styles.headerBack}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
>
<MaterialIcons
name="arrow-back"
size={22}
color={currentTheme.colors.white}
/>
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>
Account
</Text>
<View style={{ width: 22, height: 22 }} />
</Animated.View>
return ( {/* Content */}
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <Animated.View
<StatusBar translucent barStyle="light-content" backgroundColor="transparent" /> style={[
styles.content,
{
opacity: contentOpacity,
transform: [{ translateY: contentTranslateY }],
},
]}
>
{/* Profile Badge */}
<View style={styles.profileContainer}>
{avatarUrl && !avatarError ? (
<View style={[styles.avatar, { overflow: 'hidden' }]}>
<FastImage
source={{ uri: avatarUrl }}
style={styles.avatarImage}
resizeMode={FastImage.resizeMode.cover}
onError={() => setAvatarError(true)}
/>
</View>
) : (
<View
style={[
styles.avatar,
{ backgroundColor: currentTheme.colors.elevation2 },
]}
>
<Text style={styles.avatarText}>{displayName?.[0] || initial}</Text>
</View>
)}
</View>
{/* Header */} {/* Account details card */}
<Animated.View <View
style={[ style={[
styles.header, styles.card,
{ {
paddingTop: (Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top) + 12, backgroundColor: currentTheme.colors.elevation1,
opacity: headerOpacity, borderColor: currentTheme.colors.elevation2,
transform: [{ translateY: headerTranslateY }], },
}, ]}
]} >
> <View style={styles.itemRow}>
<LinearGradient <View style={styles.itemLeft}>
colors={[currentTheme.colors.darkBackground, '#111318']} <MaterialIcons
style={StyleSheet.absoluteFill} name="badge"
/> size={20}
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.headerBack} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> color={currentTheme.colors.primary}
<MaterialIcons name="arrow-back" size={22} color={currentTheme.colors.white} /> />
</TouchableOpacity> <Text
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Account</Text> style={[styles.itemTitle, { color: currentTheme.colors.highEmphasis }]}
<View style={{ width: 22, height: 22 }} /> >
</Animated.View> {t('account_manager.display_name')}
</Text>
</View>
<TextInput
placeholder={t('account_manager.display_name_placeholder')}
placeholderTextColor={currentTheme.colors.mediumEmphasis}
style={[styles.input, { color: currentTheme.colors.white }]}
value={displayName}
onChangeText={setDisplayName}
numberOfLines={1}
/>
</View>
{/* Content */} <View style={styles.divider} />
<Animated.View style={[styles.content, { opacity: contentOpacity, transform: [{ translateY: contentTranslateY }] }]}>
{/* Profile Badge */}
<View style={styles.profileContainer}>
{avatarUrl && !avatarError ? (
<View style={[styles.avatar, { overflow: 'hidden' }]}>
<FastImage
source={{ uri: avatarUrl }}
style={styles.avatarImage}
resizeMode={FastImage.resizeMode.cover}
onError={() => setAvatarError(true)}
/>
</View>
) : (
<View style={[styles.avatar, { backgroundColor: currentTheme.colors.elevation2 }]}>
<Text style={styles.avatarText}>{(displayName?.[0] || initial)}</Text>
</View>
)}
</View>
{/* Account details card */} <View
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1, borderColor: currentTheme.colors.elevation2 }]}> style={[
<View style={styles.itemRow}> styles.itemRow,
<View style={styles.itemLeft}> Platform.OS === 'android' && styles.itemRowCompact,
<MaterialIcons name="badge" size={20} color={currentTheme.colors.primary} /> ]}
<Text style={[styles.itemTitle, { color: currentTheme.colors.highEmphasis }]}>Display name</Text> >
</View> <View style={styles.itemLeft}>
<TextInput <MaterialIcons
placeholder="Add a display name" name="image"
placeholderTextColor={currentTheme.colors.mediumEmphasis} size={20}
style={[styles.input, { color: currentTheme.colors.white }]} color={currentTheme.colors.primary}
value={displayName} />
onChangeText={setDisplayName} <Text
numberOfLines={1} style={[styles.itemTitle, { color: currentTheme.colors.highEmphasis }]}
/> >
</View> Avatar URL
</Text>
</View>
<TextInput
placeholder="https://..."
placeholderTextColor={currentTheme.colors.mediumEmphasis}
style={[styles.input, { color: currentTheme.colors.white }]}
value={avatarUrl}
onChangeText={setAvatarUrl}
autoCapitalize="none"
numberOfLines={1}
/>
</View>
<View style={styles.divider} /> <View style={styles.divider} />
<View style={[styles.itemRow, Platform.OS === 'android' && styles.itemRowCompact]}> <View style={styles.itemRow}>
<View style={styles.itemLeft}> <View style={styles.itemLeft}>
<MaterialIcons name="image" size={20} color={currentTheme.colors.primary} /> <MaterialIcons
<Text style={[styles.itemTitle, { color: currentTheme.colors.highEmphasis }]}>Avatar URL</Text> name="account-circle"
</View> size={20}
<TextInput color={currentTheme.colors.primary}
placeholder="https://..." />
placeholderTextColor={currentTheme.colors.mediumEmphasis} <Text
style={[styles.input, { color: currentTheme.colors.white }]} style={[styles.itemTitle, { color: currentTheme.colors.highEmphasis }]}
value={avatarUrl} >
onChangeText={setAvatarUrl} {t('common.email')}
autoCapitalize="none" </Text>
numberOfLines={1} </View>
/> <Text
</View> style={[styles.itemValue, { color: currentTheme.colors.mediumEmphasis }]}
numberOfLines={1}
>
{user?.email || '—'}
</Text>
</View>
<View style={styles.divider} /> <View style={styles.divider} />
<View style={styles.itemRow}> <View style={styles.itemRow}>
<View style={styles.itemLeft}> <View style={styles.itemLeft}>
<MaterialIcons name="account-circle" size={20} color={currentTheme.colors.primary} /> <MaterialIcons
<Text style={[styles.itemTitle, { color: currentTheme.colors.highEmphasis }]}>Email</Text> name="fingerprint"
</View> size={20}
<Text style={[styles.itemValue, { color: currentTheme.colors.mediumEmphasis }]} numberOfLines={1}> color={currentTheme.colors.primary}
{user?.email || '—'} />
</Text> <Text
</View> style={[styles.itemTitle, { color: currentTheme.colors.highEmphasis }]}
>
{t('account_manager.user_id')}
</Text>
</View>
<Text
style={[styles.itemValue, { color: currentTheme.colors.mediumEmphasis }]}
numberOfLines={1}
>
{user?.id}
</Text>
</View>
</View>
<View style={styles.divider} /> {/* Save and Sign out */}
<TouchableOpacity
activeOpacity={0.85}
style={[
styles.saveButton,
{
backgroundColor: currentTheme.colors.elevation2,
borderColor: currentTheme.colors.elevation2,
},
]}
onPress={handleSave}
disabled={saving}
>
{saving ? (
<ActivityIndicator color={currentTheme.colors.white} />
) : (
<>
<MaterialIcons
name="save-alt"
size={18}
color={currentTheme.colors.white}
style={{ marginRight: 8 }}
/>
<Text style={styles.saveText}>{t('common.save')}</Text>
</>
)}
</TouchableOpacity>
<View style={styles.itemRow}> <TouchableOpacity
<View style={styles.itemLeft}> activeOpacity={0.85}
<MaterialIcons name="fingerprint" size={20} color={currentTheme.colors.primary} /> style={[
<Text style={[styles.itemTitle, { color: currentTheme.colors.highEmphasis }]}>User ID</Text> styles.signOutButton,
</View> { backgroundColor: currentTheme.colors.primary },
<Text style={[styles.itemValue, { color: currentTheme.colors.mediumEmphasis }]} numberOfLines={1}> ]}
{user?.id} onPress={handleSignOut}
</Text> >
</View> <MaterialIcons
</View> name="logout"
size={18}
{/* Save and Sign out */} color="#fff"
<TouchableOpacity style={{ marginRight: 8 }}
activeOpacity={0.85} />
style={[styles.saveButton, { backgroundColor: currentTheme.colors.elevation2, borderColor: currentTheme.colors.elevation2 }]} <Text style={styles.signOutText}>{t('account_manager.sign_out')}</Text>
onPress={handleSave} </TouchableOpacity>
disabled={saving} </Animated.View>
> <CustomAlert
{saving ? ( visible={alertVisible}
<ActivityIndicator color={currentTheme.colors.white} /> title={alertTitle}
) : ( message={alertMessage}
<> actions={alertActions}
<MaterialIcons name="save-alt" size={18} color={currentTheme.colors.white} style={{ marginRight: 8 }} /> onClose={() => setAlertVisible(false)}
<Text style={styles.saveText}>Save changes</Text> />
</> </View>
)} );
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.85}
style={[
styles.signOutButton,
{ backgroundColor: currentTheme.colors.primary },
]}
onPress={handleSignOut}
>
<MaterialIcons name="logout" size={18} color="#fff" style={{ marginRight: 8 }} />
<Text style={styles.signOutText}>Sign out</Text>
</TouchableOpacity>
</Animated.View>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
actions={alertActions}
onClose={() => setAlertVisible(false)}
/>
</View>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
}, },
header: { header: {
paddingHorizontal: 16, paddingHorizontal: 16,
paddingBottom: 14, paddingBottom: 14,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
}, },
headerBack: { headerBack: {
padding: 4, padding: 4,
}, },
headerTitle: { headerTitle: {
fontSize: 18, fontSize: 18,
fontWeight: '800', fontWeight: '800',
}, },
content: { content: {
flex: 1, flex: 1,
paddingHorizontal: 16, paddingHorizontal: 16,
paddingTop: 10, paddingTop: 10,
}, },
profileContainer: { profileContainer: {
alignItems: 'center', alignItems: 'center',
marginBottom: 12, marginBottom: 12,
}, },
avatar: { avatar: {
width: 72, width: 72,
height: 72, height: 72,
borderRadius: 36, borderRadius: 36,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
avatarImage: { avatarImage: {
width: '100%', width: '100%',
height: '100%', height: '100%',
}, },
avatarText: { avatarText: {
color: '#fff', color: '#fff',
fontWeight: '800', fontWeight: '800',
fontSize: 24, fontSize: 24,
}, },
card: { card: {
borderRadius: 14, borderRadius: 14,
borderWidth: StyleSheet.hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
paddingHorizontal: 14, paddingHorizontal: 14,
paddingVertical: 10, paddingVertical: 10,
}, },
itemRow: { itemRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingVertical: 10, paddingVertical: 10,
}, },
itemRowCompact: { itemRowCompact: {
paddingVertical: 6, paddingVertical: 6,
}, },
input: { input: {
flex: 1, flex: 1,
textAlign: 'right', textAlign: 'right',
paddingVertical: 6, paddingVertical: 6,
marginLeft: 12, marginLeft: 12,
}, },
itemLeft: { itemLeft: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 8, gap: 8,
}, },
itemTitle: { itemTitle: {
fontSize: 14, fontSize: 14,
fontWeight: '600', fontWeight: '600',
}, },
itemValue: { itemValue: {
fontSize: 14, fontSize: 14,
maxWidth: '65%', maxWidth: '65%',
}, },
divider: { divider: {
height: StyleSheet.hairlineWidth, height: StyleSheet.hairlineWidth,
backgroundColor: 'rgba(255,255,255,0.08)', backgroundColor: 'rgba(255,255,255,0.08)',
}, },
signOutButton: { signOutButton: {
marginTop: 16, marginTop: 16,
height: 48, height: 48,
borderRadius: 12, borderRadius: 12,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
flexDirection: 'row', flexDirection: 'row',
}, },
signOutText: { signOutText: {
color: '#fff', color: '#fff',
fontWeight: '700', fontWeight: '700',
}, },
saveButton: { saveButton: {
marginTop: 12, marginTop: 12,
height: 46, height: 46,
borderRadius: 12, borderRadius: 12,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
flexDirection: 'row', flexDirection: 'row',
borderWidth: StyleSheet.hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
}, },
saveText: { saveText: {
color: '#fff', color: '#fff',
fontWeight: '700', fontWeight: '700',
}, },
}); });
export default AccountManageScreen; export default AccountManageScreen;