chore: add notification livreur

This commit is contained in:
2026-03-02 13:15:15 +01:00
parent 6c8af9acd4
commit d78a5743a1
7 changed files with 1063 additions and 1 deletions
+50
View File
@@ -260,3 +260,53 @@ export const getMyAlerts = async (): Promise<{
};
}
};
// ============================================
// NOTIFICATIONS
// ============================================
export type LivreurNotification = {
command_id: number;
type: string;
message: string;
created_at: string;
read: boolean;
};
export const getLivreurNotifications = async (): Promise<{
success: boolean;
notifications?: LivreurNotification[];
unread_count?: number;
total?: number;
error?: string;
}> => {
try {
const { data } = await apiClient.get(`${API}/notifications`);
return {
success: true,
notifications: data.notifications || [],
unread_count: data.unread_count || 0,
total: data.total || 0,
};
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
export const markLivreurNotificationsRead = async (): Promise<{
success: boolean;
error?: string;
}> => {
try {
await apiClient.post(`${API}/notifications/read`);
return { success: true };
} catch (error: any) {
return {
success: false,
error: error.response?.data?.error || "Erreur réseau",
};
}
};
@@ -48,6 +48,13 @@ import TomTomMap, {
TomTomMapRef,
TomTomMarker,
} from "../../components/TomTomMap";
import {
setupDeliveryNotificationChannel,
registerForPushNotificationsAsync,
sendPushTokenToBackend,
addNotificationReceivedListener,
} from "../../services/pushNotifications";
import { getLivreurNotifications } from "../../api/api_delivery";
const { width: SCREEN_WIDTH } = Dimensions.get("window");
const MAP_HEIGHT = 260;
@@ -99,6 +106,10 @@ export default function DashboardScreen() {
const pendingRouteAddress = useRef<string | null>(null);
const { alert, showError, showSuccess, hideAlert } = useAlert();
// Notifications
const [unreadNotifCount, setUnreadNotifCount] = useState(0);
const pushTokenRef = useRef<string | null>(null);
const STATUS_COLORS: Record<string, string> = useMemo(
() => ({
available: colors.success,
@@ -262,6 +273,38 @@ export default function DashboardScreen() {
loadData();
}, [loadData]);
// Push notifications: enregistrement + badge
useEffect(() => {
let notifSub: ReturnType<typeof addNotificationReceivedListener> | null = null;
const setupPush = async () => {
await setupDeliveryNotificationChannel();
const token = await registerForPushNotificationsAsync();
if (token) {
pushTokenRef.current = token;
await sendPushTokenToBackend(token);
}
// Charger le compteur de notifications non lues
const res = await getLivreurNotifications();
if (res.success) {
setUnreadNotifCount(res.unread_count ?? 0);
}
};
setupPush();
// Écouter les nouvelles notifications en foreground
notifSub = addNotificationReceivedListener((_notif) => {
setUnreadNotifCount((prev) => prev + 1);
loadData();
});
return () => {
if (notifSub) notifSub.remove();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Quand GPS devient disponible, rejouer la route en attente
useEffect(() => {
if (lastCoords && pendingRouteAddress.current) {
@@ -1245,6 +1288,25 @@ export default function DashboardScreen() {
fontSize: fontSize.sm,
fontFamily: "monospace",
},
notifBadgeBar: {
flexDirection: "row",
alignItems: "center",
gap: 8,
backgroundColor: "#FF6B0015",
borderLeftWidth: 3,
borderLeftColor: "#FF6B00",
paddingHorizontal: spacing.m,
paddingVertical: 8,
marginHorizontal: spacing.l,
marginTop: spacing.s,
borderRadius: 6,
},
notifBadgeText: {
color: "#FF6B00",
fontSize: fontSize.sm,
fontWeight: "600",
flex: 1,
},
fullscreenStatusBadge: {
flexDirection: "row",
alignItems: "center",
@@ -1418,6 +1480,16 @@ export default function DashboardScreen() {
</View>
</Modal>
{/* ── Badge notifications ── */}
{unreadNotifCount > 0 && (
<View style={styles.notifBadgeBar}>
<Ionicons name="notifications" size={16} color="#FF6B00" />
<Text style={styles.notifBadgeText}>
{unreadNotifCount} nouvelle{unreadNotifCount > 1 ? "s" : ""} commande{unreadNotifCount > 1 ? "s" : ""} assignée{unreadNotifCount > 1 ? "s" : ""}
</Text>
</View>
)}
{/* ── Barre de statut ── */}
<View style={styles.statusBar}>
<Text style={styles.statusLabel}>Mon statut:</Text>
@@ -0,0 +1,125 @@
import * as Notifications from "expo-notifications";
import * as Device from "expo-device";
import Constants from "expo-constants";
import { Platform } from "react-native";
import apiClient from "../api/client";
const LIVREUR_API = "https://uber-stup.club/api/v1/livreur";
// Configuration du comportement des notifications en foreground
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
shouldShowBanner: true,
shouldShowList: true,
}),
});
export async function setupDeliveryNotificationChannel(): Promise<void> {
if (Platform.OS === "android") {
await Notifications.setNotificationChannelAsync("deliveries", {
name: "Livraisons",
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: "#FF6B00",
sound: "default",
enableVibrate: true,
showBadge: true,
});
}
}
// Enregistrer le device pour les push notifications et retourner le token
export async function registerForPushNotificationsAsync(): Promise<
string | null
> {
if (!Device.isDevice) {
console.log(
"⚠️ Push notifications ne fonctionnent pas sur un émulateur",
);
return null;
}
const { status: existingStatus } =
await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== "granted") {
console.log("❌ Permission push notifications refusée");
return null;
}
const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;
if (!projectId) {
console.log("⚠️ projectId non trouvé - push notifications désactivées");
return null;
}
try {
const tokenData = await Notifications.getExpoPushTokenAsync({
projectId,
});
const pushToken = tokenData.data;
console.log("📱 [LIVREUR] Push token:", pushToken);
return pushToken;
} catch (error) {
console.error("❌ [LIVREUR] Erreur récupération push token:", error);
return null;
}
}
// Envoyer le push token au backend (endpoint livreur)
export async function sendPushTokenToBackend(
pushToken: string,
): Promise<boolean> {
try {
const { data } = await apiClient.post(`${LIVREUR_API}/push-token`, {
push_token: pushToken,
});
console.log("✅ [LIVREUR] Push token enregistré sur le backend");
return data.success === true;
} catch (error) {
console.error("❌ [LIVREUR] Erreur envoi push token:", error);
return false;
}
}
// Supprimer le push token du backend (au logout)
export async function removePushTokenFromBackend(): Promise<void> {
try {
await apiClient.delete(`${LIVREUR_API}/push-token`);
console.log("✅ [LIVREUR] Push token supprimé du backend");
} catch (error) {
console.error("❌ [LIVREUR] Erreur suppression push token:", error);
}
}
export function addNotificationReceivedListener(
callback: (notification: Notifications.Notification) => void,
): Notifications.EventSubscription {
return Notifications.addNotificationReceivedListener(callback);
}
export function addNotificationResponseListener(
callback: (response: Notifications.NotificationResponse) => void,
): Notifications.EventSubscription {
return Notifications.addNotificationResponseReceivedListener(callback);
}
export async function setBadgeCount(count: number): Promise<void> {
try {
await Notifications.setBadgeCountAsync(count);
} catch {
// Silencieux
}
}