Skip to content

Commit b26ed00

Browse files
Banyel3claude
andcommitted
feat(rider): bottom tab nav (Jobs/Cash/Profile) + Profile screen, de-slop jobs
/design-review found the rider app read "empty" next to the customer app. Root causes: no bottom navigation (customer has Home/Orders/Profile tabs; rider was a flat stack), and a duplicated "My jobs" title (native header + an in-content H1) that ate the top third of the screen. - (tabs)/_layout.tsx: bottom tab bar mirroring the customer app — Jobs (briefcase), Cash (wallet), Profile (person). Job detail stays a stacked screen pushed from Jobs, so it's out of the bar. - Moved index.tsx + cash.tsx into (tabs)/. Cash was a tiny top-right text link (sub-44px target, easy to miss) — now a real tab. - (tabs)/profile.tsx (new): the rider had NO sign-out anywhere and no identity screen. Profile shows the signed-in rider, a verification badge (VERIFIED / pending / rejected from /rider/onboarding), and Sign out. - Jobs screen: dropped the redundant in-content "My jobs" title (the tab header already says it); leads with the live count instead. Kills the empty gap. Verified live in the iOS simulator (dev-rider-1): tab bar renders, jobs screen no longer double-titled. Type-clean, 7 rider tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 567a846 commit b26ed00

5 files changed

Lines changed: 148 additions & 31 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Ionicons } from '@expo/vector-icons';
2+
import { Tabs } from 'expo-router';
3+
import { colors, font } from '@wash-and-go/ui';
4+
5+
// Bottom tab bar mirroring the customer app (Home/Orders/Profile). Rider's
6+
// top-level destinations are Jobs (the queue), Cash (earnings), and Profile
7+
// (identity + sign-out). The job detail is a stacked screen pushed from Jobs,
8+
// so it stays out of the tab bar.
9+
export default function RiderTabsLayout() {
10+
return (
11+
<Tabs
12+
screenOptions={{
13+
tabBarActiveTintColor: colors.terra,
14+
tabBarInactiveTintColor: colors.textMuted,
15+
headerStyle: { backgroundColor: colors.bg },
16+
headerTintColor: colors.text,
17+
headerTitleStyle: { fontFamily: font.bold, color: colors.text },
18+
headerShadowVisible: false,
19+
tabBarStyle: {
20+
backgroundColor: colors.surface,
21+
borderTopColor: colors.border,
22+
},
23+
tabBarLabelStyle: { fontSize: 12, fontWeight: '600' },
24+
}}
25+
>
26+
<Tabs.Screen
27+
name="index"
28+
options={{
29+
title: 'My jobs',
30+
tabBarLabel: 'Jobs',
31+
tabBarIcon: ({ color, size, focused }) => (
32+
<Ionicons name={focused ? 'briefcase' : 'briefcase-outline'} size={size} color={color} />
33+
),
34+
}}
35+
/>
36+
<Tabs.Screen
37+
name="cash"
38+
options={{
39+
title: 'My cash',
40+
tabBarLabel: 'Cash',
41+
tabBarIcon: ({ color, size, focused }) => (
42+
<Ionicons name={focused ? 'wallet' : 'wallet-outline'} size={size} color={color} />
43+
),
44+
}}
45+
/>
46+
<Tabs.Screen
47+
name="profile"
48+
options={{
49+
title: 'Profile',
50+
tabBarLabel: 'Profile',
51+
tabBarIcon: ({ color, size, focused }) => (
52+
<Ionicons name={focused ? 'person' : 'person-outline'} size={size} color={color} />
53+
),
54+
}}
55+
/>
56+
</Tabs>
57+
);
58+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
space,
1313
type,
1414
} from '@wash-and-go/ui';
15-
import { api } from '../lib/api';
15+
import { api } from '../../lib/api';
1616

