feat(settings): account section + reset modal + language picker (Tailwind)
SettingsScreen :
- Section "Compte" : ligne user (avatar + nom + email si non-guest +
badge "Invité" orange si isGuest) + ligne "Recommencer avec un nouveau
compte" (icône RefreshCw rouge)
- Reset account : remplace Alert.alert natif par ConfirmDialog stylé
(variant destructive). Au confirm, resetAccount() puis
navigation.reset({ index: 0, routes: [{ name: 'Onboarding' }] }) après
un setTimeout(50) pour laisser RootNavigator re-render avec le screen
Onboarding monté
- Language picker : remplace le toggle inline (clic = swap FR/EN) par
l'ouverture d'un LanguagePickerModal stylé
LanguagePickerModal (composant ui réutilisable) :
- Tailwind only : Modal RN + backdrop noir 50% + card rounded-3xl + shadow
- Header icône Globe verte + titre + subtitle
- 2 options Francais/English avec drapeau emoji 28px + label 16px
font-semibold ; option active : bg vert pâle + border verte + cercle
vert avec checkmark
- Bouton Annuler ghost grisé en bas
Messages i18n explicites :
- 'Vous serez redirigé vers l'écran de connexion pour créer un nouveau
compte ou continuer en invité'
- CTA destructive : 'Oui, me déconnecter'
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
05e28b3ebc
commit
a25295e186
118
VinEye/src/components/ui/LanguagePickerModal.tsx
Normal file
118
VinEye/src/components/ui/LanguagePickerModal.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { Modal, Pressable, View } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Check, Globe } from "lucide-react-native";
|
||||
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { colors } from "@/theme/colors";
|
||||
|
||||
export type LanguageCode = "fr" | "en";
|
||||
|
||||
interface LanguageOption {
|
||||
code: LanguageCode;
|
||||
flag: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface LanguagePickerModalProps {
|
||||
visible: boolean;
|
||||
current: LanguageCode;
|
||||
onSelect: (code: LanguageCode) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const OPTIONS: LanguageOption[] = [
|
||||
{ code: "fr", flag: "🇫🇷", label: "Français" },
|
||||
{ code: "en", flag: "🇬🇧", label: "English" },
|
||||
];
|
||||
|
||||
export function LanguagePickerModal({
|
||||
visible,
|
||||
current,
|
||||
onSelect,
|
||||
onClose,
|
||||
}: LanguagePickerModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={onClose}
|
||||
statusBarTranslucent
|
||||
>
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
className="flex-1 items-center justify-center px-6 bg-black/50"
|
||||
>
|
||||
<Pressable
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
className="w-full max-w-[400px] bg-white rounded-3xl p-6"
|
||||
style={{
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 12 },
|
||||
shadowOpacity: 0.18,
|
||||
shadowRadius: 28,
|
||||
elevation: 16,
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="items-center mb-4">
|
||||
<View className="w-14 h-14 rounded-full items-center justify-center bg-[#E8F5E9]">
|
||||
<Globe size={26} color={colors.primary[700]} strokeWidth={2.4} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text className="text-[18px] font-bold text-[#1A1A1A] text-center">
|
||||
{t("settings.language.title")}
|
||||
</Text>
|
||||
<Text className="mt-2 text-[14px] leading-5 text-[#6B6B6B] text-center">
|
||||
{t("settings.language.subtitle")}
|
||||
</Text>
|
||||
|
||||
{/* Options */}
|
||||
<View className="mt-6 gap-2">
|
||||
{OPTIONS.map((opt) => {
|
||||
const isCurrent = opt.code === current;
|
||||
return (
|
||||
<Pressable
|
||||
key={opt.code}
|
||||
onPress={() => onSelect(opt.code)}
|
||||
className={`flex-row items-center gap-3 py-4 px-4 rounded-2xl border-2 active:opacity-70 ${
|
||||
isCurrent
|
||||
? "bg-[#E8F5E9] border-[#2D6A4F]"
|
||||
: "bg-white border-[#E5E7EB]"
|
||||
}`}
|
||||
>
|
||||
<Text className="text-[28px]">{opt.flag}</Text>
|
||||
<Text
|
||||
className={`flex-1 text-[16px] font-semibold ${
|
||||
isCurrent ? "text-[#2D6A4F]" : "text-[#1A1A1A]"
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</Text>
|
||||
{isCurrent && (
|
||||
<View className="w-7 h-7 rounded-full items-center justify-center bg-[#2D6A4F]">
|
||||
<Check size={16} color="#FFFFFF" strokeWidth={3} />
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* Cancel */}
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
className="mt-5 min-h-[52px] rounded-[14px] py-3 px-3 items-center justify-center bg-[#F2F2F2] border border-[#E0E0E0] active:opacity-70"
|
||||
>
|
||||
<Text className="text-[15px] font-bold text-[#2D2D2D]">
|
||||
{t("common.cancel")}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -18,9 +18,12 @@ import { toast } from "sonner-native";
|
|||
import i18n from "@/i18n";
|
||||
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { LanguagePickerModal, type LanguageCode } from "@/components/ui/LanguagePickerModal";
|
||||
import { ConfirmDialog } from "@/components/ui/ConfirmDialog";
|
||||
import { colors } from "@/theme/colors";
|
||||
import { useGameProgress } from "@/hooks/useGameProgress";
|
||||
import { useHistory } from "@/hooks/useHistory";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { storage } from "@/services/storage";
|
||||
import type { RootStackParamList } from "@/types/navigation";
|
||||
|
||||
|
|
@ -42,8 +45,11 @@ export default function SettingsScreen() {
|
|||
const navigation = useNavigation<Nav>();
|
||||
const { progress, resetProgress } = useGameProgress();
|
||||
const { clearHistory, seedTestData } = useHistory();
|
||||
const { user, resetAccount } = useAuth();
|
||||
|
||||
const [notificationsEnabled, setNotificationsEnabled] = useState(true);
|
||||
const [langPickerOpen, setLangPickerOpen] = useState(false);
|
||||
const [resetAccountOpen, setResetAccountOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
storage
|
||||
|
|
@ -66,9 +72,15 @@ export default function SettingsScreen() {
|
|||
toast.success(t("settings.seedDone"));
|
||||
}
|
||||
|
||||
function handleLanguageToggle() {
|
||||
const newLang = i18n.language === "fr" ? "en" : "fr";
|
||||
i18n.changeLanguage(newLang);
|
||||
function handleLanguagePress() {
|
||||
setLangPickerOpen(true);
|
||||
}
|
||||
|
||||
function handleLanguageSelect(code: LanguageCode) {
|
||||
if (i18n.language !== code) {
|
||||
i18n.changeLanguage(code);
|
||||
}
|
||||
setLangPickerOpen(false);
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
|
|
@ -85,12 +97,29 @@ export default function SettingsScreen() {
|
|||
]);
|
||||
}
|
||||
|
||||
function handleResetAccount() {
|
||||
setResetAccountOpen(true);
|
||||
}
|
||||
|
||||
async function handleConfirmReset() {
|
||||
setResetAccountOpen(false);
|
||||
await resetAccount();
|
||||
// Le RootNavigator re-render avec le screen Onboarding monté ;
|
||||
// on attend la frame suivante avant de reset la stack.
|
||||
setTimeout(() => {
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: "Onboarding" }],
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
|
||||
const generalItems: MenuItem[] = [
|
||||
{
|
||||
icon: "globe-outline",
|
||||
label: t("profile.language"),
|
||||
rightText: i18n.language === "fr" ? "Français" : "English",
|
||||
onPress: handleLanguageToggle,
|
||||
onPress: handleLanguagePress,
|
||||
},
|
||||
// {
|
||||
// icon: "notifications-outline",
|
||||
|
|
@ -280,6 +309,49 @@ export default function SettingsScreen() {
|
|||
</TouchableOpacity>
|
||||
*/}
|
||||
|
||||
{/* Compte (current user + reset account) */}
|
||||
<Text style={styles.sectionLabel}>{t("settings.account.sectionTitle")}</Text>
|
||||
<View style={styles.menuCard}>
|
||||
<View style={styles.accountRow}>
|
||||
<View style={[styles.iconBox, { backgroundColor: "#E8F5E9" }]}>
|
||||
<Ionicons name="person" size={20} color={colors.primary[700]} />
|
||||
</View>
|
||||
<View style={styles.accountInfo}>
|
||||
<Text style={styles.accountName} numberOfLines={1}>
|
||||
{user?.name ?? "—"}
|
||||
</Text>
|
||||
{user?.email ? (
|
||||
<Text style={styles.accountEmail} numberOfLines={1}>
|
||||
{user.email}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
{user?.isGuest ? (
|
||||
<View style={styles.guestBadge}>
|
||||
<Text style={styles.guestBadgeText}>
|
||||
{t("settings.account.guestBadge")}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={styles.divider} />
|
||||
<TouchableOpacity
|
||||
style={styles.menuRow}
|
||||
activeOpacity={0.5}
|
||||
onPress={handleResetAccount}
|
||||
>
|
||||
<View style={[styles.iconBox, { backgroundColor: "#FEF2F2" }]}>
|
||||
<Ionicons name="refresh-outline" size={20} color="#EF4444" />
|
||||
</View>
|
||||
<Text style={[styles.menuLabel, styles.menuLabelDanger]}>
|
||||
{t("settings.account.resetAction")}
|
||||
</Text>
|
||||
<View style={styles.menuRight}>
|
||||
<ChevronRight size={16} color="#D1D1D6" strokeWidth={2} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{devItems.length > 0 && (
|
||||
<>
|
||||
<Text style={styles.sectionLabel}>{t("settings.developer")}</Text>
|
||||
|
|
@ -292,6 +364,24 @@ export default function SettingsScreen() {
|
|||
<Text style={styles.versionText}>VinEye • Version 1.0.0</Text>
|
||||
<View style={{ height: 40 }} />
|
||||
</ScrollView>
|
||||
|
||||
<LanguagePickerModal
|
||||
visible={langPickerOpen}
|
||||
current={i18n.language === "fr" ? "fr" : "en"}
|
||||
onSelect={handleLanguageSelect}
|
||||
onClose={() => setLangPickerOpen(false)}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
visible={resetAccountOpen}
|
||||
title={t("settings.account.resetConfirmTitle")}
|
||||
message={t("settings.account.resetConfirmMessage")}
|
||||
confirmLabel={t("settings.account.resetConfirmOk")}
|
||||
cancelLabel={t("settings.account.resetConfirmCancel")}
|
||||
variant="destructive"
|
||||
onConfirm={handleConfirmReset}
|
||||
onCancel={() => setResetAccountOpen(false)}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
|
@ -421,6 +511,39 @@ const styles = StyleSheet.create({
|
|||
backgroundColor: "#F5F5F5",
|
||||
marginLeft: 60,
|
||||
},
|
||||
// Account section
|
||||
accountRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 16,
|
||||
gap: 12,
|
||||
},
|
||||
accountInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
accountName: {
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
color: "#1A1A1A",
|
||||
},
|
||||
accountEmail: {
|
||||
marginTop: 2,
|
||||
fontSize: 12,
|
||||
color: "#8E8E93",
|
||||
},
|
||||
guestBadge: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 999,
|
||||
backgroundColor: "#FFF4E5",
|
||||
},
|
||||
guestBadgeText: {
|
||||
fontSize: 11,
|
||||
fontWeight: "700",
|
||||
color: "#E67E22",
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
/* Styles du Referral card — gardés au cas où on réactive */
|
||||
referCard: {
|
||||
backgroundColor: "#F97316",
|
||||
|
|
|
|||
Loading…
Reference in a new issue