279 lines
6.7 KiB
Markdown
279 lines
6.7 KiB
Markdown
# React Native Customer Tracking Integration Guide
|
|
|
|
This guide outlines how to connect and implement live driver location tracking in your React Native Customer App.
|
|
|
|
---
|
|
|
|
## 📦 Dependencies
|
|
|
|
Install the required packages in your React Native project:
|
|
|
|
```bash
|
|
npm install socket.io-client react-native-maps
|
|
```
|
|
|
|
---
|
|
|
|
## 🔌 1. Tracking Socket Service (`trackingSocket.ts`)
|
|
|
|
Create a socket helper to handle the namespace connection (`/tracking`) and events:
|
|
|
|
```typescript
|
|
import { io, Socket } from "socket.io-client";
|
|
|
|
const SOCKET_URL = "http://<YOUR_BACKEND_IP>:8000/tracking";
|
|
|
|
export interface DriverLocationUpdate {
|
|
orderId: string;
|
|
latitude: number;
|
|
longitude: number;
|
|
heading?: number;
|
|
speedKph?: number;
|
|
recordedAt: string;
|
|
}
|
|
|
|
export interface TrackingStatusUpdate {
|
|
orderId: string;
|
|
status: "OUT_FOR_DELIVERY" | "DELIVERED";
|
|
timestamp: string;
|
|
}
|
|
|
|
class TrackingSocketService {
|
|
private socket: Socket | null = null;
|
|
|
|
connect(token: string): Socket {
|
|
if (this.socket?.connected) {
|
|
return this.socket;
|
|
}
|
|
|
|
this.socket = io(SOCKET_URL, {
|
|
auth: { token },
|
|
transports: ["websocket"],
|
|
reconnection: true,
|
|
});
|
|
|
|
this.socket.on("connect", () => {
|
|
console.log("Connected to Tracking WebSocket Namespace");
|
|
});
|
|
|
|
this.socket.on("connect_error", (error) => {
|
|
console.error("Socket Connection Error:", error);
|
|
});
|
|
|
|
return this.socket;
|
|
}
|
|
|
|
joinOrderTracking(orderId: string) {
|
|
this.socket?.emit("join_order_tracking", { orderId });
|
|
}
|
|
|
|
leaveOrderTracking(orderId: string) {
|
|
this.socket?.emit("leave_order_tracking", { orderId });
|
|
}
|
|
|
|
onDriverLocationUpdate(callback: (data: DriverLocationUpdate) => void) {
|
|
this.socket?.on("driver_location_update", callback);
|
|
}
|
|
|
|
onOrderStatusUpdate(callback: (data: TrackingStatusUpdate) => void) {
|
|
this.socket?.on("order_tracking_status", callback);
|
|
}
|
|
|
|
disconnect(orderId: string) {
|
|
if (this.socket) {
|
|
this.leaveOrderTracking(orderId);
|
|
this.socket.off("driver_location_update");
|
|
this.socket.off("order_tracking_status");
|
|
this.socket.disconnect();
|
|
this.socket = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const trackingSocketService = new TrackingSocketService();
|
|
```
|
|
|
|
---
|
|
|
|
## 🪝 2. Customer Tracking Hook (`useOrderTracking.ts`)
|
|
|
|
Manage room state subscription, events listening, and automatic teardown on component unmount:
|
|
|
|
```typescript
|
|
import { useEffect, useState } from "react";
|
|
import { trackingSocketService, DriverLocationUpdate } from "./trackingSocket";
|
|
|
|
export function useOrderTracking(orderId: string, userJwtToken: string) {
|
|
const [driverLocation, setDriverLocation] =
|
|
useState<DriverLocationUpdate | null>(null);
|
|
const [orderStatus, setOrderStatus] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!orderId || !userJwtToken) return;
|
|
|
|
const socket = trackingSocketService.connect(userJwtToken);
|
|
|
|
trackingSocketService.joinOrderTracking(orderId);
|
|
|
|
trackingSocketService.onDriverLocationUpdate((data) => {
|
|
if (data.orderId === orderId) {
|
|
setDriverLocation(data);
|
|
}
|
|
});
|
|
|
|
trackingSocketService.onOrderStatusUpdate((data) => {
|
|
if (data.orderId === orderId) {
|
|
setOrderStatus(data.status);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
trackingSocketService.disconnect(orderId);
|
|
};
|
|
}, [orderId, userJwtToken]);
|
|
|
|
return { driverLocation, orderStatus };
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🗺️ 3. Tracking Screen Component
|
|
|
|
Render Google Maps with the destination address, live driver position, and auto-camera alignment:
|
|
|
|
```tsx
|
|
import React, { useRef, useEffect } from "react";
|
|
import { StyleSheet, View, Text } from "react-native";
|
|
import MapView, { Marker, PROVIDER_GOOGLE } from "react-native-maps";
|
|
import { useOrderTracking } from "./useOrderTracking";
|
|
|
|
interface TrackingScreenProps {
|
|
orderId: string;
|
|
token: string;
|
|
customerDropoff: { latitude: number; longitude: number };
|
|
}
|
|
|
|
export default function OrderTrackingScreen({
|
|
orderId,
|
|
token,
|
|
customerDropoff,
|
|
}: TrackingScreenProps) {
|
|
const mapRef = useRef<MapView>(null);
|
|
const { driverLocation, orderStatus } = useOrderTracking(orderId, token);
|
|
|
|
useEffect(() => {
|
|
if (driverLocation && mapRef.current) {
|
|
mapRef.current.animateToRegion(
|
|
{
|
|
latitude: driverLocation.latitude,
|
|
longitude: driverLocation.longitude,
|
|
latitudeDelta: 0.01,
|
|
longitudeDelta: 0.01,
|
|
},
|
|
1000,
|
|
);
|
|
}
|
|
}, [driverLocation]);
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<MapView
|
|
ref={mapRef}
|
|
provider={PROVIDER_GOOGLE}
|
|
style={styles.map}
|
|
initialRegion={{
|
|
latitude: customerDropoff.latitude,
|
|
longitude: customerDropoff.longitude,
|
|
latitudeDelta: 0.02,
|
|
longitudeDelta: 0.02,
|
|
}}
|
|
>
|
|
{/* Customer dropoff point */}
|
|
<Marker
|
|
coordinate={customerDropoff}
|
|
title="Delivery Location"
|
|
pinColor="red"
|
|
/>
|
|
|
|
{/* Live Driver */}
|
|
{driverLocation && (
|
|
<Marker
|
|
coordinate={{
|
|
latitude: driverLocation.latitude,
|
|
longitude: driverLocation.longitude,
|
|
}}
|
|
title="Your Delivery Driver"
|
|
anchor={{ x: 0.5, y: 0.5 }}
|
|
rotation={driverLocation.heading ?? 0}
|
|
>
|
|
<View style={styles.driverMarker}>
|
|
<Text style={styles.driverEmoji}>🛵</Text>
|
|
</View>
|
|
</Marker>
|
|
)}
|
|
</MapView>
|
|
|
|
<View style={styles.statusContainer}>
|
|
<Text style={styles.statusTitle}>
|
|
Status:{" "}
|
|
{orderStatus === "OUT_FOR_DELIVERY"
|
|
? "Out for Delivery 🛵"
|
|
: orderStatus || "Preparing"}
|
|
</Text>
|
|
{driverLocation?.speedKph !== undefined && (
|
|
<Text style={styles.statusSubtitle}>
|
|
Driver speed: {Math.round(driverLocation.speedKph)} km/h
|
|
</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
...StyleSheet.absoluteFillObject,
|
|
justifyContent: "flex-end",
|
|
alignItems: "center",
|
|
},
|
|
map: {
|
|
...StyleSheet.absoluteFillObject,
|
|
},
|
|
driverMarker: {
|
|
backgroundColor: "#FFF",
|
|
padding: 6,
|
|
borderRadius: 20,
|
|
borderWidth: 2,
|
|
borderColor: "#FF7F00",
|
|
elevation: 4,
|
|
},
|
|
driverEmoji: {
|
|
fontSize: 20,
|
|
},
|
|
statusContainer: {
|
|
position: "absolute",
|
|
bottom: 40,
|
|
backgroundColor: "white",
|
|
padding: 16,
|
|
borderRadius: 12,
|
|
width: "90%",
|
|
elevation: 5,
|
|
shadowColor: "#000",
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.25,
|
|
shadowRadius: 3.84,
|
|
},
|
|
statusTitle: {
|
|
fontSize: 16,
|
|
fontWeight: "bold",
|
|
color: "#333",
|
|
},
|
|
statusSubtitle: {
|
|
fontSize: 14,
|
|
color: "#666",
|
|
marginTop: 4,
|
|
},
|
|
});
|
|
```
|