sg-delivery-partner/app/services/socketService.ts

147 lines
4.0 KiB
TypeScript

import { io, Socket } from 'socket.io-client';
import { Store } from '@reduxjs/toolkit';
import { SocketEvents } from '@utils/constants';
import { DeliveryOffer } from '@interfaces';
import { setDeliveryOffer } from '@store/commonReducers/delivery';
import { RouteNames } from '@utils/constants';
// Must match the REST API base URL (without trailing slash)
const BASE_URL = 'https://157a-202-8-116-13.ngrok-free.app';
type Callback = (data: any) => void;
/**
* Global singleton socket service powered by socket.io-client.
*
* Usage:
* bootstrapSocketService(store, navRef) // call once in App.tsx
* socketService.connect(token) // when user authenticates
* socketService.disconnect() // on logout
*/
class SocketService {
private socket: Socket | null = null;
private store: Store | null = null;
private navigationRef: any = null;
/**
* Must be called once at app startup so the service can dispatch actions
* and navigate globally when a `delivery_offer` event arrives.
* Each parameter is only updated if a non-null value is provided, allowing
* the store and navigationRef to be injected independently.
*/
bootstrap(store: Store | null, navigationRef: any) {
if (store) {
this.store = store;
}
if (navigationRef) {
this.navigationRef = navigationRef;
}
}
/**
* Open the socket connection with JWT auth.
*/
connect(token: string) {
if (this.socket?.connected) {
console.log('[Socket] Already connected');
return;
}
this.socket = io(BASE_URL, {
auth: { token },
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 2000,
});
this.socket.on('connect', () => {
console.log('[Socket] Connected — id:', this.socket?.id);
});
this.socket.on('connect_error', (err: Error) => {
console.error('[Socket] Connection error:', err.message);
});
this.socket.on('disconnect', (reason: string) => {
console.log('[Socket] Disconnected:', reason);
});
// ── Listen for delivery offers ─────────────────────────────────────
this.socket.on(SocketEvents.DeliveryOffer, (data: DeliveryOffer) => {
console.log('[Socket] delivery_offer received:', data);
if (this.store) {
this.store.dispatch(setDeliveryOffer(data));
}
// Navigate to NewJobRequest screen regardless of current screen
if (this.navigationRef?.isReady()) {
this.navigationRef.navigate(RouteNames.NewJobRequest);
}
});
}
/**
* Gracefully close the socket connection.
*/
disconnect() {
if (this.socket) {
this.socket.removeAllListeners();
this.socket.disconnect();
this.socket = null;
console.log('[Socket] Disconnected (manual)');
}
}
/**
* Notify the server that the partner is available for deliveries.
*/
goOnline() {
this.socket?.emit(SocketEvents.GoOnline);
console.log('[Socket] Emitted go_online');
}
/**
* Notify the server that the partner is no longer available.
*/
goOffline() {
this.socket?.emit(SocketEvents.GoOffline);
console.log('[Socket] Emitted go_offline');
}
/**
* Send a location update to the server.
*/
updateLocation(coords: { latitude: number; longitude: number }) {
this.socket?.emit(SocketEvents.LocationUpdate, coords);
}
/**
* Register a custom listener on the underlying socket.
*/
on(event: string, callback: Callback) {
this.socket?.on(event, callback);
}
/**
* Remove a custom listener from the underlying socket.
*/
off(event: string, callback?: Callback) {
if (callback) {
this.socket?.off(event, callback);
} else {
this.socket?.removeAllListeners(event);
}
}
/**
* Whether the socket is currently connected.
*/
get connected(): boolean {
return this.socket?.connected ?? false;
}
}
export const socketService = new SocketService();