Overview
The TripFacade is the primary service for managing the complete trip lifecycle from a driver’s perspective. It orchestrates the TripStore, TripApiService, and DriverWsService to handle:
- WebSocket event subscriptions (offers, assignments, trip status changes)
- Offer acceptance/rejection
- Trip phase transitions (arriving, in progress, completed)
- Countdown timers for offers and waiting periods
- Waiting time penalties and dynamic fare updates
- UI modal management
Constructor and WebSocket Subscriptions
The facade automatically subscribes to key WebSocket events in its constructor:
onOffer$
Triggered when a new trip offer is sent to the driver.
this.ws.onOffer$.subscribe(async (offer) => {
// Checks if assignment already handled (prevents duplicates)
// Verifies driver is in 'idle' phase
// Sets active trip ID and assignment ID
// Refreshes trip data
// Starts countdown timer
// Opens offer modal
});
Offer payload:
assignmentId: Unique assignment identifier
tripId: Trip identifier
expiresAt: ISO timestamp when offer expires
ttlSec: Time-to-live in seconds
onDriverAssigned$
Triggered when driver is confirmed as assigned to trip.
this.ws.onDriverAssigned$.subscribe(async (p) => {
// Sets active trip ID
// Updates phase to 'assigned'
// Stores passenger information
// Refreshes trip data
});
onArrivingStarted$
Triggered when driver starts navigating to pickup location.
this.ws.onArrivingStarted$.subscribe(async (ev) => {
// Updates phase to 'arriving'
// Closes offer modal if still open
// Refreshes trip data
});
onTripStarted$
Triggered when trip officially begins (passenger picked up).
this.ws.onTripStarted$.subscribe(async (p) => {
// Updates phase to 'in_progress'
// Refreshes trip data
});
onTripCompleted$
Triggered when trip is completed.
this.ws.onTripCompleted$.subscribe(async (p) => {
// Updates phase to 'completed'
// Refreshes trip data
// Keeps state for summary view
});
onTripCancelled$
Triggered when trip is cancelled by passenger or system.
this.ws.onTripCancelled$.subscribe(async (p) => {
// Updates phase to 'cancelled'
// Refreshes trip data
});
State Readers
vm()
Returns the complete current state snapshot.
modalVm
modalVm = this.store.modalVm
Computed signal containing formatted data for the offer modal.
trip$
Computed signal for current trip data.
phase$
phase$ = this.store.phase
Computed signal for current trip phase.
remainingSec$
remainingSec$ = this.store.remainingSec
Computed signal for offer countdown remaining seconds.
modalOpen$
modalOpen$ = this.store.modalOpen
Computed signal for modal open state.
Public Methods
acceptOffer()
async acceptOffer(): Promise<void>
Accepts the current trip offer.
Process:
- Validates
offerAssignmentId exists
- Stops countdown timer
- Calls
tripsApi.acceptAssignment(assignmentId)
- Refreshes trip data
- Marks assignment as handled (prevents duplicate actions)
- Clears offer assignment ID
- Updates phase to
'assigned'
- Closes offer modal
- Navigates to
/trips/active
Error handling: Sets error state with message from API or fallback message.
Example:
import { Component, inject } from '@angular/core';
import { TripFacade } from '@/app/store/trip/trip.facade';
@Component({
selector: 'app-offer-modal',
template: `
<button (click)="facade.acceptOffer()" [disabled]="facade.vm().status === 'loading'">
Aceptar viaje
</button>
`
})
export class OfferModalComponent {
facade = inject(TripFacade);
}
declineOffer()
async declineOffer(): Promise<void>
Declines the current trip offer.
Process:
- Validates
offerAssignmentId exists
- Stops countdown timer
- Calls
tripsApi.declineAssignment(assignmentId)
- Refreshes trip data
- Marks assignment as handled
- Clears offer assignment ID
- Updates phase to
'idle'
- Closes offer modal
Error handling: Sets error state with API message.
Example:
<button (click)="facade.declineOffer()" class="decline-btn">
Rechazar
</button>
refreshTrip()
async refreshTrip(): Promise<void>
Fetches latest trip data from API.
Process:
- Gets
activeTripId from state
- Calls
tripsApi.getById(id)
- Updates trip in store
- Extracts and sets fare information (base fare, currency)
Fare extraction logic:
const baseFare = trip.fareEstimatedTotal ?? trip.fareFinalTotal ?? null;
const currency = trip.fareFinalCurrency ?? trip.fareEstimatedCurrency ?? 'CUP';
this.store.setInitialFare(baseFare, currency);
markArrivedPickup()
async markArrivedPickup(): Promise<void>
Marks driver as arrived at pickup location.
Taken from state.activeTripId
Resolved from trip data or auth store
Process:
- Validates
tripId and driverId
- Calls
tripsApi.markArrivedPickup(tripId, driverId)
- Extracts
arrivedPickupAt timestamp from response
- Updates state with arrival timestamp
- Sets phase to
'arriving'
- Updates trip data
- Starts waiting timer with
startWaitingTimer()
Example:
<button
(click)="facade.markArrivedPickup()"
[disabled]="facade.phase$() !== 'assigned'"
>
He llegado
</button>
startTrip()
async startTrip(): Promise<void>
Starts the trip (passenger has boarded).
Process:
- Validates
tripId and driverId
- Calls
tripsApi.startTrip(tripId, driverId)
- Updates phase to
'in_progress'
- Stops waiting timer
- Refreshes trip data
Example:
<button
(click)="facade.startTrip()"
[disabled]="facade.phase$() !== 'arriving'"
>
Iniciar viaje
</button>
completeTrip()
async completeTrip(): Promise<void>
Completes the trip and processes payment.
Process:
- Calculates final fare breakdown:
base: Initial fare estimate
extra: Waiting penalty charges
finalTotal: Complete amount to collect
- Opens
DriverConfirmOrderModalComponent with fare details
- Waits for driver confirmation
- If confirmed, constructs payload:
{
driverId: string,
extraFees: number | null,
waitingTimeMinutes: number | null,
waitingReason: string | null
}
- Calls
tripsApi.completeTrip(tripId, payload)
- Updates phase to
'completed'
- Updates trip data with final results
Fare calculation:
const base = state.baseFare ?? trip?.fareEstimatedTotal ?? trip?.fareFinalTotal ?? null;
const extra = state.waitingExtraFare ?? 0;
const finalTotal = trip?.fareFinalTotal ?? state.liveFare ?? (base + extra);
Example:
<button
(click)="facade.completeTrip()"
[disabled]="facade.phase$() !== 'in_progress'"
>
Finalizar y cobrar
</button>
clearAll()
Resets all state and stops timers. Call when driver needs to return to idle state.
clearAll() {
this.stopCountdown();
this.store.reset();
}
Timer Management
Countdown Timer (Offer Expiration)
startCountdown(opts)
ISO timestamp when offer expires
Time-to-live in seconds (fallback if no expiresAtIso)
startCountdown(opts: { expiresAtIso?: string; ttlSec?: number }): void
Starts a 1-second interval timer that:
- Calculates remaining seconds from expiration time
- Updates
remainingSec in store every second
- Auto-closes modal when timer reaches 0
stopCountdown()
Stops and clears the countdown timer subscription.
Waiting Timer (Pickup Location)
startWaitingTimer()
private startWaitingTimer(): void
Starts a 1-second interval timer that:
- Increments
waitingSeconds in store
- Checks for penalty threshold (default: 5 seconds in demo)
- Applies waiting penalty when threshold reached:
const extra = Math.max(1, Math.round(base * 0.05)); // 5% or minimum 1
this.store.applyWaitingPenalty(extra, text);
this.store.setWaitingReason('Espera prolongada en el punto de recogida');
- Shows
DriverWaitingPenaltyModalComponent when penalty applied
stopWaitingTimer()
private stopWaitingTimer(): void
Stops the waiting timer.
Modal Management
The facade works with several modal components:
TripAssignedModalComponent
Shown when new offer arrives, displays trip details and countdown.
DriverConfirmOrderModalComponent
Shown before completing trip, displays fare breakdown and collects payment confirmation.
Props:
passengerName: Passenger display name
originLabel, destinationLabel: Formatted addresses
distanceText, durationText: Formatted trip metrics
base: Base fare amount
waitingExtra: Additional waiting charges
total: Final total amount
currency: Currency code
waitingSeconds: Total waiting time
waitingReason: Reason for waiting charge
paymentMode: Payment method (e.g., ‘cash’)
DriverWaitingPenaltyModalComponent
Shown when waiting penalty is first applied, alerts driver of additional charges.
Complete Usage Example
import { Component, inject, OnInit, OnDestroy } from '@angular/core';
import { TripFacade } from '@/app/store/trip/trip.facade';
@Component({
selector: 'app-active-trip',
template: `
<div class="trip-container">
<h2>{{ phaseName() }}</h2>
@if (facade.trip$(); as trip) {
<div class="trip-details">
<p>Origen: {{ facade.modalVm().originLabel }}</p>
<p>Destino: {{ facade.modalVm().destinationLabel }}</p>
<p>Distancia: {{ facade.modalVm().distanceText }}</p>
<p>Duración: {{ facade.modalVm().durationText }}</p>
</div>
<div class="fare-info">
<p>Tarifa base: {{ facade.vm().baseFare }} {{ trip.fareFinalCurrency }}</p>
<p>Tarifa actual: {{ facade.vm().liveFare }} {{ trip.fareFinalCurrency }}</p>
@if (facade.vm().waitingPenaltyApplied) {
<p class="penalty">+ {{ facade.vm().waitingExtraFare }} (espera)</p>
}
</div>
<div class="actions">
@if (facade.phase$() === 'assigned') {
<button (click)="facade.markArrivedPickup()">
Marcar llegada
</button>
}
@if (facade.phase$() === 'arriving') {
<p>Esperando: {{ facade.vm().waitingSeconds }}s</p>
<button (click)="facade.startTrip()">
Iniciar viaje
</button>
}
@if (facade.phase$() === 'in_progress') {
<button (click)="facade.completeTrip()">
Finalizar y cobrar
</button>
}
</div>
}
</div>
`
})
export class ActiveTripComponent implements OnDestroy {
facade = inject(TripFacade);
phaseName() {
const phase = this.facade.phase$();
const map = {
idle: 'Sin viaje activo',
assigned: 'Viaje asignado',
arriving: 'Llegando al punto de recogida',
in_progress: 'Viaje en curso',
completed: 'Viaje completado',
cancelled: 'Viaje cancelado'
};
return map[phase] || phase;
}
ngOnDestroy() {
// Clean up when component is destroyed
// Note: usually you want to keep state until trip is completed
// this.facade.clearAll();
}
}
State Flow Diagram
idle
↓ (WebSocket: onOffer$)
assigned → (acceptOffer)
↓
assigned → (markArrivedPickup + startWaitingTimer)
↓
arriving → (startTrip + stopWaitingTimer)
↓
in_progress → (completeTrip)
↓
completed → (clearAll)
↓
idle