Overview
Rodando uses Socket.io to deliver real-time updates during active trips:
Driver assignment notifications
Driver location tracking
Trip status changes (en route, started, completed)
Waiting time penalties
Trip cancellations
All event payloads are fully typed for type safety and auto-completion.
Event Types
The following events are emitted from the server to passengers:
Event Status Change Description assigning_startedpending → assigningSystem started searching for drivers no_drivers_foundassigning → cancelledNo available drivers within timeout driver_assignedassigning → acceptedDriver accepted the trip (basic) driver_accepted_enrichedassigning → acceptedDriver accepted (includes profile/vehicle) driver_en_routeaccepted → en_routeDriver is heading to pickup driver_arrived_pickupen_route → arrivingDriver arrived at pickup location trip_startedarriving → in_progressPassenger onboard, trip started trip_completedin_progress → completedTrip finished successfully trip_cancelled* → cancelledTrip cancelled by driver or system
Event Payloads
All payload types are defined in src/app/core/realtime/trip-realtime.ts:
assigning_started
interface AssigningStartedPayload {
tripId : string ;
at : string ; // ISO 8601 timestamp
previousStatus : 'pending' ;
currentStatus : 'assigning' ;
}
When emitted: Immediately after trip creation when the system begins searching for available drivers.
no_drivers_found
interface NoDriversFoundPayload {
tripId : string ;
at : string ;
reason ?: string | null ; // e.g., "No drivers within 5km"
}
When emitted: After the search timeout (typically 25-60 seconds) if no driver accepts.
The trip status changes to cancelled automatically. The passenger should be prompted to try again or adjust pickup location.
driver_assigned (Basic)
interface DriverAssignedPayloadForPassenger {
tripId : string ;
driverId : string ;
vehicleId : string ;
at : string ;
currentStatus : 'accepted' ;
}
When emitted: When a driver accepts the trip (basic payload without profile data).
driver_accepted_enriched (Enriched)
interface DriverAcceptedEnrichedPayload {
tripId : string ;
at : string ;
currentStatus : 'accepted' ;
driver : DriverSlimForPassenger ;
vehicle : VehicleSlimForPassenger ;
}
interface DriverSlimForPassenger {
id : string ;
name : string ;
profilePictureUrl ?: string | null ;
ratingAvg ?: number | null ; // e.g., 4.8
ratingCount ?: number | null ; // e.g., 142
phone ?: string | null ;
}
interface VehicleSlimForPassenger {
id : string ;
plateNumber ?: string | null ; // e.g., "ABC-1234"
color ?: string | null ; // e.g., "Blanco"
make ?: string | null ; // e.g., "Toyota"
model ?: string | null ; // e.g., "Corolla"
year ?: number | null ; // e.g., 2019
}
When emitted: Alternative to driver_assigned that includes driver profile and vehicle details in one payload.
Example handling:
socket . on ( 'driver_accepted_enriched' , ( payload : DriverAcceptedEnrichedPayload ) => {
console . log ( `Driver ${ payload . driver . name } accepted` );
console . log ( `Vehicle: ${ payload . vehicle . make } ${ payload . vehicle . model } ` );
console . log ( `Plate: ${ payload . vehicle . plateNumber } ` );
console . log ( `Rating: ${ payload . driver . ratingAvg } /5 ( ${ payload . driver . ratingCount } trips)` );
// Update UI with driver info
this . store . setDriver ( payload . driver );
this . store . setVehicle ( payload . vehicle );
});
driver_en_route
interface DriverEnRoutePayload {
tripId : string ;
driverId : string ;
at : string ;
etaMinutes ?: number | null ; // Estimated time to pickup
driverPosition ?: { // Current driver location
lat : number ;
lng : number ;
} | null ;
}
When emitted: When the driver starts driving toward the pickup location.
Example handling:
socket . on ( 'driver_en_route' , ( payload : DriverEnRoutePayload ) => {
console . log ( `Driver is ${ payload . etaMinutes } minutes away` );
if ( payload . driverPosition ) {
// Update driver marker on map
this . updateDriverMarker ( payload . driverPosition );
}
});
The driverPosition is updated periodically (every 5-10 seconds) via separate location events or included in subsequent driver_en_route events.
driver_arrived_pickup
interface DriverArrivedPickupPayload {
tripId : string ;
driverId : string ;
at : string ;
currentStatus : 'arriving' ;
}
When emitted: When the driver is within ~50 meters of the pickup point.
Waiting time penalties may start accruing after a grace period (typically 2-3 minutes). Listen for separate waiting_penalty events.
trip_started
interface TripStartedPayload {
tripId : string ;
driverId : string ;
at : string ;
currentStatus : 'in_progress' ;
}
When emitted: When the driver confirms the passenger is onboard and starts the trip.
Example handling:
socket . on ( 'trip_started' , ( payload : TripStartedPayload ) => {
console . log ( 'Trip started at' , payload . at );
// Show "Trip in progress" UI
this . router . navigate ([ '/trip-progress' ]);
});
trip_completed
interface TripCompletedPayload {
tripId : string ;
driverId : string ;
at : string ;
currentStatus : 'completed' ;
fareTotal : number ; // Final fare amount
currency : string ; // "CUP" or "USD"
}
When emitted: When the driver ends the trip at the destination.
Example handling:
socket . on ( 'trip_completed' , ( payload : TripCompletedPayload ) => {
console . log ( `Trip completed. Total: ${ payload . fareTotal } ${ payload . currency } ` );
// Show rating modal
this . modalCtrl . create ({
component: TripCompletedRatingModalComponent ,
componentProps: {
tripId: payload . tripId ,
fare: payload . fareTotal ,
currency: payload . currency
}
}). then ( modal => modal . present ());
});
trip_cancelled
interface TripCancelledPayload {
tripId : string ;
at : string ;
reason ?: string | null ; // e.g., "Driver cancelled", "Passenger no-show"
currentStatus : 'cancelled' ;
}
When emitted: When the trip is cancelled by driver, passenger, or system.
Possible reasons:
"Driver cancelled" - Driver initiated cancellation
"Passenger no-show" - Passenger didn’t arrive within timeout
"System timeout" - System timeout (e.g., no response from passenger)
"Passenger cancelled" - Passenger cancelled the trip
Socket.io Setup
The app uses a dedicated Socket.io service for managing connections:
src/app/core/realtime/socket.service.ts
import { io , Socket } from 'socket.io-client' ;
import { environment } from '@/environments/environment' ;
export class SocketService {
private socket ?: Socket ;
connect ( accessToken : string ) {
this . socket = io ( environment . socketUrl , {
auth: { token: accessToken },
transports: [ 'websocket' , 'polling' ],
reconnection: true ,
reconnectionDelay: 1000 ,
reconnectionAttempts: 10
});
this . socket . on ( 'connect' , () => {
console . log ( 'Socket connected:' , this . socket ?. id );
});
this . socket . on ( 'disconnect' , ( reason ) => {
console . warn ( 'Socket disconnected:' , reason );
});
this . socket . on ( 'connect_error' , ( err ) => {
console . error ( 'Socket connection error:' , err . message );
});
}
disconnect () {
this . socket ?. disconnect ();
this . socket = undefined ;
}
on < T >( event : string , handler : ( payload : T ) => void ) {
this . socket ?. on ( event , handler );
}
off ( event : string , handler ?: Function ) {
this . socket ?. off ( event , handler );
}
}
Subscribing to Trip Events
Use the TripActiveFacade to handle real-time updates automatically:
src/app/store/trips/trip-active.facade.ts
export class TripActiveFacade {
private socket = inject ( SocketService );
subscribeToTrip ( tripId : string ) {
// Join trip room
this . socket . emit ( 'join_trip' , { tripId });
// Listen for all trip events
this . socket . on < DriverAcceptedEnrichedPayload >(
'driver_accepted_enriched' ,
( payload ) => this . handleDriverAccepted ( payload )
);
this . socket . on < DriverEnRoutePayload >(
'driver_en_route' ,
( payload ) => this . handleDriverEnRoute ( payload )
);
this . socket . on < TripStartedPayload >(
'trip_started' ,
( payload ) => this . handleTripStarted ( payload )
);
this . socket . on < TripCompletedPayload >(
'trip_completed' ,
( payload ) => this . handleTripCompleted ( payload )
);
this . socket . on < TripCancelledPayload >(
'trip_cancelled' ,
( payload ) => this . handleTripCancelled ( payload )
);
}
private handleDriverAccepted ( payload : DriverAcceptedEnrichedPayload ) {
this . store . setDriver ( payload . driver );
this . store . setVehicle ( payload . vehicle );
this . store . setStatus ( 'accepted' );
}
// ... other handlers
}
Complete Example
trip-active.facade.ts
active.component.ts
import { inject , Injectable } from '@angular/core' ;
import { SocketService } from '@/app/core/realtime/socket.service' ;
import {
DriverAcceptedEnrichedPayload ,
DriverEnRoutePayload ,
TripStartedPayload ,
TripCompletedPayload ,
TripCancelledPayload
} from '@/app/core/realtime/trip-realtime' ;
@ Injectable ({ providedIn: 'root' })
export class TripActiveFacade {
private socket = inject ( SocketService );
private store = inject ( TripActiveStore );
subscribeToTrip ( tripId : string ) {
this . socket . on < DriverAcceptedEnrichedPayload >(
'driver_accepted_enriched' ,
( p ) => {
this . store . setDriver ( p . driver );
this . store . setVehicle ( p . vehicle );
this . store . setStatus ( 'accepted' );
}
);
this . socket . on < DriverEnRoutePayload >(
'driver_en_route' ,
( p ) => {
this . store . setEta ( p . etaMinutes );
if ( p . driverPosition ) {
this . store . setDriverPosition ( p . driverPosition );
}
}
);
this . socket . on < TripStartedPayload >(
'trip_started' ,
( p ) => {
this . store . setStatus ( 'in_progress' );
this . router . navigate ([ '/trip-progress' ]);
}
);
this . socket . on < TripCompletedPayload >(
'trip_completed' ,
( p ) => {
this . store . setStatus ( 'completed' );
this . store . setFare ( p . fareTotal , p . currency );
this . showRatingModal ( p . tripId );
}
);
this . socket . on < TripCancelledPayload >(
'trip_cancelled' ,
( p ) => {
this . store . setStatus ( 'cancelled' );
this . showCancellationAlert ( p . reason );
}
);
}
unsubscribeFromTrip ( tripId : string ) {
this . socket . emit ( 'leave_trip' , { tripId });
this . socket . off ( 'driver_accepted_enriched' );
this . socket . off ( 'driver_en_route' );
this . socket . off ( 'trip_started' );
this . socket . off ( 'trip_completed' );
this . socket . off ( 'trip_cancelled' );
}
}
Best Practices
Always unsubscribe Remove event listeners when leaving trip-related components to prevent memory leaks: ngOnDestroy () {
this . socket . off ( 'driver_en_route' );
this . socket . off ( 'trip_completed' );
}
Handle reconnection Socket.io reconnects automatically, but you may need to rejoin trip rooms: this . socket . on ( 'connect' , () => {
const tripId = this . facade . tripId ();
if ( tripId ) {
this . socket . emit ( 'join_trip' , { tripId });
}
});
Type all payloads Always use typed payloads for auto-completion and type safety: // Good
this . socket . on < TripStartedPayload >( 'trip_started' , ( p ) => {
console . log ( p . at ); // Type-safe!
});
// Bad
this . socket . on ( 'trip_started' , ( p : any ) => {
console . log ( p . at ); // No type safety
});
Socket.io events are not queued. If the client is disconnected when an event is emitted, it will be lost. Always fetch trip state via REST API when reconnecting.
The at field in all payloads is an ISO 8601 timestamp (e.g., "2026-03-09T14:32:10.234Z"). Use new Date(payload.at) to parse it.