1717
export default function CashScreen() {
1818
const [data, setData] = useState<RiderCashDetail | null>(null);

apps/rider-mobile/src/app/index.tsx renamed to apps/rider-mobile/src/app/(tabs)/index.tsx

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Feather } from '@expo/vector-icons';
22
import { router } from 'expo-router';
33
import React, { useCallback, useEffect, useState } from 'react';
4-
import { Pressable, StyleSheet, Text, View } from 'react-native';
4+
import { StyleSheet, Text, View } from 'react-native';
55
import { STATUS_META, statusLabel, type OrderView } from '@wash-and-go/domain';
66
import {
77
Card,
@@ -18,8 +18,8 @@ import {
1818
toneColor,
1919
type,
2020
} from '@wash-and-go/ui';
21-
import { api } from '../lib/api';
22-
import { actionLabel, GROUP_LABEL, jobGroup, sortJobs } from '../lib/triage';
21+
import { api } from '../../lib/api';
22+
import { actionLabel, GROUP_LABEL, jobGroup, sortJobs } from '../../lib/triage';
2323

2424
type State =
2525
| { kind: 'loading' }
@@ -89,22 +89,12 @@ export default function JobsScreen() {
8989

9090
return (
9191
<Screen>
92-
<View style={styles.header}>
93-
<View style={{ flex: 1 }}>
94-
<Text style={styles.title}>My jobs</Text>
95-
<Text style={styles.count}>
96-
{state.jobs.length} active
97-
{needsAction > 0 ? ` · ${needsAction} need${needsAction > 1 ? '' : 's'} you` : ''}
98-
</Text>
99-
</View>
100-
<Pressable
101-
testID="my-cash"
102-
onPress={() => router.push('/cash' as never)}
103-
accessibilityRole="button"
104-
>
105-
<Text style={styles.cashLink}>My cash →</Text>
106-
</Pressable>
107-
</View>
92+
{/* The tab header already says "My jobs" — this is just the live count,
93+
so the screen leads with content instead of a duplicate title. */}
94+
<Text style={styles.count}>
95+
{state.jobs.length} active
96+
{needsAction > 0 ? ` · ${needsAction} need${needsAction > 1 ? '' : 's'} you` : ''}
97+
</Text>
10898

10999
{state.jobs.map((j) => {
110100
const group = jobGroup(j);
@@ -152,15 +142,7 @@ export default function JobsScreen() {
152142
}
153143

154144
const styles = StyleSheet.create({
155-
header: {
156-
marginBottom: space.xs,
157-
flexDirection: 'row',
158-
alignItems: 'flex-start',
159-
justifyContent: 'space-between',
160-
},
161-
title: { ...type.h1, color: colors.text },
162-
count: { ...type.small, color: colors.textMuted, marginTop: 3 },
163-
cashLink: { ...type.small, color: colors.navy, fontWeight: '700' },
145+
count: { ...type.small, color: colors.textMuted, marginBottom: space.sm },
164146
actionCard: { borderColor: colors.terra, borderWidth: 1.5 },
165147
row: { flexDirection: 'row', alignItems: 'center', gap: space.md },
166148
icon: {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { signOut } from 'firebase/auth';
2+
import React, { useEffect, useState } from 'react';
3+
import { Alert, Platform, Text, View } from 'react-native';
4+
import type { RiderProfileView } from '@wash-and-go/domain';
5+
import {
6+
Card,
7+
Muted,
8+
Pill,
9+
PrimaryButton,
10+
Screen,
11+
colors,
12+
space,
13+
type,
14+
useToast,
15+
} from '@wash-and-go/ui';
16+
import { api } from '../../lib/api';
17+
import { auth, DEV_UID } from '../../lib/firebase';
18+
19+
// Verification status → label + colour for the badge.
20+
const STATUS: Record<string, { label: string; color: string }> = {
21+
VERIFIED: { label: 'Verified rider', color: colors.success },
22+
SUBMITTED: { label: 'Verification pending', color: colors.warning },
23+
DRAFT: { label: 'Not submitted', color: colors.textMuted },
24+
REJECTED: { label: 'Rejected', color: colors.danger },
25+
};
26+
27+
export default function ProfileScreen() {
28+
const toast = useToast();
29+
const name = auth.currentUser?.displayName ?? auth.currentUser?.email ?? DEV_UID ?? 'Rider';
30+
const [profile, setProfile] = useState<RiderProfileView | null>(null);
31+
32+
useEffect(() => {
33+
// Best-effort — the screen is useful even if this fails.
34+
api.getMyRiderOnboarding().then(setProfile).catch(() => setProfile(null));
35+
}, []);
36+
37+
function doSignOut() {
38+
// The root auth gate redirects to /login once the user becomes null.
39+
signOut(auth).catch((e) =>
40+
toast.error(e instanceof Error ? e.message : 'Could not sign out.'),
41+
);
42+
}
43+
44+
function confirmSignOut() {
45+
if (Platform.OS === 'web') {
46+
if (typeof window !== 'undefined' && window.confirm('Sign out of Wash & Go?')) {
47+
doSignOut();
48+
}
49+
return;
50+
}
51+
Alert.alert('Sign out', 'Sign out of Wash & Go?', [
52+
{ text: 'Cancel', style: 'cancel' },
53+
{ text: 'Sign out', style: 'destructive', onPress: doSignOut },
54+
]);
55+
}
56+
57+
const status = profile ? STATUS[profile.status] : null;
58+
59+
return (
60+
<Screen>
61+
<Card>
62+
<Muted>Signed in as</Muted>
63+
<Text style={[type.title, { color: colors.text, marginTop: 2 }]}>{name}</Text>
64+
{status ? (
65+
<View style={{ flexDirection: 'row', marginTop: space.sm }}>
66+
<Pill text={status.label} color={status.color} />
67+
</View>
68+
) : null}
69+
</Card>
70+
71+
<Card>
72+
<Muted>Payouts and support are coming soon.</Muted>
73+
</Card>
74+
75+
<PrimaryButton label="Sign out" onPress={confirmSignOut} testID="sign-out" />
76+
</Screen>
77+
);
78+
}

apps/rider-mobile/src/app/_layout.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,8 @@ export default function RootLayout() {
6666
contentStyle: { backgroundColor: colors.bg },
6767
}}
6868
>
69-
<Stack.Screen name="index" options={{ title: 'My jobs' }} />
69+
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
7070
<Stack.Screen name="login" options={{ headerShown: false }} />
71-
<Stack.Screen name="cash" options={{ title: 'My cash' }} />
7271
<Stack.Screen name="orders/[id]" options={{ title: 'Job' }} />
7372
</Stack>
7473
</ToastProvider>

0 commit comments

Comments
 (0)