chore: add change password

This commit is contained in:
2026-03-01 16:32:43 +01:00
parent 7b23f82355
commit e4020c6388
8 changed files with 261 additions and 7 deletions
+27 -2
View File
@@ -11,10 +11,12 @@ import { ThemeProvider, useTheme } from "./src/context/ThemeContext";
import HomeScreen from "./src/screens/HomeScreen";
import ClientLoginScreen from "./src/screens/auth/ClientLoginScreen";
import ChangePasswordScreen from "./src/screens/auth/ChangePasswordScreen";
import ClientNavigator from "./src/navigation/ClientNavigator";
import type { RootStackParamList } from "./src/navigation/types";
import type { RootStackParamList, ChangePasswordStackParamList } from "./src/navigation/types";
const Stack = createNativeStackNavigator<RootStackParamList>();
const CPStack = createNativeStackNavigator<ChangePasswordStackParamList>();
function AuthStack() {
const { colors } = useTheme();
@@ -40,8 +42,27 @@ function AuthStack() {
);
}
function ForceChangePasswordStack() {
const { colors } = useTheme();
return (
<CPStack.Navigator
screenOptions={{
headerStyle: { backgroundColor: colors.bgSecondary },
headerTintColor: colors.textWhite,
gestureEnabled: false,
}}
>
<CPStack.Screen
name="ChangePassword"
component={ChangePasswordScreen}
options={{ title: "Changer le mot de passe", headerLeft: () => null }}
/>
</CPStack.Navigator>
);
}
function RootNavigator() {
const { isAuthenticated, isLoading, role } = useAuth();
const { isAuthenticated, isLoading, role, mustChangePassword } = useAuth();
const { colors, isDark } = useTheme();
if (isLoading) {
@@ -59,6 +80,10 @@ function RootNavigator() {
);
}
if (isAuthenticated && role === "client" && mustChangePassword) {
return <ForceChangePasswordStack />;
}
if (isAuthenticated && role === "client") {
return (
<CartProvider>
+1 -1
View File
@@ -40,7 +40,7 @@
"expo-notifications",
{
"icon": "./assets/icon.png",
"color": "#7C3AED",
"color": "#000000",
"defaultChannel": "orders",
"sounds": []
}
+20
View File
@@ -507,6 +507,26 @@ export const cancelCommand = async (
// PENALTIES
// ============================================
export const changePassword = async (
currentPassword: string,
newPassword: string,
): Promise<{ success: boolean; message?: string }> => {
try {
await apiClient.put(`${V1}/auth/change-password`, {
current_password: currentPassword,
new_password: newPassword,
});
return { success: true };
} catch (error: any) {
return {
success: false,
message:
error.response?.data?.error ||
"Erreur lors du changement de mot de passe",
};
}
};
export const getMyPenalties = async (): Promise<PenaltiesResponse> => {
try {
const { data } = await apiClient.get(`${V1}/penalties`);
+1
View File
@@ -37,6 +37,7 @@ export interface UserResponse {
point?: number;
point_zipette?: number; // ✅ AJOUTER CETTE LIGNE
amende?: number;
must_change_password?: boolean;
}
/**
+16 -3
View File
@@ -17,12 +17,14 @@ interface AuthState {
role: UserRole;
isLoading: boolean;
isAuthenticated: boolean;
mustChangePassword: boolean;
}
interface AuthContextType extends AuthState {
loginClient: (token: string) => Promise<void>;
loginClient: (token: string, mustChangePassword?: boolean) => Promise<void>;
loginAdmin: (token: string, role: 'admin' | 'cabine' | 'livreur') => Promise<void>;
logout: () => Promise<void>;
setMustChangePassword: (value: boolean) => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
@@ -34,6 +36,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: null,
isLoading: true,
isAuthenticated: false,
mustChangePassword: false,
});
// Restore session on mount
@@ -52,6 +55,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: 'client',
isLoading: false,
isAuthenticated: true,
mustChangePassword: false,
});
return;
}
@@ -65,6 +69,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: savedRole as UserRole,
isLoading: false,
isAuthenticated: true,
mustChangePassword: false,
});
return;
}
@@ -78,6 +83,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: null,
isLoading: false,
isAuthenticated: false,
mustChangePassword: false,
});
} catch {
await clearAllAuth();
@@ -87,6 +93,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: null,
isLoading: false,
isAuthenticated: false,
mustChangePassword: false,
});
}
};
@@ -94,7 +101,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
restore();
}, []);
const loginClient = async (token: string) => {
const loginClient = async (token: string, mustChangePassword = false) => {
const uname = extractUsernameFromToken(token);
await storeToken(token);
if (uname) await setUsername(uname);
@@ -105,9 +112,14 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: 'client',
isLoading: false,
isAuthenticated: true,
mustChangePassword,
});
};
const setMustChangePassword = (value: boolean) => {
setState(prev => ({ ...prev, mustChangePassword: value }));
};
const loginAdmin = async (token: string, role: 'admin' | 'cabine' | 'livreur') => {
const uname = extractUsernameFromToken(token);
await storeAdminToken(token);
@@ -130,11 +142,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: null,
isLoading: false,
isAuthenticated: false,
mustChangePassword: false,
});
};
return (
<AuthContext.Provider value={{ ...state, loginClient, loginAdmin, logout }}>
<AuthContext.Provider value={{ ...state, loginClient, loginAdmin, logout, setMustChangePassword }}>
{children}
</AuthContext.Provider>
);
+4
View File
@@ -5,6 +5,10 @@ export type RootStackParamList = {
role: undefined;
};
export type ChangePasswordStackParamList = {
ChangePassword: undefined;
};
export type AuthStackParamList = {
RoleSelect: undefined;
ClientLogin: undefined;
@@ -0,0 +1,191 @@
import React, { useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
ActivityIndicator,
} from "react-native";
import { Feather } from "@expo/vector-icons";
import { useAuth } from "../../auth/AuthContext";
import { useTheme } from "../../context/ThemeContext";
import { changePassword } from "../../api/api";
export default function ChangePasswordScreen() {
const { setMustChangePassword } = useAuth();
const { colors } = useTheme();
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showCurrent, setShowCurrent] = useState(false);
const [showNew, setShowNew] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [loading, setLoading] = useState(false);
const handleChangePassword = async () => {
if (!currentPassword || !newPassword || !confirmPassword) {
Alert.alert("Erreur", "Veuillez remplir tous les champs");
return;
}
if (newPassword.length < 8) {
Alert.alert(
"Erreur",
"Le nouveau mot de passe doit contenir au moins 8 caractères",
);
return;
}
if (newPassword !== confirmPassword) {
Alert.alert("Erreur", "Les nouveaux mots de passe ne correspondent pas");
return;
}
setLoading(true);
try {
const result = await changePassword(currentPassword, newPassword);
if (result.success) {
setMustChangePassword(false);
} else {
Alert.alert("Erreur", result.message || "Erreur inattendue");
}
} finally {
setLoading(false);
}
};
return (
<View style={[styles.container, { backgroundColor: colors.bgPrimary }]}>
<View style={styles.iconWrapper}>
<Feather name="lock" size={48} color="#7c3aed" />
</View>
<Text style={[styles.title, { color: colors.textPrimary }]}>
Changez votre mot de passe
</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>
Pour accéder à l'application, vous devez définir un nouveau mot de
passe personnel.
</Text>
<View style={[styles.inputWrapper, { backgroundColor: colors.bgSecondary, borderColor: colors.border }]}>
<Feather name="lock" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.textPrimary }]}
placeholder="Mot de passe actuel"
placeholderTextColor={colors.textMuted}
secureTextEntry={!showCurrent}
value={currentPassword}
onChangeText={setCurrentPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity onPress={() => setShowCurrent(!showCurrent)}>
<Feather name={showCurrent ? "eye-off" : "eye"} size={18} color={colors.textMuted} />
</TouchableOpacity>
</View>
<View style={[styles.inputWrapper, { backgroundColor: colors.bgSecondary, borderColor: colors.border }]}>
<Feather name="key" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.textPrimary }]}
placeholder="Nouveau mot de passe (min. 8 car.)"
placeholderTextColor={colors.textMuted}
secureTextEntry={!showNew}
value={newPassword}
onChangeText={setNewPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity onPress={() => setShowNew(!showNew)}>
<Feather name={showNew ? "eye-off" : "eye"} size={18} color={colors.textMuted} />
</TouchableOpacity>
</View>
<View style={[styles.inputWrapper, { backgroundColor: colors.bgSecondary, borderColor: colors.border }]}>
<Feather name="check-circle" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.textPrimary }]}
placeholder="Confirmer le nouveau mot de passe"
placeholderTextColor={colors.textMuted}
secureTextEntry={!showConfirm}
value={confirmPassword}
onChangeText={setConfirmPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity onPress={() => setShowConfirm(!showConfirm)}>
<Feather name={showConfirm ? "eye-off" : "eye"} size={18} color={colors.textMuted} />
</TouchableOpacity>
</View>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleChangePassword}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>Confirmer</Text>
)}
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 24,
justifyContent: "center",
},
iconWrapper: {
alignItems: "center",
marginBottom: 16,
},
title: {
fontSize: 22,
fontWeight: "bold",
marginBottom: 10,
textAlign: "center",
},
subtitle: {
fontSize: 14,
marginBottom: 32,
textAlign: "center",
lineHeight: 20,
},
inputWrapper: {
flexDirection: "row",
alignItems: "center",
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 12,
height: 50,
marginBottom: 14,
},
inputIcon: {
marginRight: 8,
},
input: {
flex: 1,
fontSize: 15,
},
button: {
backgroundColor: "#7c3aed",
height: 50,
borderRadius: 8,
justifyContent: "center",
alignItems: "center",
marginTop: 8,
},
buttonDisabled: {
opacity: 0.7,
},
buttonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
});
@@ -60,7 +60,7 @@ const LoginClient = () => {
formData.password,
);
if (result.success && result.access_token) {
await loginClient(result.access_token);
await loginClient(result.access_token, result.user?.must_change_password ?? false);
} else {
const errorMessage =
result.message || "Identifiants incorrects";