From d299601667751671a42521a91b5bbf83351405d0 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Mon, 2 Mar 2026 20:46:07 +0100 Subject: [PATCH] chore: fix notif --- frontend-admin/app.json | 13 +- .../src/navigation/DeliveryNavigator.tsx | 416 ++++++++++++++---- .../src/screens/delivery/DashboardScreen.tsx | 70 --- .../src/services/pushNotifications.ts | 124 ------ mobile/app.json | 16 +- mobile/google-services.json | 29 -- ...is-firebase-adminsdk-fbsvc-51a81342e3.json | 13 - mobile/src/context/NotificationContext.tsx | 106 +---- mobile/src/navigation/ClientNavigator.tsx | 3 - mobile/src/services/pushNotifications.ts | 134 ------ 10 files changed, 348 insertions(+), 576 deletions(-) delete mode 100644 frontend-admin/src/services/pushNotifications.ts delete mode 100644 mobile/google-services.json delete mode 100644 mobile/milieu-nantais-firebase-adminsdk-fbsvc-51a81342e3.json delete mode 100644 mobile/src/services/pushNotifications.ts diff --git a/frontend-admin/app.json b/frontend-admin/app.json index a59cb3ba..800bf641 100644 --- a/frontend-admin/app.json +++ b/frontend-admin/app.json @@ -27,9 +27,7 @@ "android.permission.ACCESS_BACKGROUND_LOCATION", "android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION", - "android.permission.RECEIVE_BOOT_COMPLETED", - "android.permission.VIBRATE", - "android.permission.POST_NOTIFICATIONS" + "android.permission.VIBRATE" ] }, "web": { @@ -38,15 +36,6 @@ "plugins": [ "expo-font", "expo-location", - [ - "expo-notifications", - { - "icon": "./assets/icon.png", - "color": "#000000", - "defaultChannel": "deliveries", - "sounds": [] - } - ], [ "expo-build-properties", { diff --git a/frontend-admin/src/navigation/DeliveryNavigator.tsx b/frontend-admin/src/navigation/DeliveryNavigator.tsx index c22e3306..b914d79e 100644 --- a/frontend-admin/src/navigation/DeliveryNavigator.tsx +++ b/frontend-admin/src/navigation/DeliveryNavigator.tsx @@ -1,11 +1,23 @@ -import React from "react"; -import { TouchableOpacity, View } from "react-native"; +import React, { useState, useEffect, useRef, useCallback } from "react"; +import { + TouchableOpacity, + View, + Text, + Modal, + ScrollView, + StyleSheet, +} from "react-native"; import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; import { Ionicons } from "@expo/vector-icons"; import { useAuth } from "../auth/AuthContext"; import { useTheme } from "../context/ThemeContext"; import { logoutAdmin } from "../api/api_admin"; -import { fontSize, spacing } from "../theme"; +import { + getLivreurNotifications, + markLivreurNotificationsRead, +} from "../api/api_delivery"; +import type { LivreurNotification } from "../api/api_delivery"; +import { fontSize, spacing, borderRadius } from "../theme"; import type { DeliveryTabParamList } from "./types"; import DashboardScreen from "../screens/delivery/DashboardScreen"; @@ -14,99 +26,341 @@ import AlertsScreen from "../screens/delivery/AlertsScreen"; const Tab = createBottomTabNavigator(); +function formatNotifTime(dateStr: string): string { + try { + const diffMs = Date.now() - new Date(dateStr).getTime(); + const diffMin = Math.floor(diffMs / 60000); + if (diffMin < 1) return "À l'instant"; + if (diffMin < 60) return `Il y a ${diffMin} min`; + const diffH = Math.floor(diffMin / 60); + if (diffH < 24) return `Il y a ${diffH}h`; + return `Il y a ${Math.floor(diffH / 24)} jours`; + } catch { + return ""; + } +} + export default function DeliveryNavigator() { const { logout } = useAuth(); const { colors, isDark, toggleTheme } = useTheme(); + const [notifications, setNotifications] = useState([]); + const [unreadCount, setUnreadCount] = useState(0); + const [showModal, setShowModal] = useState(false); + const seenIdsRef = useRef>(new Set()); + const isFirstLoad = useRef(true); + + const fetchNotifications = useCallback(async () => { + const res = await getLivreurNotifications(); + if (!res.success || !res.notifications) return; + setNotifications(res.notifications); + + if (!isFirstLoad.current) { + let newUnread = 0; + for (const n of res.notifications) { + if (!n.read) { + const key = `${n.command_id}-${n.type}-${n.created_at}`; + if (!seenIdsRef.current.has(key)) { + seenIdsRef.current.add(key); + newUnread++; + } + } + } + if (newUnread > 0) { + setUnreadCount((prev) => prev + newUnread); + } + } else { + for (const n of res.notifications) { + const key = `${n.command_id}-${n.type}-${n.created_at}`; + seenIdsRef.current.add(key); + } + setUnreadCount(res.unread_count ?? 0); + isFirstLoad.current = false; + } + }, []); + + useEffect(() => { + fetchNotifications(); + const interval = setInterval(fetchNotifications, 15000); + return () => clearInterval(interval); + }, [fetchNotifications]); + + const openModal = async () => { + setShowModal(true); + if (unreadCount > 0) { + await markLivreurNotificationsRead(); + setUnreadCount(0); + setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); + } + }; + const handleLogout = async () => { await logoutAdmin(); await logout(); }; return ( - ( - - + ( + + {/* Cloche notifications */} + + + {unreadCount > 0 && ( + + + {unreadCount > 9 ? "9+" : unreadCount} + + + )} + + + + + + + + + + ), + tabBarStyle: { + backgroundColor: colors.bgSecondary, + borderTopColor: colors.border, + borderTopWidth: 1, + }, + tabBarActiveTintColor: colors.success, + tabBarInactiveTintColor: colors.textMuted, + tabBarLabelStyle: { fontSize: fontSize.xs }, + }} + > + ( - - + ), + }} + /> + ( - + ), + }} + /> + ( + + ), + }} + /> + + + {/* Modal notifications */} + setShowModal(false)} + > + + + {/* Header modal */} + + + + + Notifications + + + setShowModal(false)}> + + + + + {/* Liste */} + + {notifications.length === 0 ? ( + + Aucune notification + + ) : ( + notifications.map((n, i) => ( + + + {n.message} + + + {formatNotifTime(n.created_at)} + + + )) + )} + - ), - tabBarStyle: { - backgroundColor: colors.bgSecondary, - borderTopColor: colors.border, - borderTopWidth: 1, - }, - tabBarActiveTintColor: colors.success, - tabBarInactiveTintColor: colors.textMuted, - tabBarLabelStyle: { fontSize: fontSize.xs }, - }} - > - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - + + + ); } + +const styles = StyleSheet.create({ + badge: { + position: "absolute", + top: -4, + right: -4, + backgroundColor: "#ef4444", + borderRadius: 10, + minWidth: 18, + height: 18, + justifyContent: "center", + alignItems: "center", + }, + badgeText: { + color: "#fff", + fontSize: 10, + fontWeight: "bold", + }, + overlay: { + flex: 1, + justifyContent: "flex-end", + backgroundColor: "rgba(0,0,0,0.5)", + }, + modalContent: { + height: "70%", + borderTopLeftRadius: borderRadius.lg, + borderTopRightRadius: borderRadius.lg, + }, + modalHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + padding: spacing.l, + borderBottomWidth: 1, + }, + modalTitleRow: { + flexDirection: "row", + alignItems: "center", + gap: spacing.s, + }, + modalTitle: { + fontSize: fontSize.lg, + fontWeight: "bold", + }, + emptyText: { + textAlign: "center", + marginTop: spacing.xl, + fontSize: fontSize.sm, + }, + notifItem: { + padding: spacing.m, + borderBottomWidth: 1, + borderLeftWidth: 3, + }, + notifMessage: { + fontSize: fontSize.sm, + marginBottom: spacing.xs, + }, + notifTime: { + fontSize: fontSize.xs, + }, +}); diff --git a/frontend-admin/src/screens/delivery/DashboardScreen.tsx b/frontend-admin/src/screens/delivery/DashboardScreen.tsx index 43d02fe3..b1ea37d1 100644 --- a/frontend-admin/src/screens/delivery/DashboardScreen.tsx +++ b/frontend-admin/src/screens/delivery/DashboardScreen.tsx @@ -49,13 +49,6 @@ import TomTomMap, { TomTomMarker, } from "../../components/TomTomMap"; import DetailsModal from "../../components/ui/Modal"; -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; @@ -110,10 +103,7 @@ export default function DashboardScreen() { const pendingRouteAddress = useRef(null); const { alert, showError, showSuccess, hideAlert } = useAlert(); - // Notifications - const [unreadNotifCount, setUnreadNotifCount] = useState(0); const [detailsDelivery, setDetailsDelivery] = useState(null); - const pushTokenRef = useRef(null); const STATUS_COLORS: Record = useMemo( () => ({ @@ -281,37 +271,6 @@ export default function DashboardScreen() { loadData(); }, [loadData]); - // Push notifications: enregistrement + badge - useEffect(() => { - let notifSub: ReturnType | 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(() => { @@ -1401,25 +1360,6 @@ export default function DashboardScreen() { fontWeight: "700", }, - 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", @@ -1666,16 +1606,6 @@ export default function DashboardScreen() { - {/* ── Badge notifications ── */} - {unreadNotifCount > 0 && ( - - - - {unreadNotifCount} nouvelle{unreadNotifCount > 1 ? "s" : ""} commande{unreadNotifCount > 1 ? "s" : ""} assignée{unreadNotifCount > 1 ? "s" : ""} - - - )} - {/* ── Barre de statut ── */} Mon statut: diff --git a/frontend-admin/src/services/pushNotifications.ts b/frontend-admin/src/services/pushNotifications.ts deleted file mode 100644 index a944fa2f..00000000 --- a/frontend-admin/src/services/pushNotifications.ts +++ /dev/null @@ -1,124 +0,0 @@ -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 () => ({ - shouldPlaySound: true, - shouldSetBadge: true, - shouldShowBanner: true, - shouldShowList: true, - }), -}); - -export async function setupDeliveryNotificationChannel(): Promise { - 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 { - 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 { - 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 { - try { - await Notifications.setBadgeCountAsync(count); - } catch { - // Silencieux - } -} diff --git a/mobile/app.json b/mobile/app.json index 57695aa7..6ef3d1c6 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -17,7 +17,6 @@ "bundleIdentifier": "com.uberstup.clientpanel" }, "android": { - "googleServicesFile": "./google-services.json", "adaptiveIcon": { "foregroundImage": "./assets/icon.png", "backgroundColor": "#000000" @@ -27,9 +26,7 @@ "package": "com.uberstup.clientpanel", "versionCode": 1, "permissions": [ - "android.permission.RECEIVE_BOOT_COMPLETED", - "android.permission.VIBRATE", - "android.permission.POST_NOTIFICATIONS" + "android.permission.VIBRATE" ] }, "web": { @@ -37,16 +34,7 @@ }, "plugins": [ "expo-font", - "expo-router", - [ - "expo-notifications", - { - "icon": "./assets/icon.png", - "color": "#000000", - "defaultChannel": "orders", - "sounds": [] - } - ] + "expo-router" ], "extra": { "eas": { diff --git a/mobile/google-services.json b/mobile/google-services.json deleted file mode 100644 index bf4356bb..00000000 --- a/mobile/google-services.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "project_info": { - "project_number": "358179533147", - "project_id": "milieu-nantais", - "storage_bucket": "milieu-nantais.firebasestorage.app" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:358179533147:android:20728599f7c862e2362a7e", - "android_client_info": { - "package_name": "com.uberstup.clientpanel" - } - }, - "oauth_client": [], - "api_key": [ - { - "current_key": "AIzaSyDrmjxyOKq5r2VXwfrfiNx-aQrGwNkiyCs" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [] - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/mobile/milieu-nantais-firebase-adminsdk-fbsvc-51a81342e3.json b/mobile/milieu-nantais-firebase-adminsdk-fbsvc-51a81342e3.json deleted file mode 100644 index 67c78230..00000000 --- a/mobile/milieu-nantais-firebase-adminsdk-fbsvc-51a81342e3.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "service_account", - "project_id": "milieu-nantais", - "private_key_id": "51a81342e30ff18ab089745e431631c6e060687f", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDPOZDgOSeDfSpu\n/pCQube8ltjsBCFlIjxuUDpN5d+3GPpH7GZlUnNzScajw9WwLrprvzcAjnaN7Nx5\nyrv/SijLPzVIL9ntniDb/reCh+XycMz36zmuxLzFZcyQmYaYOl3XcVjm5u8GC+l6\nIFeHK55WymhH3/dygssGeLGLJRrTbfkj6YL+CqHM+roomdBlAQNh8Ds+XauyeCZ6\n/fDFlKoyloLKtbvjdxRN0dTP44r93afFFDJjGsLxDyMSmB/JO2cxD+hsndTOnOYI\n8BNyFxukYgTICmSn32CELY1ZTnNpDB7u7VZLwZFgOXxJRp5GblddXfW2KOZa0zxW\nf7c+r8mFAgMBAAECggEABgvjtIsuYjmNeqoeXT6yBARxxWc0cz89vKlGHj5CGhVW\nPjGBZlzyxe8l5uLgshCW+2xbpj5B6s+5uisGc8M97/pC632L8hE+W7TxtVMSTFHq\nxSQdrYILkQXwo1p7cSco1E+4HJqp6EcCM8BMVYMaDmW/B7PbBEIZOvHcGleFkhwy\npGMziXZ9xr8WozrbMCPNpLTLbAZvOxWTC0ZEpj5Bwz2LIwLqiHDSfk74xsLnn+Gp\n7Ed91so7vz9fRJxfT7QlVnHzFZ8YGCQrUl1aq5yy8KuPMn5J1XuQOdy1XbMy1B44\nHQR1+4FnYzrcBgaFgFwGyZk1Mi/sDMgKGcp4HZZKWQKBgQD1NM93aXbeEaAD6gzH\nz8HCCy1GRJTS9/r0mpx4i3kqUlSZkh0zO7UGUXENZ4WOF5OTH4YtqNuOpzUxZhy2\nTkXLlyT6GgWlQYcWkum6FGmFpRPeWxImT2ClhL68+UoqzkwDho3ZWMgaZZfc//rI\ngoVDYRyMsh3A/qBngUKPzYKHqQKBgQDYWL/B7YhR37gatRjPBJBx955+Hrgts8m4\nTlDySOm7LrMvQ12XCEzj9ylgmE2ah+/PG2AEWutLQBiYSCiN+/PUFXHWlYHmfdJg\nX/IQAfVm9UxcTYLklJ460tyX5IJ0rGpUeSF4mkxhX71mig02RUuwmdNTnCzZsbtd\naCfqBR6sfQKBgGr6D0lZibJ4ngcXJVxHF5FArw/o+8TOy33LtlghAUulf6NAS9z+\nP4vyHBBtCadkQc3+AtwIv0ENUferRPuESo738fnrQGtIm1cN4Up9fUwBKRnpQZHV\nL6UGtPBdEi56sk8XdOtOAH8Ds62HQDIaP2mWBI6dZr28WN7PVerHDhuBAoGAIjP9\ntPIRoCgHWimRT4FAONRV+UhwA8GtvXacM4G1egNLDsjOrgcA6PV1VDEf67NcBVkl\nl+qr0kzC1qhpyPCxPrFZOvyN+duge94PTdqRaoKTXPBgJjUcwt39RAI7Yai0csBn\nu0Jhmu9g6SogJplT+wqGr2w8ZvpDTeQek0/V5p0CgYBC4inrAe28IPPqY7NtBHX/\nE939v79U+pA3Xq1WyLfgLd6QrUHsRMO5xvObSUlNrA2U+QQiLkygDnxE0VK9qdYK\nN/zsbZZQdTuDSy1NcMJ50h0ed4CrmYn+mJS3rGlj0Ov9vPa4Ppf/E8Of1uwFFiB4\ng3QpbaO3cuvxiu4EHf25Lg==\n-----END PRIVATE KEY-----\n", - "client_email": "firebase-adminsdk-fbsvc@milieu-nantais.iam.gserviceaccount.com", - "client_id": "102638886608570330042", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40milieu-nantais.iam.gserviceaccount.com", - "universe_domain": "googleapis.com" -} diff --git a/mobile/src/context/NotificationContext.tsx b/mobile/src/context/NotificationContext.tsx index f9a3e446..83203463 100644 --- a/mobile/src/context/NotificationContext.tsx +++ b/mobile/src/context/NotificationContext.tsx @@ -10,15 +10,6 @@ import React, { import { getClientNotifications, markNotificationsRead } from "../api/api"; import type { ClientNotification } from "../api/api"; import { getToken } from "../auth/tokenStorage"; -import { - registerForPushNotificationsAsync, - sendPushTokenToBackend, - setupNotificationChannel, - addNotificationReceivedListener, - addNotificationResponseListener, - setBadgeCount, - getLastNotificationResponse, -} from "../services/pushNotifications"; interface ToastData { message: string; @@ -32,7 +23,6 @@ interface NotificationContextType { refreshNotifications: () => Promise; toast: ToastData | null; clearToast: () => void; - pushToken: string | null; navigateToOrder: number | null; clearNavigateToOrder: () => void; } @@ -44,7 +34,6 @@ const NotificationContext = createContext({ refreshNotifications: async () => {}, toast: null, clearToast: () => {}, - pushToken: null, navigateToOrder: null, clearNavigateToOrder: () => {}, }); @@ -57,35 +46,30 @@ function getToastType( notifType: string, ): "success" | "error" | "warning" | "info" { switch (notifType) { - case "order_confirmed": - case "order_approved": + case "assigned": return "success"; - case "order_en_route": - case "order_assigned": + case "en_route": + case "support": return "info"; - case "order_delivered": - return "warning"; + case "livre": + return "success"; + case "failed": + return "error"; default: return "info"; } } export function NotificationProvider({ children }: { children: ReactNode }) { - const [notifications, setNotifications] = useState( - [], - ); + const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [toast, setToast] = useState(null); - const [pushToken, setPushToken] = useState(null); const [navigateToOrder, setNavigateToOrder] = useState(null); const seenIdsRef = useRef>(new Set()); const isFirstLoadRef = useRef(true); const clearToast = useCallback(() => setToast(null), []); - const clearNavigateToOrder = useCallback( - () => setNavigateToOrder(null), - [], - ); + const clearNavigateToOrder = useCallback(() => setNavigateToOrder(null), []); const showToast = useCallback( (message: string, type: ToastData["type"]) => { @@ -95,68 +79,6 @@ export function NotificationProvider({ children }: { children: ReactNode }) { [], ); - useEffect(() => { - let isMounted = true; - - const initPush = async () => { - const token = await getToken(); - if (!token) return; - - await setupNotificationChannel(); - - const expoPushToken = await registerForPushNotificationsAsync(); - if (expoPushToken && isMounted) { - setPushToken(expoPushToken); - await sendPushTokenToBackend(expoPushToken); - } - - const lastResponse = await getLastNotificationResponse(); - if (lastResponse && isMounted) { - const data = lastResponse.notification.request.content.data; - if (data?.command_id) { - setNavigateToOrder(data.command_id as number); - } - } - }; - - initPush(); - - return () => { - isMounted = false; - }; - }, []); - - useEffect(() => { - const receivedSub = addNotificationReceivedListener((notification) => { - const { title, body } = notification.request.content; - const data = notification.request.content.data; - - // Afficher un toast pour la notification push en foreground - if (body) { - const notifType = (data?.type as string) || ""; - showToast(body, getToastType(notifType)); - } - - // Rafraîchir la liste des notifications - fetchNotifications(); - }); - - // L'utilisateur tap sur une notification - const responseSub = addNotificationResponseListener((response) => { - const data = response.notification.request.content.data; - if (data?.command_id) { - setNavigateToOrder(data.command_id as number); - } - // Rafraîchir les notifications - fetchNotifications(); - }); - - return () => { - receivedSub.remove(); - responseSub.remove(); - }; - }, [showToast]); - const fetchNotifications = useCallback(async () => { const token = await getToken(); if (!token) return; @@ -168,11 +90,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) { setNotifications(newNotifs); setUnreadCount(response.unread_count); - // Mettre à jour le badge de l'app - await setBadgeCount(response.unread_count); - if (!isFirstLoadRef.current) { - // Détecter les NOUVELLES notifications non lues for (const notif of newNotifs) { if (notif.read) continue; const key = `${notif.command_id}-${notif.type}-${notif.created_at}`; @@ -182,7 +100,6 @@ export function NotificationProvider({ children }: { children: ReactNode }) { } } } else { - // Premier chargement : enregistrer tous les IDs sans toast for (const notif of newNotifs) { const key = `${notif.command_id}-${notif.type}-${notif.created_at}`; seenIdsRef.current.add(key); @@ -191,7 +108,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) { } } } catch { - // Silencieux en cas d'erreur + // Silencieux } }, [showToast]); @@ -206,8 +123,6 @@ export function NotificationProvider({ children }: { children: ReactNode }) { setNotifications((prev) => prev.map((n) => ({ ...n, read: true })), ); - // Remettre le badge à 0 - await setBadgeCount(0); } } catch { // Silencieux @@ -230,7 +145,6 @@ export function NotificationProvider({ children }: { children: ReactNode }) { refreshNotifications: fetchNotifications, toast, clearToast, - pushToken, navigateToOrder, clearNavigateToOrder, }} diff --git a/mobile/src/navigation/ClientNavigator.tsx b/mobile/src/navigation/ClientNavigator.tsx index 01e9d012..c4cc1a20 100644 --- a/mobile/src/navigation/ClientNavigator.tsx +++ b/mobile/src/navigation/ClientNavigator.tsx @@ -18,7 +18,6 @@ import { useAuth } from "../auth/AuthContext"; import { useTheme } from "../context/ThemeContext"; import { logoutUser } from "../api/api"; import type { ClientNotification } from "../api/api"; -import { removePushTokenFromBackend } from "../services/pushNotifications"; import { fontSize, spacing, borderRadius } from "../theme"; import Toast from "../components/ui/Toast"; import type { ClientTabParamList, ClientStackParamList } from "./types"; @@ -58,7 +57,6 @@ function ClientTabs() { markAllRead, toast, clearToast, - pushToken, navigateToOrder, clearNavigateToOrder, } = useNotifications(); @@ -76,7 +74,6 @@ function ClientTabs() { }, [navigateToOrder, navigation, clearNavigateToOrder]); const handleLogout = async () => { - try { await removePushTokenFromBackend(); } catch { /* silencieux */ } await logoutUser(); await logout(); }; diff --git a/mobile/src/services/pushNotifications.ts b/mobile/src/services/pushNotifications.ts deleted file mode 100644 index 7d45067c..00000000 --- a/mobile/src/services/pushNotifications.ts +++ /dev/null @@ -1,134 +0,0 @@ -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 V1 = "https://uber-stup.club/api/v1"; - -// Configuration du comportement des notifications en foreground -Notifications.setNotificationHandler({ - handleNotification: async () => ({ - shouldPlaySound: true, - shouldSetBadge: true, - shouldShowBanner: true, - shouldShowList: true, - }), -}); - -export async function setupNotificationChannel(): Promise { - if (Platform.OS === "android") { - await Notifications.setNotificationChannelAsync("orders", { - name: "Commandes", - importance: Notifications.AndroidImportance.MAX, - vibrationPattern: [0, 250, 250, 250], - lightColor: "#7C3AED", - 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; - } - - // Vérifier/demander les permissions - 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; - } - - // Récupérer le projectId Expo - 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("📱 Push token:", pushToken); - return pushToken; - } catch (error) { - console.error("❌ Erreur récupération push token:", error); - return null; - } -} - -// Envoyer le push token au backend -export async function sendPushTokenToBackend( - pushToken: string, -): Promise { - try { - const { data } = await apiClient.post(`${V1}/push-token`, { - push_token: pushToken, - platform: Platform.OS, - }); - console.log("✅ Push token enregistré sur le backend"); - return data.success === true; - } catch (error) { - console.error("❌ Erreur envoi push token au backend:", error); - return false; - } -} - -// Supprimer le push token du backend (au logout) -export async function removePushTokenFromBackend(): Promise { - try { - await apiClient.delete(`${V1}/push-token`); - console.log("✅ Push token supprimé du backend"); - } catch (error) { - console.error("❌ Erreur suppression push token:", error); - } -} - -// Listeners pour les notifications -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); -} - -// Mettre à jour le badge -export async function setBadgeCount(count: number): Promise { - try { - await Notifications.setBadgeCountAsync(count); - } catch { - // Silencieux - certains appareils ne supportent pas les badges - } -} - -// Récupérer la dernière notification qui a ouvert l'app -export async function getLastNotificationResponse(): Promise { - return await Notifications.getLastNotificationResponseAsync(); -}