The Android map never drew because mapType was "mutedStandard", an Apple Maps value. Android's MapManager looks the name up in a fixed table and unboxes the result into an int, so an unrecognised value threw a NullPointerException in the native view manager before any tile rendered. Android now gets "standard" plus a customMapStyle that mutes POI and transit labels, since showsPointsOfInterest is iOS-only too. Location hung indefinitely: getCurrentPositionAsync was called with no accuracy and no timeout, so it waited for a GPS fix that never arrives indoors or on an emulator with no mock location. useUserLocation now takes a cached fix first for an immediate render, caps the precise reading at 15 seconds, and checks device location services separately from app permission. Reverse geocoding moved off the critical path so a failed lookup costs the address label rather than the coordinates. The single boolean became five states, each with its own notice and either a retry or a settings shortcut, since retrying a hard denial does nothing. Map also no longer deletes itself when the driver fetch fails or the location is still pending: drivers are an overlay, and calculateRegion already falls back to Beirut. Adds a four-tile service selector above Recent Rides - Car, Moto, Courier, My Car - with the choice held in useServiceStore for the booking flow to read. English only for now; the intended Arabic names are recorded in constants/services.ts for the language pass. Selection styling matches the role picker on sign-up so the two read as one control. app.config.js layers the Google Maps key and expo-location permission strings onto app.json. No effect under Expo Go, which ignores native config, but required for the first EAS build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
150 lines
4.7 KiB
TypeScript
150 lines
4.7 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>
|
|
)}
|
|
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;
|