chore: fix notif
This commit is contained in:
+1
-12
@@ -27,9 +27,7 @@
|
|||||||
"android.permission.ACCESS_BACKGROUND_LOCATION",
|
"android.permission.ACCESS_BACKGROUND_LOCATION",
|
||||||
"android.permission.ACCESS_COARSE_LOCATION",
|
"android.permission.ACCESS_COARSE_LOCATION",
|
||||||
"android.permission.ACCESS_FINE_LOCATION",
|
"android.permission.ACCESS_FINE_LOCATION",
|
||||||
"android.permission.RECEIVE_BOOT_COMPLETED",
|
"android.permission.VIBRATE"
|
||||||
"android.permission.VIBRATE",
|
|
||||||
"android.permission.POST_NOTIFICATIONS"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
@@ -38,15 +36,6 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-font",
|
"expo-font",
|
||||||
"expo-location",
|
"expo-location",
|
||||||
[
|
|
||||||
"expo-notifications",
|
|
||||||
{
|
|
||||||
"icon": "./assets/icon.png",
|
|
||||||
"color": "#000000",
|
|
||||||
"defaultChannel": "deliveries",
|
|
||||||
"sounds": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"expo-build-properties",
|
"expo-build-properties",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import {
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
Modal,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
} from "react-native";
|
||||||
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "../auth/AuthContext";
|
||||||
import { useTheme } from "../context/ThemeContext";
|
import { useTheme } from "../context/ThemeContext";
|
||||||
import { logoutAdmin } from "../api/api_admin";
|
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 type { DeliveryTabParamList } from "./types";
|
||||||
|
|
||||||
import DashboardScreen from "../screens/delivery/DashboardScreen";
|
import DashboardScreen from "../screens/delivery/DashboardScreen";
|
||||||
@@ -14,99 +26,341 @@ import AlertsScreen from "../screens/delivery/AlertsScreen";
|
|||||||
|
|
||||||
const Tab = createBottomTabNavigator<DeliveryTabParamList>();
|
const Tab = createBottomTabNavigator<DeliveryTabParamList>();
|
||||||
|
|
||||||
|
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() {
|
export default function DeliveryNavigator() {
|
||||||
const { logout } = useAuth();
|
const { logout } = useAuth();
|
||||||
const { colors, isDark, toggleTheme } = useTheme();
|
const { colors, isDark, toggleTheme } = useTheme();
|
||||||
|
|
||||||
|
const [notifications, setNotifications] = useState<LivreurNotification[]>([]);
|
||||||
|
const [unreadCount, setUnreadCount] = useState(0);
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const seenIdsRef = useRef<Set<string>>(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 () => {
|
const handleLogout = async () => {
|
||||||
await logoutAdmin();
|
await logoutAdmin();
|
||||||
await logout();
|
await logout();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<>
|
||||||
screenOptions={{
|
<Tab.Navigator
|
||||||
headerStyle: { backgroundColor: colors.bgSecondary },
|
screenOptions={{
|
||||||
headerTintColor: colors.textWhite,
|
headerStyle: { backgroundColor: colors.bgSecondary },
|
||||||
headerRight: () => (
|
headerTintColor: colors.textWhite,
|
||||||
<View
|
headerRight: () => (
|
||||||
style={{
|
<View
|
||||||
flexDirection: "row",
|
style={{
|
||||||
alignItems: "center",
|
flexDirection: "row",
|
||||||
marginRight: spacing.l,
|
alignItems: "center",
|
||||||
}}
|
marginRight: spacing.l,
|
||||||
>
|
gap: spacing.m,
|
||||||
<TouchableOpacity
|
}}
|
||||||
onPress={toggleTheme}
|
|
||||||
style={{ marginRight: spacing.m }}
|
|
||||||
>
|
>
|
||||||
|
{/* Cloche notifications */}
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={openModal}
|
||||||
|
style={{ position: "relative" }}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name="notifications-outline"
|
||||||
|
size={24}
|
||||||
|
color={colors.textSecondary}
|
||||||
|
/>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<View style={styles.badge}>
|
||||||
|
<Text style={styles.badgeText}>
|
||||||
|
{unreadCount > 9 ? "9+" : unreadCount}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity onPress={toggleTheme}>
|
||||||
|
<Ionicons
|
||||||
|
name={isDark ? "sunny-outline" : "moon-outline"}
|
||||||
|
size={22}
|
||||||
|
color={colors.textSecondary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={handleLogout}>
|
||||||
|
<Ionicons
|
||||||
|
name="log-out-outline"
|
||||||
|
size={24}
|
||||||
|
color={colors.textSecondary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
tabBarStyle: {
|
||||||
|
backgroundColor: colors.bgSecondary,
|
||||||
|
borderTopColor: colors.border,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
},
|
||||||
|
tabBarActiveTintColor: colors.success,
|
||||||
|
tabBarInactiveTintColor: colors.textMuted,
|
||||||
|
tabBarLabelStyle: { fontSize: fontSize.xs },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab.Screen
|
||||||
|
name="Dashboard"
|
||||||
|
component={DashboardScreen}
|
||||||
|
options={{
|
||||||
|
title: "Livraisons",
|
||||||
|
tabBarIcon: ({ color, size }) => (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={isDark ? "sunny-outline" : "moon-outline"}
|
name="navigate-outline"
|
||||||
size={22}
|
size={size}
|
||||||
color={colors.textSecondary}
|
color={color}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
),
|
||||||
<TouchableOpacity onPress={handleLogout}>
|
}}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="Stats"
|
||||||
|
component={StatsScreen}
|
||||||
|
options={{
|
||||||
|
title: "Stats",
|
||||||
|
tabBarIcon: ({ color, size }) => (
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="log-out-outline"
|
name="stats-chart-outline"
|
||||||
size={24}
|
size={size}
|
||||||
color={colors.textSecondary}
|
color={color}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="Alerts"
|
||||||
|
component={AlertsScreen}
|
||||||
|
options={{
|
||||||
|
title: "Alertes",
|
||||||
|
tabBarIcon: ({ color, size }) => (
|
||||||
|
<Ionicons
|
||||||
|
name="alert-circle-outline"
|
||||||
|
size={size}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tab.Navigator>
|
||||||
|
|
||||||
|
{/* Modal notifications */}
|
||||||
|
<Modal
|
||||||
|
visible={showModal}
|
||||||
|
animationType="slide"
|
||||||
|
transparent
|
||||||
|
onRequestClose={() => setShowModal(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.overlay}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.modalContent,
|
||||||
|
{ backgroundColor: colors.bgPrimary },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* Header modal */}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.modalHeader,
|
||||||
|
{ borderBottomColor: colors.border },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.modalTitleRow}>
|
||||||
|
<Ionicons
|
||||||
|
name="notifications-outline"
|
||||||
|
size={20}
|
||||||
|
color={colors.success}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.modalTitle,
|
||||||
|
{ color: colors.textPrimary },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Notifications
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity onPress={() => setShowModal(false)}>
|
||||||
|
<Ionicons
|
||||||
|
name="close"
|
||||||
|
size={24}
|
||||||
|
color={colors.textSecondary}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Liste */}
|
||||||
|
<ScrollView style={{ flex: 1 }}>
|
||||||
|
{notifications.length === 0 ? (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.emptyText,
|
||||||
|
{ color: colors.textMuted },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Aucune notification
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
notifications.map((n, i) => (
|
||||||
|
<View
|
||||||
|
key={`${n.command_id}-${n.type}-${n.created_at}-${i}`}
|
||||||
|
style={[
|
||||||
|
styles.notifItem,
|
||||||
|
{
|
||||||
|
borderBottomColor: colors.border,
|
||||||
|
borderLeftColor: n.read
|
||||||
|
? "transparent"
|
||||||
|
: colors.success,
|
||||||
|
backgroundColor: n.read
|
||||||
|
? "transparent"
|
||||||
|
: colors.success + "10",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.notifMessage,
|
||||||
|
{ color: colors.textPrimary },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{n.message}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.notifTime,
|
||||||
|
{ color: colors.textMuted },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{formatNotifTime(n.created_at)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
),
|
</View>
|
||||||
tabBarStyle: {
|
</Modal>
|
||||||
backgroundColor: colors.bgSecondary,
|
</>
|
||||||
borderTopColor: colors.border,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
},
|
|
||||||
tabBarActiveTintColor: colors.success,
|
|
||||||
tabBarInactiveTintColor: colors.textMuted,
|
|
||||||
tabBarLabelStyle: { fontSize: fontSize.xs },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tab.Screen
|
|
||||||
name="Dashboard"
|
|
||||||
component={DashboardScreen}
|
|
||||||
options={{
|
|
||||||
title: "Livraisons",
|
|
||||||
tabBarIcon: ({ color, size }) => (
|
|
||||||
<Ionicons
|
|
||||||
name="navigate-outline"
|
|
||||||
size={size}
|
|
||||||
color={color}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name="Stats"
|
|
||||||
component={StatsScreen}
|
|
||||||
options={{
|
|
||||||
title: "Stats",
|
|
||||||
tabBarIcon: ({ color, size }) => (
|
|
||||||
<Ionicons
|
|
||||||
name="stats-chart-outline"
|
|
||||||
size={size}
|
|
||||||
color={color}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name="Alerts"
|
|
||||||
component={AlertsScreen}
|
|
||||||
options={{
|
|
||||||
title: "Alertes",
|
|
||||||
tabBarIcon: ({ color, size }) => (
|
|
||||||
<Ionicons
|
|
||||||
name="alert-circle-outline"
|
|
||||||
size={size}
|
|
||||||
color={color}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tab.Navigator>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -49,13 +49,6 @@ import TomTomMap, {
|
|||||||
TomTomMarker,
|
TomTomMarker,
|
||||||
} from "../../components/TomTomMap";
|
} from "../../components/TomTomMap";
|
||||||
import DetailsModal from "../../components/ui/Modal";
|
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 { width: SCREEN_WIDTH } = Dimensions.get("window");
|
||||||
const MAP_HEIGHT = 260;
|
const MAP_HEIGHT = 260;
|
||||||
@@ -110,10 +103,7 @@ export default function DashboardScreen() {
|
|||||||
const pendingRouteAddress = useRef<string | null>(null);
|
const pendingRouteAddress = useRef<string | null>(null);
|
||||||
const { alert, showError, showSuccess, hideAlert } = useAlert();
|
const { alert, showError, showSuccess, hideAlert } = useAlert();
|
||||||
|
|
||||||
// Notifications
|
|
||||||
const [unreadNotifCount, setUnreadNotifCount] = useState(0);
|
|
||||||
const [detailsDelivery, setDetailsDelivery] = useState<EnrichedDelivery | null>(null);
|
const [detailsDelivery, setDetailsDelivery] = useState<EnrichedDelivery | null>(null);
|
||||||
const pushTokenRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, string> = useMemo(
|
const STATUS_COLORS: Record<string, string> = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -281,37 +271,6 @@ export default function DashboardScreen() {
|
|||||||
loadData();
|
loadData();
|
||||||
}, [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
|
// Quand GPS devient disponible, rejouer la route en attente
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1401,25 +1360,6 @@ export default function DashboardScreen() {
|
|||||||
fontWeight: "700",
|
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: {
|
fullscreenStatusBadge: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -1666,16 +1606,6 @@ export default function DashboardScreen() {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
</DetailsModal>
|
</DetailsModal>
|
||||||
|
|
||||||
{/* ── 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 ── */}
|
{/* ── Barre de statut ── */}
|
||||||
<View style={styles.statusBar}>
|
<View style={styles.statusBar}>
|
||||||
<Text style={styles.statusLabel}>Mon statut:</Text>
|
<Text style={styles.statusLabel}>Mon statut:</Text>
|
||||||
|
|||||||
@@ -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<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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-14
@@ -17,7 +17,6 @@
|
|||||||
"bundleIdentifier": "com.uberstup.clientpanel"
|
"bundleIdentifier": "com.uberstup.clientpanel"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"googleServicesFile": "./google-services.json",
|
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/icon.png",
|
"foregroundImage": "./assets/icon.png",
|
||||||
"backgroundColor": "#000000"
|
"backgroundColor": "#000000"
|
||||||
@@ -27,9 +26,7 @@
|
|||||||
"package": "com.uberstup.clientpanel",
|
"package": "com.uberstup.clientpanel",
|
||||||
"versionCode": 1,
|
"versionCode": 1,
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"android.permission.RECEIVE_BOOT_COMPLETED",
|
"android.permission.VIBRATE"
|
||||||
"android.permission.VIBRATE",
|
|
||||||
"android.permission.POST_NOTIFICATIONS"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
@@ -37,16 +34,7 @@
|
|||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-font",
|
"expo-font",
|
||||||
"expo-router",
|
"expo-router"
|
||||||
[
|
|
||||||
"expo-notifications",
|
|
||||||
{
|
|
||||||
"icon": "./assets/icon.png",
|
|
||||||
"color": "#000000",
|
|
||||||
"defaultChannel": "orders",
|
|
||||||
"sounds": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
],
|
||||||
"extra": {
|
"extra": {
|
||||||
"eas": {
|
"eas": {
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -10,15 +10,6 @@ import React, {
|
|||||||
import { getClientNotifications, markNotificationsRead } from "../api/api";
|
import { getClientNotifications, markNotificationsRead } from "../api/api";
|
||||||
import type { ClientNotification } from "../api/api";
|
import type { ClientNotification } from "../api/api";
|
||||||
import { getToken } from "../auth/tokenStorage";
|
import { getToken } from "../auth/tokenStorage";
|
||||||
import {
|
|
||||||
registerForPushNotificationsAsync,
|
|
||||||
sendPushTokenToBackend,
|
|
||||||
setupNotificationChannel,
|
|
||||||
addNotificationReceivedListener,
|
|
||||||
addNotificationResponseListener,
|
|
||||||
setBadgeCount,
|
|
||||||
getLastNotificationResponse,
|
|
||||||
} from "../services/pushNotifications";
|
|
||||||
|
|
||||||
interface ToastData {
|
interface ToastData {
|
||||||
message: string;
|
message: string;
|
||||||
@@ -32,7 +23,6 @@ interface NotificationContextType {
|
|||||||
refreshNotifications: () => Promise<void>;
|
refreshNotifications: () => Promise<void>;
|
||||||
toast: ToastData | null;
|
toast: ToastData | null;
|
||||||
clearToast: () => void;
|
clearToast: () => void;
|
||||||
pushToken: string | null;
|
|
||||||
navigateToOrder: number | null;
|
navigateToOrder: number | null;
|
||||||
clearNavigateToOrder: () => void;
|
clearNavigateToOrder: () => void;
|
||||||
}
|
}
|
||||||
@@ -44,7 +34,6 @@ const NotificationContext = createContext<NotificationContextType>({
|
|||||||
refreshNotifications: async () => {},
|
refreshNotifications: async () => {},
|
||||||
toast: null,
|
toast: null,
|
||||||
clearToast: () => {},
|
clearToast: () => {},
|
||||||
pushToken: null,
|
|
||||||
navigateToOrder: null,
|
navigateToOrder: null,
|
||||||
clearNavigateToOrder: () => {},
|
clearNavigateToOrder: () => {},
|
||||||
});
|
});
|
||||||
@@ -57,35 +46,30 @@ function getToastType(
|
|||||||
notifType: string,
|
notifType: string,
|
||||||
): "success" | "error" | "warning" | "info" {
|
): "success" | "error" | "warning" | "info" {
|
||||||
switch (notifType) {
|
switch (notifType) {
|
||||||
case "order_confirmed":
|
case "assigned":
|
||||||
case "order_approved":
|
|
||||||
return "success";
|
return "success";
|
||||||
case "order_en_route":
|
case "en_route":
|
||||||
case "order_assigned":
|
case "support":
|
||||||
return "info";
|
return "info";
|
||||||
case "order_delivered":
|
case "livre":
|
||||||
return "warning";
|
return "success";
|
||||||
|
case "failed":
|
||||||
|
return "error";
|
||||||
default:
|
default:
|
||||||
return "info";
|
return "info";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotificationProvider({ children }: { children: ReactNode }) {
|
export function NotificationProvider({ children }: { children: ReactNode }) {
|
||||||
const [notifications, setNotifications] = useState<ClientNotification[]>(
|
const [notifications, setNotifications] = useState<ClientNotification[]>([]);
|
||||||
[],
|
|
||||||
);
|
|
||||||
const [unreadCount, setUnreadCount] = useState(0);
|
const [unreadCount, setUnreadCount] = useState(0);
|
||||||
const [toast, setToast] = useState<ToastData | null>(null);
|
const [toast, setToast] = useState<ToastData | null>(null);
|
||||||
const [pushToken, setPushToken] = useState<string | null>(null);
|
|
||||||
const [navigateToOrder, setNavigateToOrder] = useState<number | null>(null);
|
const [navigateToOrder, setNavigateToOrder] = useState<number | null>(null);
|
||||||
const seenIdsRef = useRef<Set<string>>(new Set());
|
const seenIdsRef = useRef<Set<string>>(new Set());
|
||||||
const isFirstLoadRef = useRef(true);
|
const isFirstLoadRef = useRef(true);
|
||||||
|
|
||||||
const clearToast = useCallback(() => setToast(null), []);
|
const clearToast = useCallback(() => setToast(null), []);
|
||||||
const clearNavigateToOrder = useCallback(
|
const clearNavigateToOrder = useCallback(() => setNavigateToOrder(null), []);
|
||||||
() => setNavigateToOrder(null),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const showToast = useCallback(
|
const showToast = useCallback(
|
||||||
(message: string, type: ToastData["type"]) => {
|
(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 fetchNotifications = useCallback(async () => {
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
@@ -168,11 +90,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
|||||||
setNotifications(newNotifs);
|
setNotifications(newNotifs);
|
||||||
setUnreadCount(response.unread_count);
|
setUnreadCount(response.unread_count);
|
||||||
|
|
||||||
// Mettre à jour le badge de l'app
|
|
||||||
await setBadgeCount(response.unread_count);
|
|
||||||
|
|
||||||
if (!isFirstLoadRef.current) {
|
if (!isFirstLoadRef.current) {
|
||||||
// Détecter les NOUVELLES notifications non lues
|
|
||||||
for (const notif of newNotifs) {
|
for (const notif of newNotifs) {
|
||||||
if (notif.read) continue;
|
if (notif.read) continue;
|
||||||
const key = `${notif.command_id}-${notif.type}-${notif.created_at}`;
|
const key = `${notif.command_id}-${notif.type}-${notif.created_at}`;
|
||||||
@@ -182,7 +100,6 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Premier chargement : enregistrer tous les IDs sans toast
|
|
||||||
for (const notif of newNotifs) {
|
for (const notif of newNotifs) {
|
||||||
const key = `${notif.command_id}-${notif.type}-${notif.created_at}`;
|
const key = `${notif.command_id}-${notif.type}-${notif.created_at}`;
|
||||||
seenIdsRef.current.add(key);
|
seenIdsRef.current.add(key);
|
||||||
@@ -191,7 +108,7 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Silencieux en cas d'erreur
|
// Silencieux
|
||||||
}
|
}
|
||||||
}, [showToast]);
|
}, [showToast]);
|
||||||
|
|
||||||
@@ -206,8 +123,6 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
|||||||
setNotifications((prev) =>
|
setNotifications((prev) =>
|
||||||
prev.map((n) => ({ ...n, read: true })),
|
prev.map((n) => ({ ...n, read: true })),
|
||||||
);
|
);
|
||||||
// Remettre le badge à 0
|
|
||||||
await setBadgeCount(0);
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Silencieux
|
// Silencieux
|
||||||
@@ -230,7 +145,6 @@ export function NotificationProvider({ children }: { children: ReactNode }) {
|
|||||||
refreshNotifications: fetchNotifications,
|
refreshNotifications: fetchNotifications,
|
||||||
toast,
|
toast,
|
||||||
clearToast,
|
clearToast,
|
||||||
pushToken,
|
|
||||||
navigateToOrder,
|
navigateToOrder,
|
||||||
clearNavigateToOrder,
|
clearNavigateToOrder,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { useAuth } from "../auth/AuthContext";
|
|||||||
import { useTheme } from "../context/ThemeContext";
|
import { useTheme } from "../context/ThemeContext";
|
||||||
import { logoutUser } from "../api/api";
|
import { logoutUser } from "../api/api";
|
||||||
import type { ClientNotification } from "../api/api";
|
import type { ClientNotification } from "../api/api";
|
||||||
import { removePushTokenFromBackend } from "../services/pushNotifications";
|
|
||||||
import { fontSize, spacing, borderRadius } from "../theme";
|
import { fontSize, spacing, borderRadius } from "../theme";
|
||||||
import Toast from "../components/ui/Toast";
|
import Toast from "../components/ui/Toast";
|
||||||
import type { ClientTabParamList, ClientStackParamList } from "./types";
|
import type { ClientTabParamList, ClientStackParamList } from "./types";
|
||||||
@@ -58,7 +57,6 @@ function ClientTabs() {
|
|||||||
markAllRead,
|
markAllRead,
|
||||||
toast,
|
toast,
|
||||||
clearToast,
|
clearToast,
|
||||||
pushToken,
|
|
||||||
navigateToOrder,
|
navigateToOrder,
|
||||||
clearNavigateToOrder,
|
clearNavigateToOrder,
|
||||||
} = useNotifications();
|
} = useNotifications();
|
||||||
@@ -76,7 +74,6 @@ function ClientTabs() {
|
|||||||
}, [navigateToOrder, navigation, clearNavigateToOrder]);
|
}, [navigateToOrder, navigation, clearNavigateToOrder]);
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try { await removePushTokenFromBackend(); } catch { /* silencieux */ }
|
|
||||||
await logoutUser();
|
await logoutUser();
|
||||||
await logout();
|
await logout();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<void> {
|
|
||||||
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<boolean> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<Notifications.NotificationResponse | null> {
|
|
||||||
return await Notifications.getLastNotificationResponseAsync();
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user