Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/route-access-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ The app now has an interactive phone-first driver flow:
6. `ROUTES_FOUND` returns zero or more selectable route choices. Each choice carries company guidance, route access identifiers, and its own short-lived route-scoped driver token.
7. From the driver's point of view, multi-company assignments are just multiple routes; each route card shows the company/shop and route metadata attached to that route.
8. The app records required `LOCATION_INFORMATION` and `PERSONAL_INFORMATION` consent through the selected route token, then loads assigned-route detail for each route choice.
9. Loaded routes render in `Pending`, `In Progress`, and `Completed` tabs; a route card can open detail or start delivery.
10. Delivery start requests foreground location permission and moves to `delivery_active` only when permission is granted.
9. Every created child route renders in `Ready` until delivery starts. Driver assignment does not change this execution state; a route card can open detail or start delivery.
10. Delivery start requests foreground location permission, records `ROUTE_STARTED`, and moves the route to `In progress` only when permission is granted.
11. Live tracking starts at the company/pickup step, then proceeds through ordered stops without presenting turn-by-turn instruction UI.
12. Each stop can play a local area tip, open stop details, capture required proof photo, and record optional delivery notes, location-specific tips, and additional notes.
13. Delivery finish stops continuous location, records or queues `ROUTE_COMPLETED`, and discards route-scoped local retry items only after route completion is recorded.
13. Delivery finish stops continuous location, records or queues `ROUTE_COMPLETED`, moves the route to `Completed`, and discards route-scoped local retry items only after route completion is recorded.
14. An authoritative empty route list, `NOT_FOUND`, or a missing prior assignment removes that route from the app and clears only route-scoped cache. Network errors retain the last safe cache.
15. `NO_ASSIGNED_ROUTE`, `DISABLED`, `BLOCKED`, and API errors stay in safe user-visible states without exposing other tenant/driver data.

