The home screen passed its FlatList header as `ListHeaderComponent={() =>
(...)}`. VirtualizedList renders a function-valued header prop as
`<HeaderComponent />`, so a fresh arrow function on each render is a fresh
element type: React unmounted the entire header subtree — MapView included —
and mounted a new one. Home re-renders several times on mount (useFetch
loading->data, useUserLocation pending->granted, session resolve), and
recreating the Android GoogleMap surface each time left it grey with the
Google logo and tiles that never finished loading.
Pass the header as an element instead so the type stays stable and MapView
mounts once. The same `() => (...)` pattern in ListEmptyComponent here, and in
rides.tsx and confirm-ride.tsx, cost needless remounts of list chrome and
DriverCards; fixed alongside.
Also pin expo-crypto to ~13.0.2. It was ^57.0.1 — a wrong-major native module
for SDK 51 that nothing in the app imports. npm hoisted it to the top level
and gave expo-auth-session a nested 13.0.2 to satisfy its ~13.0.0 constraint,
leaving two copies of one native module in the tree for autolinking to choose
between.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
154 lines
5.1 KiB
TypeScript
154 lines
5.1 KiB
TypeScript
import { router } from "expo-router";
|
|
import {
|
|
ActivityIndicator,
|
|
FlatList,
|
|
Image,
|
|
Text,
|
|
TouchableOpacity,
|
|
View,
|
|
} from "react-native";
|
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
|
|
import { GoogleTextInput } from "@/components/google-text-input";
|
|
import { LocationNotice } from "@/components/location-notice";
|
|
import { Map } from "@/components/map";
|
|
import { RideCard } from "@/components/ride-card";
|
|
import { ServiceSelector } from "@/components/service-selector";
|
|
import { icons, images } from "@/constants";
|
|
import { useSession } from "@/lib/session";
|
|
import { useUserLocation } from "@/lib/use-user-location";
|
|
import { useLocationStore } from "@/store";
|
|
import { useFetch } from "@/lib/fetch";
|
|
import type { Ride } from "@/types/type";
|
|
|
|
const Home = () => {
|
|
const setDestinationLocation = useLocationStore(
|
|
(state) => state.setDestinationLocation,
|
|
);
|
|
const { signOut, user } = useSession();
|
|
const { data: recentRides, loading } = useFetch<Ride[]>(
|
|
`/(api)/ride/${user?.id}`,
|
|
);
|
|
|
|
const { status: locationStatus, retry: retryLocation } = useUserLocation();
|
|
|
|
const handleSignOut = () => {
|
|
signOut();
|
|
|
|
router.replace("/(auth)/sign-in");
|
|
};
|
|
const handleDestinationPress = (location: {
|
|
latitude: number;
|
|
longitude: number;
|
|
address: string;
|
|
}) => {
|
|
setDestinationLocation(location);
|
|
|
|
router.push("/(root)/find-ride");
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView className="bg-general-500">
|
|
<FlatList
|
|
data={recentRides?.slice(0, 5)}
|
|
renderItem={({ item }) => <RideCard ride={item} />}
|
|
className="px-5"
|
|
keyboardShouldPersistTaps="handled"
|
|
contentContainerStyle={{
|
|
paddingBottom: 100,
|
|
}}
|
|
ListEmptyComponent={
|
|
<View className="flex flex-col items-center justify-center">
|
|
{!loading ? (
|
|
<>
|
|
<Image
|
|
source={images.noResult}
|
|
alt="No recent rides found"
|
|
className="w-40 h-40"
|
|
resizeMode="contain"
|
|
/>
|
|
<Text className="text-sm">No recent rides found.</Text>
|
|
</>
|
|
) : (
|
|
<ActivityIndicator size="small" color="#000" />
|
|
)}
|
|
</View>
|
|
}
|
|
// Passed as an *element*, not as `() => (...)`. VirtualizedList
|
|
// renders a function prop as `<HeaderComponent />`, so a fresh arrow
|
|
// function each render is a fresh element type: React unmounts the
|
|
// whole header — MapView included — and mounts a new one. Recreating
|
|
// the Android map surface on every re-render leaves it grey with the
|
|
// Google logo and tiles that never finish loading.
|
|
ListHeaderComponent={
|
|
<>
|
|
<View className="flex flex-row items-center justify-between my-5">
|
|
<Text
|
|
className="text-base font-JakartaExtraBold"
|
|
numberOfLines={1}
|
|
>
|
|
Welcome {user?.name || user?.email} 👋
|
|
</Text>
|
|
|
|
<View className="flex flex-row items-center gap-x-1">
|
|
<TouchableOpacity
|
|
onPress={handleSignOut}
|
|
className="justify-center items-center w-10 h-10 rounded-full bg-white"
|
|
>
|
|
<Image source={icons.out} className="w-4 h-4" alt="Logout" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
<GoogleTextInput
|
|
icon={icons.search}
|
|
containerStyles="bg-white shadow-md shadow-neutral-300"
|
|
handlePress={handleDestinationPress}
|
|
/>
|
|
|
|
<Text className="text-xl font-JakartaBold mt-5 mb-3">
|
|
Your Current Location
|
|
</Text>
|
|
|
|
<View className="w-full h-[300px] rounded-2xl overflow-hidden bg-white">
|
|
{locationStatus === "pending" || locationStatus === "granted" ? (
|
|
<>
|
|
{/* The map draws straight away on the Beirut fallback so the
|
|
slot never sits empty while the fix is still coming. */}
|
|
<Map />
|
|
|
|
{locationStatus === "pending" ? (
|
|
<View className="absolute bottom-3 self-center flex-row items-center rounded-full bg-white/95 px-4 py-2 shadow-md shadow-neutral-400/40">
|
|
<ActivityIndicator size="small" color="#0286ff" />
|
|
<Text className="ml-2 text-xs font-JakartaMedium text-general-200">
|
|
Finding your location…
|
|
</Text>
|
|
</View>
|
|
) : null}
|
|
</>
|
|
) : (
|
|
<LocationNotice
|
|
status={locationStatus}
|
|
onRetry={retryLocation}
|
|
/>
|
|
)}
|
|
</View>
|
|
|
|
<Text className="text-xl font-JakartaBold mt-5 mb-3">
|
|
What do you need?
|
|
</Text>
|
|
|
|
<ServiceSelector />
|
|
|
|
<Text className="text-xl font-JakartaBold mt-5 mb-3">
|
|
Recent Rides
|
|
</Text>
|
|
</>
|
|
}
|
|
/>
|
|
</SafeAreaView>
|
|
);
|
|
};
|
|
|
|
export default Home;
|