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:
Yanis 2026-05-01 09:33:46 +02:00
parent 05e28b3ebc
commit a25295e186
2 changed files with 245 additions and 4 deletions

View 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>
);
}

View file

@ -18,9 +18,12 @@ import { toast } from "sonner-native";
import i18n from "@/i18n"; import i18n from "@/i18n";
import { Text } from "@/components/ui/text"; 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 { colors } from "@/theme/colors";
import { useGameProgress } from "@/hooks/useGameProgress"; import { useGameProgress } from "@/hooks/useGameProgress";
import { useHistory } from "@/hooks/useHistory"; import { useHistory } from "@/hooks/useHistory";
import { useAuth } from "@/contexts/AuthContext";
import { storage } from "@/services/storage"; import { storage } from "@/services/storage";
import type { RootStackParamList } from "@/types/navigation"; import type { RootStackParamList } from "@/types/navigation";
@ -42,8 +45,11 @@ export default function SettingsScreen() {
const navigation = useNavigation<Nav>(); const navigation = useNavigation<Nav>();
const { progress, resetProgress } = useGameProgress(); const { progress, resetProgress } = useGameProgress();
const { clearHistory, seedTestData } = useHistory(); const { clearHistory, seedTestData } = useHistory();
const { user, resetAccount } = useAuth();
const [notificationsEnabled, setNotificationsEnabled] = useState(true); const [notificationsEnabled, setNotificationsEnabled] = useState(true);
const [langPickerOpen, setLangPickerOpen] = useState(false);
const [resetAccountOpen, setResetAccountOpen] = useState(false);
useEffect(() => { useEffect(() => {
storage storage
@ -66,9 +72,15 @@ export default function SettingsScreen() {
toast.success(t("settings.seedDone")); toast.success(t("settings.seedDone"));
} }
function handleLanguageToggle() { function handleLanguagePress() {
const newLang = i18n.language === "fr" ? "en" : "fr"; setLangPickerOpen(true);
i18n.changeLanguage(newLang); }
function handleLanguageSelect(code: LanguageCode) {
if (i18n.language !== code) {
i18n.changeLanguage(code);
}
setLangPickerOpen(false);
} }
function handleReset() { 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[] = [ const generalItems: MenuItem[] = [
{ {
icon: "globe-outline", icon: "globe-outline",
label: t("profile.language"), label: t("profile.language"),
rightText: i18n.language === "fr" ? "Français" : "English", rightText: i18n.language === "fr" ? "Français" : "English",
onPress: handleLanguageToggle, onPress: handleLanguagePress,
}, },
// { // {
// icon: "notifications-outline", // icon: "notifications-outline",
@ -280,6 +309,49 @@ export default function SettingsScreen() {
</TouchableOpacity> </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 && ( {devItems.length > 0 && (
<> <>
<Text style={styles.sectionLabel}>{t("settings.developer")}</Text> <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> <Text style={styles.versionText}>VinEye Version 1.0.0</Text>
<View style={{ height: 40 }} /> <View style={{ height: 40 }} />
</ScrollView> </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> </SafeAreaView>
); );
} }
@ -421,6 +511,39 @@ const styles = StyleSheet.create({
backgroundColor: "#F5F5F5", backgroundColor: "#F5F5F5",
marginLeft: 60, 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 */ /* Styles du Referral card — gardés au cas où on réactive */
referCard: { referCard: {
backgroundColor: "#F97316", backgroundColor: "#F97316",