import LottieView from 'lottie-react-native';
import { useEffect, useState } from 'react';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { DriverNavigation } from '@/components/DriverNavigation';
import { RideMap } from '@/components/RideMap';
import { useDispatch } from '@/lib/dispatch/DispatchContext';
import { device, startsDriving } from '@/lib/dispatch/device';
import type { DriverJobView, DriverOfferView, DriverResult } from '@/lib/dispatch/protocol';
import { useSecondsLeft } from '@/lib/dispatch/useSecondsLeft';
import { pickupRoute } from '@/lib/navigation';
import { useRide } from '@/lib/RideContext';
import { formatDistance, formatDuration, formatPrice, getRideType } from '@/lib/rideTypes';
export default function DriverScreen() {
const insets = useSafeAreaInsets();
const { connected, snapshot, error, send } = useDispatch();
const { pickup: position } = useRide();
const [driving, setDriving] = useState(startsDriving);
useEffect(() => {
if (connected) send({ type: 'setDriving', driving });
}, [connected, driving, send]);
useEffect(() => {
if (connected) send({ type: 'setPosition', position });
}, [connected, position, send]);
const driver = snapshot?.role === 'driver' ? snapshot : null;
const job = driver?.job ?? null;
const nextJob = driver?.nextJob ?? null;
const offers = driver?.offers ?? [];
const openOffers = offers.filter((offer) => !offer.declined);
const passedOffers = offers.filter((offer) => offer.declined);
return (
<View style={styles.screen}>
{}
{job ? (
<DriverNavigation key={job.rideId} route={pickupRoute(job.riderName)} />
) : (
<RideMap pickup={position} userLocation={null} showsUserLocation={false} destination={null} route={null} topInset={insets.top} />
)}
{!job && (driving ? <Waiting /> : <Offline onStart={() => setDriving(true)} />)}
<View style={[styles.sheet, { paddingBottom: insets.bottom + 16 }]}>
{job && <Job job={job} onComplete={() => send({ type: 'completeTrip', rideId: job.rideId })} />}
{nextJob && (
<Text style={styles.next} testID="next-job">
Next: {nextJob.riderName} β {nextJob.destination.title} Β· you earn {formatPrice(nextJob.earnings)}
</Text>
)}
{!job &&
passedOffers.map((offer) => (
<Text key={offer.rideId} style={styles.passed} testID="offer-passed">
You passed at {formatPrice(offer.price)} Β· {offer.riderName} β {offer.destination.title}
</Text>
))}
{!job && driver?.lastResult && <ResultBanner result={driver.lastResult} />}
{error && (
<Text style={styles.error} testID="driver-error">
{error}
</Text>
)}
<View style={styles.statusRow}>
<View style={[styles.dot, { backgroundColor: connected && driving ? '#1a8f3c' : '#bbb' }]} />
<Text style={styles.name}>{device.name}</Text>
<Text style={styles.muted} testID="driver-status">
{!connected ? 'Connectingβ¦' : driving ? 'Driving' : 'Offline'}
</Text>
{}
{driving ? (
openOffers.length === 0 && <GoOfflineButton onPress={() => setDriving(false)} />
) : (
job && (
<Pressable style={styles.smallButton} onPress={() => setDriving(true)} accessibilityRole="button" testID="start-driving">
<Text style={styles.smallButtonText}>Start driving</Text>
</Pressable>
)
)}
</View>
</View>
{openOffers.length > 0 && (
<View style={styles.overlay} testID="offer-sheet">
<ScrollView
style={styles.offerSheet}
contentContainerStyle={[styles.offerSheetContent, { paddingBottom: insets.bottom + 16 }]}
>
<View style={styles.sheetHeader}>
<Text style={styles.sheetTitle}>{openOffers.length === 1 ? 'New ride request' : `${openOffers.length} ride requests`}</Text>
<GoOfflineButton onPress={() => setDriving(false)} />
</View>
{job && <Text style={styles.muted}>Accepting makes this your next ride, after {job.riderName}.</Text>}
{openOffers.map((offer) => (
<Offer
key={`${offer.rideId}:${offer.round}`}
offer={offer}
onAccept={() => send({ type: 'accept', rideId: offer.rideId, round: offer.round })}
onPass={() => send({ type: 'decline', rideId: offer.rideId, round: offer.round })}
/>
))}
</ScrollView>
</View>
)}
</View>
);
}
function GoOfflineButton({ onPress }: { onPress: () => void }) {
return (
<Pressable style={styles.smallButton} onPress={onPress} accessibilityRole="button" testID="stop-driving">
<Text style={styles.smallButtonText}>Go offline</Text>
</Pressable>
);
}
function Waiting() {
return (
<View style={styles.center} testID="driver-waiting" pointerEvents="none">
<LottieView source={require('../../assets/lottie/finding-driver.json')} autoPlay loop style={styles.animation} />
<Text style={styles.centerTitle}>Looking for ride requests</Text>
<Text style={styles.muted}>Requests pop up here while youβre driving.</Text>
</View>
);
}
function Offline({ onStart }: { onStart: () => void }) {
return (
<View style={styles.center} testID="driver-offline">
<View style={styles.offlineCard}>
<Text style={styles.centerTitle}>Youβre offline</Text>
<Text style={styles.muted}>Start driving to get ride requests.</Text>
<Pressable style={[styles.button, styles.primary, styles.startButton]} onPress={onStart} accessibilityRole="button" testID="start-driving">
<Text style={styles.primaryText}>Start driving</Text>
</Pressable>
</View>
</View>
);
}
function Offer({ offer, onAccept, onPass }: { offer: DriverOfferView; onAccept: () => void; onPass: () => void }) {
const secondsLeft = useSecondsLeft(offer.closesAt);
return (
<View style={styles.card} testID="offer">
<View style={styles.cardHeader}>
<Text style={styles.cardTitle} testID="offer-round">
Ride request Β· round {offer.round}
</Text>
<Text style={styles.muted}>{secondsLeft}s left</Text>
</View>
<Text style={styles.destination} testID="offer-destination">
{offer.riderName} β {offer.destination.title}
</Text>
<Text style={styles.muted}>
{getRideType(offer.rideTypeId).name} Β· {formatDistance(offer.distanceMeters)} Β· {formatDuration(offer.durationSeconds)}
</Text>
<View>
<Text style={styles.price} testID="offer-earnings">
{formatPrice(offer.earnings)}
</Text>
<Text style={styles.muted}>
You earn Β· rider pays <Text testID="offer-price">{formatPrice(offer.price)}</Text>
</Text>
</View>
<View style={styles.buttons}>
<Pressable style={[styles.button, styles.secondary]} onPress={onPass} accessibilityRole="button" testID="decline-offer">
<Text style={styles.secondaryText}>Pass</Text>
</Pressable>
<Pressable style={[styles.button, styles.primary]} onPress={onAccept} accessibilityRole="button" testID="accept-offer">
<Text style={styles.primaryText}>Accept</Text>
</Pressable>
</View>
</View>
);
}
function Job({ job, onComplete }: { job: DriverJobView; onComplete: () => void }) {
return (
<View style={styles.card} testID="driver-job">
<Text style={styles.won}>You got the ride</Text>
<Text style={styles.destination}>
{job.riderName} β {job.destination.title}
</Text>
<Text style={styles.fare} testID="job-earnings">
You earn {formatPrice(job.earnings)}
</Text>
<Text style={styles.muted} testID="job-fare">
Fare {formatPrice(job.fare)}
</Text>
<Pressable style={[styles.button, styles.primary]} onPress={onComplete} accessibilityRole="button" testID="complete-trip">
<Text style={styles.primaryText}>Complete trip</Text>
</Pressable>
</View>
);
}
const RESULT_TEXT: Record<DriverResult['outcome'], string> = {
lost: 'Another driver took that ride',
expired: 'That request closed before you answered',
cancelled: 'The rider cancelled their request',
completed: 'Trip complete',
};
function ResultBanner({ result }: { result: DriverResult }) {
return (
<Text style={styles.banner} testID="driver-result">
{RESULT_TEXT[result.outcome]}
{result.outcome === 'completed' ? ` Β· you earned ${formatPrice(result.earnings)}` : ''}
</Text>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: '#e8ecef' },
sheet: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
backgroundColor: '#fff',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingHorizontal: 16,
paddingTop: 16,
gap: 10,
shadowColor: '#000',
shadowOpacity: 0.15,
shadowRadius: 12,
elevation: 12,
},
statusRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
dot: { width: 10, height: 10, borderRadius: 5 },
name: { fontSize: 17, fontWeight: '700', color: '#111', flex: 1 },
muted: { color: '#666', fontSize: 14 },
error: { backgroundColor: '#fdecec', color: '#9b1c1c', padding: 10, borderRadius: 8, overflow: 'hidden' },
banner: { backgroundColor: '#f1f1f1', color: '#333', padding: 12, borderRadius: 8, overflow: 'hidden' },
next: { backgroundColor: '#eef7f1', color: '#0b6e4f', padding: 10, borderRadius: 8, overflow: 'hidden', fontSize: 14, fontWeight: '600' },
center: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 160, alignItems: 'center', justifyContent: 'center', gap: 6, paddingHorizontal: 24 },
animation: { width: 220, height: 220 },
centerTitle: { fontSize: 20, fontWeight: '700', color: '#111' },
offlineCard: { backgroundColor: '#fff', borderRadius: 16, padding: 20, gap: 6, alignItems: 'center', alignSelf: 'stretch' },
startButton: { flex: 0, alignSelf: 'stretch', marginTop: 10 },
smallButton: { backgroundColor: '#eee', borderRadius: 8, paddingHorizontal: 12, paddingVertical: 6 },
smallButtonText: { fontSize: 14, fontWeight: '600', color: '#111' },
passed: { backgroundColor: '#eef4fa', color: '#1a5f8f', padding: 10, borderRadius: 8, overflow: 'hidden', fontSize: 14 },
overlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.35)', justifyContent: 'flex-end' },
offerSheet: { flexGrow: 0, maxHeight: '75%', backgroundColor: '#fff', borderTopLeftRadius: 20, borderTopRightRadius: 20 },
offerSheetContent: { padding: 16, gap: 12 },
sheetHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
sheetTitle: { fontSize: 20, fontWeight: '700', color: '#111' },
card: { backgroundColor: '#f6f6f6', borderRadius: 14, padding: 16, gap: 8 },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
cardTitle: { fontSize: 13, fontWeight: '600', color: '#666', textTransform: 'uppercase' },
destination: { fontSize: 18, fontWeight: '600', color: '#111' },
price: { fontSize: 32, fontWeight: '700', color: '#111' },
fare: { fontSize: 16, color: '#111' },
won: { fontSize: 13, fontWeight: '700', color: '#1a8f3c', textTransform: 'uppercase' },
buttons: { flexDirection: 'row', gap: 8 },
button: { flex: 1, borderRadius: 10, paddingVertical: 14, alignItems: 'center' },
primary: { backgroundColor: '#111' },
primaryText: { color: '#fff', fontSize: 16, fontWeight: '600' },
secondary: { backgroundColor: '#e4e4e4' },
secondaryText: { color: '#111', fontSize: 16, fontWeight: '600' },
});