chore: add notification livreur
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user