feat: add 2FA and change title

This commit is contained in:
2026-05-09 15:17:19 +02:00
parent 771f514af3
commit 1672dc1e20
19 changed files with 622 additions and 62 deletions
+27
View File
@@ -787,6 +787,7 @@ export interface PublicSettings {
crypto_only: boolean;
nowpayments_currencies: string[];
telegram_notifications_enabled: boolean;
two_fa_enabled: boolean;
}
export const getPublicSettings = async (): Promise<PublicSettings> => {
@@ -801,6 +802,7 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
crypto_only: false,
nowpayments_currencies: [],
telegram_notifications_enabled: false,
two_fa_enabled: false,
};
try {
const { data } = await apiClient.get(`${V1}/app-settings`);
@@ -821,6 +823,7 @@ export const getPublicSettings = async (): Promise<PublicSettings> => {
: [],
telegram_notifications_enabled:
data.telegram_notifications_enabled ?? false,
two_fa_enabled: data.two_fa_enabled ?? false,
};
} catch {
return defaults;
@@ -889,6 +892,30 @@ export const unlinkTelegram = async (): Promise<void> => {
}
};
export const get2FAStatus = async (): Promise<{
two_fa_enabled: boolean;
telegram_linked: boolean;
admin_2fa_enabled: boolean;
}> => {
try {
const { data } = await apiClient.get(`${V1}/two-fa/status`);
return data;
} catch {
return { two_fa_enabled: false, telegram_linked: false, admin_2fa_enabled: false };
}
};
export const toggle2FA = async (
enabled: boolean,
): Promise<{ success: boolean; error?: string }> => {
try {
const { data } = await apiClient.post(`${V1}/two-fa/toggle`, { enabled });
return { success: data.success ?? true };
} catch (e: any) {
return { success: false, error: e?.response?.data?.error || "Erreur" };
}
};
export const calculateOrderTotal = (order: any): number => {
if (typeof order.total === "number" && order.total > 0) return order.total;
if (typeof order.total_prix === "number" && order.total_prix > 0)
@@ -45,7 +45,7 @@ export default function OrderHistoryScreen() {
const [orders, setOrders] = useState<CompletedOrder[]>([]);
const [stats, setStats] = useState<ClientStats | null>(null);
const [penalties, setPenalties] = useState<PenaltyInfo | null>(null);
const [appSettings, setAppSettings] = useState<PublicSettings>({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: false, pool_names: ['Pool 1', 'Pool 2'], crypto_payment_enabled: false, nowpayments_currencies: [], crypto_only: false, telegram_notifications_enabled: false });
const [appSettings, setAppSettings] = useState<PublicSettings>({ penalties_enabled: true, show_amende_score: true, points_enabled: true, points_separated: true, referral_enabled: false, pool_names: ['Pool 1', 'Pool 2'], crypto_payment_enabled: false, nowpayments_currencies: [], crypto_only: false, telegram_notifications_enabled: false, two_fa_enabled: false });
const [referralBalance, setReferralBalance] = useState(0);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
+71 -2
View File
@@ -6,6 +6,7 @@ import {
ScrollView,
StyleSheet,
TouchableOpacity,
Switch,
Alert,
Modal,
KeyboardAvoidingView,
@@ -17,7 +18,7 @@ import { Feather } from "@expo/vector-icons";
import { useFocusEffect } from "@react-navigation/native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Ionicons } from "@expo/vector-icons";
import { getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram, changePassword } from "../../api/api";
import { getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram, changePassword, get2FAStatus, toggle2FA, getPublicSettings } from "../../api/api";
import TextInput from "../../components/ui/TextInput";
import Button from "../../components/ui/Button";
import { useTheme } from "../../context/ThemeContext";
@@ -49,6 +50,11 @@ export default function ProfileScreen() {
const [telegramEnabled, setTelegramEnabled] = useState(false);
const [telegramLoading, setTelegramLoading] = useState(false);
// 2FA
const [twoFAEnabled, setTwoFAEnabled] = useState(false);
const [twoFAAdminEnabled, setTwoFAAdminEnabled] = useState(false);
const [twoFALoading, setTwoFALoading] = useState(false);
// Modal confirmation infos par défaut
const [showSaveModal, setShowSaveModal] = useState(false);
@@ -64,15 +70,19 @@ export default function ProfileScreen() {
const loadData = useCallback(async () => {
setLoadingProfile(true);
const [savedAddress, savedPhone, savedSignal, profileRes, tgStatus] = await Promise.all([
const [savedAddress, savedPhone, savedSignal, profileRes, tgStatus, twoFAStatus, pubSettings] = await Promise.all([
AsyncStorage.getItem(STORAGE_ADDRESS),
AsyncStorage.getItem(STORAGE_PHONE),
AsyncStorage.getItem(STORAGE_SIGNAL),
getMyProfile(),
getTelegramStatus(),
get2FAStatus(),
getPublicSettings(),
]);
setTelegramLinked(tgStatus.linked);
setTelegramEnabled(tgStatus.enabled);
setTwoFAEnabled(twoFAStatus.two_fa_enabled);
setTwoFAAdminEnabled(pubSettings.two_fa_enabled);
if (savedAddress !== null) setDefaultAddress(savedAddress);
if (savedPhone !== null) setDefaultPhone(savedPhone);
@@ -136,6 +146,7 @@ export default function ProfileScreen() {
onPress: async () => {
await unlinkTelegram();
setTelegramLinked(false);
setTwoFAEnabled(false);
},
},
],
@@ -170,6 +181,17 @@ export default function ProfileScreen() {
}
};
const handleToggle2FA = async (value: boolean) => {
setTwoFALoading(true);
const res = await toggle2FA(value);
setTwoFALoading(false);
if (res.success) {
setTwoFAEnabled(value);
} else {
Alert.alert("Erreur", res.error || "Impossible de modifier la 2FA");
}
};
const saveContact = async () => {
setSavingContact(true);
const res = await updateMyProfile({ nom: nom.trim(), prenom: prenom.trim(), telephone: telephone.trim() });
@@ -327,6 +349,17 @@ export default function ProfileScreen() {
},
pwdInputIcon: { marginRight: spacing.s },
pwdInput: { flex: 1, fontSize: fontSize.sm },
twoFARow: {
flexDirection: "row" as const,
alignItems: "center" as const,
justifyContent: "space-between" as const,
paddingTop: spacing.xs,
},
twoFALabel: {
color: colors.textPrimary,
fontSize: fontSize.sm,
fontWeight: fontWeight.semibold,
},
}), [colors]);
if (loadingProfile) {
@@ -505,6 +538,42 @@ export default function ProfileScreen() {
</View>
)}
{/* Carte 2FA — visible uniquement si l'admin l'a activé ET Telegram est lié */}
{twoFAAdminEnabled && telegramLinked && (
<View style={styles.card}>
<View style={styles.cardTitle}>
<Ionicons name="shield-checkmark-outline" size={18} color="#6366f1" />
<Text style={styles.cardTitleText}>Double authentification (2FA)</Text>
</View>
<Text style={styles.hint}>
À chaque connexion, un code à 6 chiffres vous sera envoyé sur Telegram avant d'accéder à votre compte.
</Text>
<View style={styles.twoFARow}>
<View style={{ flex: 1 }}>
<Text style={styles.twoFALabel}>
{twoFAEnabled ? "Activée" : "Désactivée"}
</Text>
{twoFAEnabled && (
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs, marginTop: 2 }}>
<Ionicons name="checkmark-circle" size={13} color="#10b981" />
<Text style={{ color: "#10b981", fontSize: fontSize.xs }}>Protection activée</Text>
</View>
)}
</View>
{twoFALoading ? (
<ActivityIndicator size="small" color="#6366f1" />
) : (
<Switch
value={twoFAEnabled}
onValueChange={handleToggle2FA}
trackColor={{ false: colors.border, true: "#6366f155" }}
thumbColor={twoFAEnabled ? "#6366f1" : colors.textMuted}
/>
)}
</View>
</View>
)}
</ScrollView>
<Modal