Expand Down
254 changes: 120 additions & 134 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@
"@expo/vector-icons": "^15.0.2",
"@maplibre/maplibre-react-native": "^11.3.4",
"@react-native-async-storage/async-storage": "2.2.0",
"expo": "~56.0.15",
"expo": "~56.0.16",
"expo-camera": "~56.0.8",
"expo-dev-client": "~56.0.22",
"expo-dev-client": "~56.0.23",
"expo-font": "~56.0.7",
"expo-image-picker": "~56.0.20",
"expo-location": "~56.0.20",
"expo-notifications": "~56.0.20",
"expo-image-picker": "~56.0.21",
"expo-location": "~56.0.21",
"expo-notifications": "~56.0.21",
"expo-secure-store": "~56.0.4",
"expo-speech": "~56.0.3",
"expo-status-bar": "~56.0.4",
"expo-task-manager": "~56.0.21",
"expo-task-manager": "~56.0.22",
"libphonenumber-js": "1.13.6",
"react": "19.2.3",
"react-native": "0.85.3",
Expand Down
22 changes: 9 additions & 13 deletions src/app/AppRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2217,8 +2217,8 @@ function MyRoutesPage({
</View>
<View style={styles.routeCardStatusGroup}>
<StatusChip
tone={getChipTone(activeRouteStatus ?? 'upcoming')}
label={formatRouteStatus(activeRouteStatus ?? 'upcoming')}
tone={getChipTone(activeRouteStatus ?? 'ready')}
label={formatRouteStatus(activeRouteStatus ?? 'ready')}
/>
<Text style={styles.routeToggleText}>{isRouteCardExpanded ? '−' : '+'}</Text>
</View>
Expand Down Expand Up @@ -2580,11 +2580,11 @@ function RouteSessionScreen({
const depotIsProcessing = routeStatus === 'active' && currentNavigationStepIndex === COMPANY_STEP_INDEX;
const depotMeta = depotIsProcessing ? 'Pickup' : routeStatus === 'completed' || currentNavigationStepIndex > COMPANY_STEP_INDEX ? 'Done' : undefined;
const depotMetaTone = depotIsProcessing ? 'blue' : 'green';
const depotState = routeStatus === 'upcoming' || depotIsProcessing ? 'current' : 'completed';
const depotState = routeStatus === 'ready' || depotIsProcessing ? 'current' : 'completed';
const currentTaskTitle = isCompanyStep ? 'Company Pickup' : stop === null ? 'Next Stop' : `Stop ${stop.sequence}`;
const currentTaskAddress = isCompanyStep ? company?.pickupGuidance ?? 'Pickup point' : stop === null ? 'Stop address' : formatStopAddress(stop);
const currentTaskPayment = stop === null ? null : formatAssignedRoutePaymentStatus(stop.normalizedPaymentStatus);
const primaryProgressAction = routeStatus === 'upcoming'
const primaryProgressAction = routeStatus === 'ready'
? { disabled: isStartingRoute, label: 'Start Session', loading: isStartingRoute, onPress: onStartRoute }
: routeStatus === 'active' && allStopsCompleted
? { disabled: isFinishingRoute, label: 'Finish Route', loading: isFinishingRoute, onPress: onFinishRoute }
Expand Down Expand Up @@ -2754,7 +2754,7 @@ function LiveTrackingScreen({
<View style={styles.trackingMetrics}>
<MetricBlock label="Distance" value={formatAssignedRouteDistance(route.routeMetrics)} />
<MetricBlock label="ETA" value={formatAssignedRouteDuration(route.routeMetrics)} />
<MetricBlock label="Status" value={routeStatus === 'active' ? 'In progress' : 'Pending'} tone={routeStatus === 'active' ? 'green' : 'neutral'} />
<MetricBlock label="Status" value={routeStatus === 'active' ? 'In progress' : formatRouteStatus(routeStatus)} tone={routeStatus === 'active' ? 'green' : 'neutral'} />
</View>
{payment !== null ? (
<View style={styles.paymentInlineRow}>
Expand Down Expand Up @@ -3806,7 +3806,7 @@ function getRouteStatus(deliveryStartResult: DeliveryStartResult | null, deliver
return 'active';
}

return 'upcoming';
return 'ready';
}

function formatRouteStatus(status: RouteStatus): string {
Expand All @@ -3815,10 +3815,8 @@ function formatRouteStatus(status: RouteStatus): string {
return 'In progress';
case 'completed':
return 'Completed';
case 'unfinished':
return 'Unfinished';
case 'upcoming':
return 'Pending';
case 'ready':
return 'Ready';
}
}

Expand Down Expand Up @@ -3923,9 +3921,7 @@ function getChipTone(status: RouteStatus): 'blue' | 'green' | 'neutral' {
return 'blue';
case 'completed':
return 'green';
case 'unfinished':
return 'neutral';
case 'upcoming':
case 'ready':
return 'neutral';
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/app/routeListBehavior.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe('routes list behavior', () => {
assert.match(source, /setCollapsedRouteKey\(\(value\) => value === activeRouteCollapseKey \? null : activeRouteCollapseKey\)/u);
assert.match(source, /<Text numberOfLines=\{1\} style=\{styles\.cardTitle\}>\{activeSession\.route\.name\}<\/Text>/u);
assert.match(source, /<Text numberOfLines=\{1\} style=\{styles\.helperText\}>\{activeSession\.route\.deliveryDate\}<\/Text>/u);
assert.match(source, /label=\{formatRouteStatus\(activeRouteStatus \?\? 'upcoming'\)\}/u);
assert.match(source, /label=\{formatRouteStatus\(activeRouteStatus \?\? 'ready'\)\}/u);
assert.doesNotMatch(source, /<DataRow label="Company"/u);
assert.doesNotMatch(source, /<DataRow label="Route"/u);
assert.match(source, /\{getInitials\(activeSession\.route\.name\)\}/u);
Expand All @@ -99,7 +99,7 @@ describe('routes list behavior', () => {
assert.match(noAssignedRouteSource, /resetRouteProgress\(\)/u);
assert.match(noAssignedRouteSource, /clearCachedRouteAccess\(\)/u);
assert.doesNotMatch(noAssignedRouteSource, /setMessage\(/u);
assert.doesNotMatch(source, /Signed in\. No current or upcoming route is assigned/u);
assert.doesNotMatch(source, /Signed in\. No current or ready route is assigned/u);
assert.match(source, /assignedRouteResult\.kind !== 'no_assigned_route'/u);
assert.match(appStateRefreshSource, /screen === 'mainTabs'/u);
assert.doesNotMatch(appStateRefreshSource, /selectedMainTab === 'routes'/u);
Expand Down
9 changes: 4 additions & 5 deletions src/domain/driverFlow/driverFlow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('driver app MVP flow', () => {
getMvpScenarioScreens().map((screen) => screen.title),
[
'Login / Driver Verification',
'Upcoming Routes',
'My Routes',
'Route Details',
'Route Session',
'Live Tracking',
Expand All @@ -69,11 +69,10 @@ describe('driver app MVP flow', () => {
);
});

it('shows route lists by English delivery status tabs', () => {
it('shows route lists by lifecycle status tabs', () => {
assert.deepEqual(getMvpRouteTabs(), [
{ id: 'upcoming', label: 'Pending' },
{ id: 'active', label: 'In Progress' },
{ id: 'unfinished', label: 'Unfinished' },
{ id: 'ready', label: 'Ready' },
{ id: 'active', label: 'In progress' },
{ id: 'completed', label: 'Completed' },
]);
});
Expand Down
13 changes: 6 additions & 7 deletions src/domain/driverFlow/driverFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export type MvpScenarioScreen = {
};

export type MvpRouteTab = {
id: 'active' | 'completed' | 'unfinished' | 'upcoming';
label: 'Completed' | 'In Progress' | 'Pending' | 'Unfinished';
id: 'active' | 'completed' | 'ready';
label: 'Completed' | 'In progress' | 'Ready';
};

export type StopCompletionProofField = {
Expand Down Expand Up @@ -75,9 +75,9 @@ export function getMvpScenarioScreens(): MvpScenarioScreen[] {
},
{
id: 'routeList',
title: 'Upcoming Routes',
title: 'My Routes',
purpose:
'Show current, unfinished, and future assigned routes from nearest date to farthest, grouped into Pending, In Progress, Unfinished, and Completed tabs.',
'Show assigned routes from nearest date to farthest, grouped into Ready, In progress, and Completed tabs.',
primaryAction: 'Start Session',
},
// routeDetail is metadata for the read-only preview entry; the live operational screen is routeSession.
Expand Down Expand Up @@ -135,9 +135,8 @@ export function getMvpScenarioScreens(): MvpScenarioScreen[] {

export function getMvpRouteTabs(): MvpRouteTab[] {
return [
{ id: 'upcoming', label: 'Pending' },
{ id: 'active', label: 'In Progress' },
{ id: 'unfinished', label: 'Unfinished' },
{ id: 'ready', label: 'Ready' },
{ id: 'active', label: 'In progress' },
{ id: 'completed', label: 'Completed' },
];
}
Expand Down
4 changes: 2 additions & 2 deletions src/domain/route/assignedRoute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ describe('driver assigned route UX flow', () => {
assert.deepEqual(result, {
flowState: 'consent_recorded',
kind: 'no_assigned_route',
message: 'No current or upcoming route is available for this driver and route context.',
message: 'No ready or in-progress route is available for this driver and route context.',
});
});

Expand Down Expand Up @@ -330,7 +330,7 @@ describe('driver assigned route UX flow', () => {
it('defines tab-level synthetic validation scenarios with safe coordinates and OSRM evidence expectations', () => {
assert.deepEqual(
assignedRouteValidationScenarios.map((scenario) => scenario.tab),
['upcoming', 'active', 'completed'],
['ready', 'active', 'completed'],
);

for (const scenario of assignedRouteValidationScenarios) {
Expand Down
2 changes: 1 addition & 1 deletion src/domain/route/assignedRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export async function loadAssignedRouteAfterConsent(
return {
flowState: 'consent_recorded',
kind: 'no_assigned_route',
message: 'No current or upcoming route is available for this driver and route context.',
message: 'No ready or in-progress route is available for this driver and route context.',
};
}

Expand Down
14 changes: 7 additions & 7 deletions src/domain/route/assignedRouteValidationScenarios.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AssignedRoute } from './assignedRoute';

export type AssignedRouteValidationTab = 'active' | 'completed' | 'upcoming';
export type AssignedRouteValidationTab = 'active' | 'completed' | 'ready';

export type AssignedRouteValidationScenario = {
expectedEvidence: {
Expand All @@ -23,13 +23,13 @@ export const assignedRouteValidationScenarios: AssignedRouteValidationScenario[]
hasOsrmMetrics: true,
safeForOperationalSmoke: true,
},
id: 'validation-upcoming-osrm-ready',
label: 'Upcoming tab — synthetic Toronto route with OSRM geometry and route-level metrics',
id: 'validation-ready-osrm-route',
label: 'Ready tab — synthetic Toronto route with OSRM geometry and route-level metrics',
route: buildValidationRoute({
deliveryDate: '2026-06-04',
id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1',
metrics: { distanceMeters: 6120.4, durationSeconds: 1680 },
name: '[TEST] OSRM Upcoming Route',
name: '[TEST] OSRM Ready Route',
stops: [
validationStop({
address1: '100 King St W',
Expand All @@ -38,7 +38,7 @@ export const assignedRouteValidationScenarios: AssignedRouteValidationScenario[]
longitude: -79.3817,
orderName: '#TEST-OSRM-1001',
postalCode: 'M5X 1A9',
recipientName: 'TEST Recipient Upcoming 1',
recipientName: 'TEST Recipient Ready 1',
sequence: 1,
}),
validationStop({
Expand All @@ -49,12 +49,12 @@ export const assignedRouteValidationScenarios: AssignedRouteValidationScenario[]
longitude: -79.3909,
orderName: '#TEST-OSRM-1002',
postalCode: 'M5V 1Z2',
recipientName: 'TEST Recipient Upcoming 2',
recipientName: 'TEST Recipient Ready 2',
sequence: 2,
}),
],
}),
tab: 'upcoming',
tab: 'ready',
},
{
expectedEvidence: {
Expand Down
33 changes: 12 additions & 21 deletions src/domain/route/routeSessionClassification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
const now = new Date('2026-06-02T13:00:00.000Z');

describe('assigned route session classification', () => {
it('retains past assigned routes as unfinished instead of dropping them', () => {
it('retains past unstarted assigned routes as ready instead of a fourth unfinished state', () => {
const sessions = [
session('past', '2026-05-30', 'America/Toronto'),
session('today', '2026-06-02', 'America/Toronto'),
Expand All @@ -22,19 +22,10 @@ describe('assigned route session classification', () => {
filterVisibleAssignedRouteSessions(sessions, {
now,
selectedRouteId: null,
selectedRouteStatus: 'upcoming',
selectedTab: 'unfinished',
selectedRouteStatus: 'ready',
selectedTab: 'ready',
}).map((item) => item.route.id),
['past'],
);
assert.deepEqual(
filterVisibleAssignedRouteSessions(sessions, {
now,
selectedRouteId: null,
selectedRouteStatus: 'upcoming',
selectedTab: 'upcoming',
}).map((item) => item.route.id),
['today', 'future'],
['past', 'today', 'future'],
);
});

Expand All @@ -46,18 +37,18 @@ describe('assigned route session classification', () => {
now: boundaryNow,
route: route('toronto-today', '2026-06-01', 'America/Toronto'),
selectedRouteId: null,
selectedRouteStatus: 'upcoming',
selectedRouteStatus: 'ready',
}),
'upcoming',
'ready',
);
assert.equal(
classifyAssignedRouteSession({
now: boundaryNow,
route: route('seoul-past', '2026-06-01', 'Asia/Seoul'),
selectedRouteId: null,
selectedRouteStatus: 'upcoming',
selectedRouteStatus: 'ready',
}),
'unfinished',
'ready',
);
});

Expand All @@ -73,7 +64,7 @@ describe('assigned route session classification', () => {
);
});

it('lets current-session completion override unfinished only for the selected route', () => {
it('lets current-session completion override ready only for the selected route', () => {
assert.equal(
classifyAssignedRouteSession({
now,
Expand All @@ -90,17 +81,17 @@ describe('assigned route session classification', () => {
selectedRouteId: 'selected',
selectedRouteStatus: 'completed',
}),
'unfinished',
'ready',
);
});

it('opens the first loaded past assigned route on the Unfinished tab', () => {
it('opens the first loaded past assigned route on the Ready tab', () => {
assert.equal(
getInitialAssignedRouteTab({
now,
route: route('past-assigned', '2026-05-30', 'America/Toronto'),
}),
'unfinished',
'ready',
);
});
});
Expand Down
8 changes: 4 additions & 4 deletions src/domain/route/routeSessionClassification.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type RouteSessionStatus = 'active' | 'completed' | 'unfinished' | 'upcoming';
export type RouteSessionStatus = 'active' | 'completed' | 'ready';

export type RouteSessionClassificationRoute = {
deliveryDate: string;
Expand Down Expand Up @@ -28,10 +28,10 @@ export function classifyAssignedRouteSession(input: RouteSessionClassificationIn
now: input.now,
timezone: input.route.timezone,
})) {
return 'unfinished';
return 'ready';
}

return 'upcoming';
return 'ready';
}

export function filterVisibleAssignedRouteSessions<T extends { route: RouteSessionClassificationRoute }>(
Expand All @@ -54,7 +54,7 @@ export function getInitialAssignedRouteTab(input: {
now: input.now,
route: input.route,
selectedRouteId: null,
selectedRouteStatus: 'upcoming',
selectedRouteStatus: 'ready',
});
}

Expand Down