chore: add change password
This commit is contained in:
+27
-2
@@ -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
@@ -40,7 +40,7 @@
|
||||
"expo-notifications",
|
||||
{
|
||||
"icon": "./assets/icon.png",
|
||||
"color": "#7C3AED",
|
||||
"color": "#000000",
|
||||
"defaultChannel": "orders",
|
||||
"sounds": []
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface UserResponse {
|
||||
point?: number;
|
||||
point_zipette?: number; // ✅ AJOUTER CETTE LIGNE
|
||||
amende?: number;
|
||||
must_change_password?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user