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://8415-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();