Skip to content

Commit a70a9bb

Browse files
Banyel3claude
andcommitted
feat(mobile): unified toast system — no more inline red error text
Replaces the ad-hoc inline red error `<Text>` scattered across the apps with one shared toast: green = success, red = error, slide-in from the top, tap or auto-dismiss. - packages/ui: ToastProvider + useToast() (toast.success/error/info), animated, safe-area aware. Wired into both apps' root _layout. - customer app: login, checkout, book (+ the geocode "could not search" error), change-laundry, addresses, profile, notifications, order detail (cancel/rate) — every transient error → toast.error; success paths (address saved/removed, booking cancelled, rating thanks, address pinned, all-caught-up) → toast.success. - rider app: login, job detail (status transition + cash) → toasts. - Full-screen load-failure ErrorState (with retry) intentionally kept — those aren't transient alerts. Sign-out confirm Alert kept (it's a confirmation). - type-clean: ui + both apps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c088bc2 commit a70a9bb

13 files changed

Lines changed: 225 additions & 99 deletions

File tree

apps/customer-mobile/src/app/(tabs)/profile.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ import {
99
Screen,
1010
colors,
1111
type,
12+
useToast,
1213
} from '@wash-and-go/ui';
1314
import { auth } from '../../lib/firebase';
1415

1516
export default function ProfileScreen() {
17+
const toast = useToast();
1618
const email = auth.currentUser?.email ?? null;
1719

1820
function doSignOut() {
1921
// The root auth gate redirects to /login once the user becomes null.
20-
void signOut(auth);
22+
signOut(auth).catch((e) =>
23+
toast.error(e instanceof Error ? e.message : 'Could not sign out.'),
24+
);
2125
}
2226

2327
function confirmSignOut() {

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

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { onAuthStateChanged, type User } from 'firebase/auth';
1212
import { useEffect, useState } from 'react';
1313
import { Text as RNText, TextInput as RNTextInput } from 'react-native';
1414
import { SafeAreaProvider } from 'react-native-safe-area-context';
15-
import { colors, font } from '@wash-and-go/ui';
15+
import { colors, font, ToastProvider } from '@wash-and-go/ui';
1616
import { auth } from '../lib/firebase';
1717

1818
// Base default font so every Text/TextInput inherits Plus Jakarta, not the
@@ -50,25 +50,27 @@ export default function RootLayout() {
5050

5151
return (
5252
<SafeAreaProvider>
53-
<StatusBar style="dark" />
54-
<Stack
55-
screenOptions={{
56-
headerStyle: { backgroundColor: colors.bg },
57-
headerTintColor: colors.text,
58-
headerTitleStyle: { fontFamily: font.bold, color: colors.text },
59-
headerShadowVisible: false,
60-
contentStyle: { backgroundColor: colors.bg },
61-
}}
62-
>
63-
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
64-
<Stack.Screen name="login" options={{ headerShown: false }} />
65-
<Stack.Screen name="addresses" options={{ title: 'Saved addresses' }} />
66-
<Stack.Screen name="notifications" options={{ title: 'Notifications' }} />
67-
<Stack.Screen name="book" options={{ title: 'Book pickup' }} />
68-
<Stack.Screen name="checkout" options={{ title: 'Review & confirm' }} />
69-
<Stack.Screen name="change-laundry" options={{ title: 'Choose a laundry' }} />
70-
<Stack.Screen name="orders/[id]" options={{ title: 'Order' }} />
71-
</Stack>
53+
<ToastProvider>
54+
<StatusBar style="dark" />
55+
<Stack
56+
screenOptions={{
57+
headerStyle: { backgroundColor: colors.bg },
58+
headerTintColor: colors.text,
59+
headerTitleStyle: { fontFamily: font.bold, color: colors.text },
60+
headerShadowVisible: false,
61+
contentStyle: { backgroundColor: colors.bg },
62+
}}
63+
>
64+
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
65+
<Stack.Screen name="login" options={{ headerShown: false }} />
66+
<Stack.Screen name="addresses" options={{ title: 'Saved addresses' }} />
67+
<Stack.Screen name="notifications" options={{ title: 'Notifications' }} />
68+
<Stack.Screen name="book" options={{ title: 'Book pickup' }} />
69+
<Stack.Screen name="checkout" options={{ title: 'Review & confirm' }} />
70+
<Stack.Screen name="change-laundry" options={{ title: 'Choose a laundry' }} />
71+
<Stack.Screen name="orders/[id]" options={{ title: 'Order' }} />
72+
</Stack>
73+
</ToastProvider>
7274
</SafeAreaProvider>
7375
);
7476
}

apps/customer-mobile/src/app/addresses.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import {
1212
radius,
1313
space,
1414
type,
15+
useToast,
1516
} from '@wash-and-go/ui';
1617
import { api } from '../lib/api';
1718

1819
export default function AddressesScreen() {
20+
const toast = useToast();
1921
const [list, setList] = useState<AddressView[] | null>(null);
2022
const [error, setError] = useState<string | null>(null);
2123
const [label, setLabel] = useState('');
@@ -39,7 +41,6 @@ export default function AddressesScreen() {
3941
const l = line.trim();
4042
if (l.length < 3 || busy) return;
4143
setBusy(true);
42-
setError(null);
4344
try {
4445
await api.createAddress({
4546
line: l,
@@ -49,35 +50,38 @@ export default function AddressesScreen() {
4950
setLabel('');
5051
setLine('');
5152
await load();
53+
toast.success('Address saved');
5254
} catch (e) {
53-
setError(e instanceof Error ? e.message : 'Could not add that address.');
55+
toast.error(e instanceof Error ? e.message : 'Could not add that address.');
5456
} finally {
5557
setBusy(false);
5658
}
57-
}, [line, label, busy, list, load]);
59+
}, [line, label, busy, list, load, toast]);
5860

5961
const setDefault = useCallback(
6062
async (id: string) => {
6163
try {
6264
await api.updateAddress(id, { isDefault: true });
6365
await load();
66+
toast.success('Default address updated');
6467
} catch (e) {
65-
setError(e instanceof Error ? e.message : 'Could not update that address.');
68+
toast.error(e instanceof Error ? e.message : 'Could not update that address.');
6669
}
6770
},
68-
[load],
71+
[load, toast],
6972
);
7073

7174
const remove = useCallback(
7275
async (id: string) => {
7376
try {
7477
await api.deleteAddress(id);
7578
await load();
79+
toast.success('Address removed');
7680
} catch (e) {
77-
setError(e instanceof Error ? e.message : 'Could not remove that address.');
81+
toast.error(e instanceof Error ? e.message : 'Could not remove that address.');
7882
}
7983
},
80-
[load],
84+
[load, toast],
8185
);
8286

8387
if (!list && !error) {
@@ -164,7 +168,6 @@ export default function AddressesScreen() {
164168
multiline
165169
testID="addr-line-input"
166170
/>
167-
{error ? <Text style={styles.error}>{error}</Text> : null}
168171
<PrimaryButton
169172
label="Add address"
170173
onPress={add}
@@ -197,7 +200,6 @@ const styles = StyleSheet.create({
197200
color: colors.text,
198201
fontSize: 15,
199202
},
200-
error: { color: colors.danger, ...type.body },
201203
defaultBadge: {
202204
backgroundColor: colors.terra,
203205
paddingHorizontal: 8,

apps/customer-mobile/src/app/book.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
radius,
1313
space,
1414
type,
15+
useToast,
1516
} from '@wash-and-go/ui';
1617
import { api } from '../lib/api';
1718
import { LOAD_BUCKETS, LoadBucket } from '../lib/format';
@@ -48,9 +49,9 @@ export default function BookScreen() {
4849
const [address, setAddress] = useState('');
4950
const [gpsLoading, setGpsLoading] = useState(false);
5051
const [geoLoading, setGeoLoading] = useState(false);
51-
const [error, setError] = useState<string | null>(null);
5252
const [saved, setSaved] = useState<AddressView[]>([]);
5353
const [saveNew, setSaveNew] = useState(false);
54+
const toast = useToast();
5455

5556
// Load the address book to prefill pickup (best-effort — booking works without).
5657
useEffect(() => {
@@ -73,29 +74,28 @@ export default function BookScreen() {
7374
const q = address.trim();
7475
if (q.length < 2) return;
7576
setGeoLoading(true);
76-
setError(null);
7777
try {
7878
const hit = await api.geocode(q);
7979
if (hit) {
8080
setCoords(hit.point);
8181
setAddress(hit.label);
82+
toast.success('Address pinned.');
8283
} else {
83-
setError('No match for that address. Try GPS or add more detail.');
84+
toast.error('No match for that address. Try GPS or add more detail.');
8485
}
8586
} catch {
86-
setError('Could not search that address. Try again or use GPS.');
87+
toast.error('Could not search that address. Try again or use GPS.');
8788
} finally {
8889
setGeoLoading(false);
8990
}
9091
}, [address]);
9192

9293
const useMyLocation = useCallback(async () => {
9394
setGpsLoading(true);
94-
setError(null);
9595
try {
9696
const { status } = await Location.requestForegroundPermissionsAsync();
9797
if (status !== 'granted') {
98-
setError('Location permission is needed to set your pickup point.');
98+
toast.error('Location permission is needed to set your pickup point.');
9999
return;
100100
}
101101
const pos = await Location.getCurrentPositionAsync({
@@ -116,7 +116,7 @@ export default function BookScreen() {
116116
// best-effort
117117
}
118118
} catch {
119-
setError('Could not read your location. Try again or type your address.');
119+
toast.error('Could not read your location. Try again or type your address.');
120120
} finally {
121121
setGpsLoading(false);
122122
}
@@ -321,8 +321,6 @@ export default function BookScreen() {
321321
<Text style={[type.body, { color: colors.text }]}>Save this pickup for next time</Text>
322322
</Pressable>
323323

324-
{error ? <Text style={styles.error}>{error}</Text> : null}
325-
326324
<PrimaryButton label="Continue" onPress={cont} disabled={!canContinue} />
327325
</Screen>
328326
);
@@ -358,7 +356,6 @@ const styles = StyleSheet.create({
358356
color: colors.text,
359357
fontSize: 15,
360358
},
361-
error: { color: colors.danger, ...type.body },
362359
defaultBadge: {
363360
backgroundColor: colors.terra,
364361
paddingHorizontal: 8,

apps/customer-mobile/src/app/checkout.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
peso,
1616
space,
1717
type,
18+
useToast,
1819
} from '@wash-and-go/ui';
1920
import { api } from '../lib/api';
2021

@@ -52,6 +53,7 @@ export default function CheckoutScreen() {
5253
const [loading, setLoading] = useState(true);
5354
const [error, setError] = useState<string | null>(null);
5455
const [submitting, setSubmitting] = useState(false);
56+
const toast = useToast();
5557
// One key per checkout mount — a retried confirm dedupes to a single order.
5658
const idempotencyKey = useMemo(() => newIdempotencyKey(), []);
5759

@@ -81,7 +83,6 @@ export default function CheckoutScreen() {
8183
const confirm = useCallback(async () => {
8284
if (!quote) return;
8385
setSubmitting(true);
84-
setError(null);
8586
try {
8687
const order = await api.createOrder(
8788
{
@@ -97,7 +98,7 @@ export default function CheckoutScreen() {
9798
);
9899
router.replace(`/orders/${order.id}`);
99100
} catch (e) {
100-
setError(e instanceof Error ? e.message : 'Could not place your order.');
101+
toast.error(e instanceof Error ? e.message : 'Could not place your order.');
101102
} finally {
102103
setSubmitting(false);
103104
}
@@ -186,7 +187,6 @@ export default function CheckoutScreen() {
186187
<Text style={styles.est}>Delivery adjusts by distance · final weight at the shop.</Text>
187188
</Card>
188189

189-
{error ? <Text style={styles.errText}>{error}</Text> : null}
190190
<PrimaryButton
191191
label={submitting ? 'Booking…' : 'Confirm booking'}
192192
onPress={confirm}
@@ -243,5 +243,4 @@ const styles = StyleSheet.create({
243243
change: { color: colors.terraDark, fontWeight: '700', fontSize: 13 },
244244
divider: { height: StyleSheet.hairlineWidth, backgroundColor: colors.border, marginVertical: space.xs },
245245
est: { color: colors.terraDark, fontSize: 12, fontWeight: '600' },
246-
errText: { color: colors.danger, ...type.body },
247246
});

apps/customer-mobile/src/app/login.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
colors,
1414
radius,
1515
space,
16-
type,
16+
useToast,
1717
} from '@wash-and-go/ui';
1818
import { auth } from '../lib/firebase';
1919
import { api } from '../lib/api';
@@ -42,14 +42,13 @@ export default function LoginScreen() {
4242
const [email, setEmail] = useState('');
4343
const [password, setPassword] = useState('');
4444
const [busy, setBusy] = useState(false);
45-
const [error, setError] = useState<string | null>(null);
45+
const toast = useToast();
4646

4747
const valid = email.includes('@') && password.length >= 6;
4848

4949
async function submit() {
5050
if (!valid) return;
5151
setBusy(true);
52-
setError(null);
5352
try {
5453
if (mode === 'signup') {
5554
await createUserWithEmailAndPassword(auth, email.trim(), password);
@@ -60,7 +59,7 @@ export default function LoginScreen() {
6059
await api.postSession();
6160
router.replace('/');
6261
} catch (e) {
63-
setError(friendly((e as { code?: string })?.code));
62+
toast.error(friendly((e as { code?: string })?.code));
6463
} finally {
6564
setBusy(false);
6665
}
@@ -94,8 +93,6 @@ export default function LoginScreen() {
9493
style={styles.input}
9594
/>
9695

97-
{error ? <Text style={styles.error}>{error}</Text> : null}
98-
9996
<PrimaryButton
10097
label={mode === 'signin' ? 'Sign in' : 'Create account'}
10198
onPress={submit}
@@ -106,7 +103,6 @@ export default function LoginScreen() {
106103
<Pressable
107104
onPress={() => {
108105
setMode(mode === 'signin' ? 'signup' : 'signin');
109-
setError(null);
110106
}}
111107
style={{ padding: space.md }}
112108
>
@@ -133,5 +129,4 @@ const styles = StyleSheet.create({
133129
color: colors.text,
134130
fontSize: 15,
135131
},
136-
error: { color: colors.danger, ...type.body },
137132
});

apps/customer-mobile/src/app/notifications.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import {
1111
colors,
1212
space,
1313
type,
14+
useToast,
1415
} from '@wash-and-go/ui';
1516
import { api } from '../lib/api';
1617

1718
export default function NotificationsScreen() {
19+
const toast = useToast();
1820
const [items, setItems] = useState<NotificationView[] | null>(null);
1921
const [error, setError] = useState<string | null>(null);
2022

@@ -51,10 +53,11 @@ export default function NotificationsScreen() {
5153
try {
5254
await api.markAllNotificationsRead();
5355
await load();
54-
} catch {
55-
// best-effort
56+
toast.success('All caught up');
57+
} catch (e) {
58+
toast.error(e instanceof Error ? e.message : 'Could not mark all as read.');
5659
}
57-
}, [load]);
60+
}, [load, toast]);
5861

5962
if (!items && !error) {
6063
return (

0 commit comments

Comments
 (0)