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 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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue