diff --git a/mobile/.env.example b/mobile/.env.example new file mode 100644 index 0000000..e1f393b --- /dev/null +++ b/mobile/.env.example @@ -0,0 +1,3 @@ +# prod only +EXPO_PUBLIC_GOOGLE_MAPS_IOS_API_KEY= +EXPO_PUBLIC_GOOGLE_MAPS_ANDROID_API_KEY= \ No newline at end of file diff --git a/mobile/app.json b/mobile/app.json index 0208b51..0ba528b 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -9,7 +9,10 @@ "userInterfaceStyle": "automatic", "newArchEnabled": true, "ios": { - "supportsTablet": true + "supportsTablet": true, + "config": { + "googleMapsApiKey": "" + } }, "android": { "adaptiveIcon": { @@ -19,7 +22,13 @@ "monochromeImage": "./assets/images/android-icon-monochrome.png" }, "edgeToEdgeEnabled": true, - "predictiveBackGestureEnabled": false + "predictiveBackGestureEnabled": false, + "config": { + "googleMaps": { + "apiKey": "" + } + }, + "permissions": ["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"] }, "web": { "output": "static", @@ -38,6 +47,12 @@ "backgroundColor": "#000000" } } + ], + [ + "expo-location", + { + "locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location." + } ] ], "experiments": { diff --git a/mobile/app/(tabs)/_layout.tsx b/mobile/app/(tabs)/_layout.tsx index 54e11d0..ba401e2 100644 --- a/mobile/app/(tabs)/_layout.tsx +++ b/mobile/app/(tabs)/_layout.tsx @@ -1,10 +1,10 @@ -import { Tabs } from 'expo-router'; -import React from 'react'; +import { Tabs } from "expo-router"; +import React from "react"; -import { HapticTab } from '@/components/haptic-tab'; -import { IconSymbol } from '@/components/ui/icon-symbol'; -import { Colors } from '@/constants/theme'; -import { useColorScheme } from '@/hooks/use-color-scheme'; +import { HapticTab } from "@/components/haptic-tab"; +import { IconSymbol } from "@/components/ui/icon-symbol"; +import { Colors } from "@/constants/theme"; +import { useColorScheme } from "@/hooks/use-color-scheme"; export default function TabLayout() { const colorScheme = useColorScheme(); @@ -12,22 +12,27 @@ export default function TabLayout() { return ( + }} + > , + title: "Map", + tabBarIcon: ({ color }) => ( + + ), }} /> , + title: "Explore", + tabBarIcon: ({ color }) => ( + + ), }} /> diff --git a/mobile/app/(tabs)/index.tsx b/mobile/app/(tabs)/index.tsx index 786b736..9568592 100644 --- a/mobile/app/(tabs)/index.tsx +++ b/mobile/app/(tabs)/index.tsx @@ -1,98 +1,314 @@ -import { Image } from 'expo-image'; -import { Platform, StyleSheet } from 'react-native'; +import GooglePlacesAutocomplete from "@/components/google-places-autocomplete"; +import LocationDrawer from "@/components/location-drawer"; +import * as Location from "expo-location"; +import { useEffect, useRef, useState } from "react"; +import { Platform, StyleSheet, View } from "react-native"; +import MapView, { Marker, PROVIDER_GOOGLE } from "react-native-maps"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { HelloWave } from '@/components/hello-wave'; -import ParallaxScrollView from '@/components/parallax-scroll-view'; -import { ThemedText } from '@/components/themed-text'; -import { ThemedView } from '@/components/themed-view'; -import { Link } from 'expo-router'; +interface SearchResult { + latitude: number; + longitude: number; + name: string; + address: string; +} export default function HomeScreen() { + const insets = useSafeAreaInsets(); + const [location, setLocation] = useState( + null + ); + const [errorMsg, setErrorMsg] = useState(null); + const [searchResult, setSearchResult] = useState(null); + const [drawerVisible, setDrawerVisible] = useState(false); + const mapRef = useRef(null); + + useEffect(() => { + (async () => { + const { status } = await Location.requestForegroundPermissionsAsync(); + if (status !== "granted") { + setErrorMsg("Permission to access location was denied"); + return; + } + + const location = await Location.getCurrentPositionAsync({}); + setLocation(location); + })(); + }, []); + + // Recenter map when drawer closes + useEffect(() => { + if (!drawerVisible && searchResult && mapRef.current) { + // Get current region to preserve zoom level + mapRef.current.getCamera().then((camera) => { + // Just adjust the center point without changing zoom + mapRef.current?.animateCamera( + { + center: { + latitude: searchResult.latitude, + longitude: searchResult.longitude, + }, + zoom: camera.zoom, + }, + { duration: 250 } + ); + }); + } + }, [drawerVisible, searchResult]); + + if (errorMsg) { + console.error(errorMsg); + } + + const handlePlaceSelect = (data: any, details: any) => { + const searchLocationData = { + latitude: details.geometry.location.lat, + longitude: details.geometry.location.lng, + name: + details.name || + data.structured_formatting?.main_text || + data.description.split(",")[0], + address: details.formatted_address || data.description, + }; + setSearchResult(searchLocationData); + setDrawerVisible(true); + + // Animate map to the search result (offset for drawer visibility) + if (mapRef.current) { + // Shift latitude to center in the visible area (drawer takes 60%, so center in top 40%) + // To center in the top 40%, shift up by 30% of the total + const adjustedLatitude = searchLocationData.latitude - 0.05 * 0.3; + + mapRef.current.animateToRegion( + { + latitude: adjustedLatitude, + longitude: searchLocationData.longitude, + latitudeDelta: 0.05, + longitudeDelta: 0.05, + }, + 1000 + ); + } + }; + + const handlePoiClick = async (e: any) => { + const poi = e.nativeEvent; + if (!poi.placeId) return; + + // Close any open drawer first + if (drawerVisible) { + setDrawerVisible(false); + } + + // Clear previous search result + setSearchResult(null); + + try { + const apiKey = process.env.EXPO_PUBLIC_GOOGLE_MAPS_IOS_API_KEY || ""; + + if (apiKey) { + // Get place details using the place ID + const detailsUrl = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${poi.placeId}&key=${apiKey}`; + const response = await fetch(detailsUrl); + const data = await response.json(); + + if (data.result) { + const locationData = { + latitude: data.result.geometry.location.lat, + longitude: data.result.geometry.location.lng, + name: data.result.name, + address: + data.result.formatted_address || data.result.vicinity || "", + }; + + setSearchResult(locationData); + setDrawerVisible(true); + + // Animate map to the POI (offset for drawer visibility) + if (mapRef.current) { + // Shift latitude to center in the visible area (drawer takes 60%, so center in top 40%) + // To center in the top 40%, shift up by 30% of the total + const adjustedLatitude = locationData.latitude - 0.005 * 0.3; + + mapRef.current.animateToRegion( + { + latitude: adjustedLatitude, + longitude: locationData.longitude, + latitudeDelta: 0.005, + longitudeDelta: 0.005, + }, + 500 + ); + } + } + } + } catch (error) { + console.error("POI click error:", error); + } + }; + + const handleMapPress = async (e: any) => { + const coordinate = e.nativeEvent.coordinate; + if (!coordinate) return; + + try { + const apiKey = process.env.EXPO_PUBLIC_GOOGLE_MAPS_IOS_API_KEY || ""; + + // First, try to find a nearby place using Google Places Nearby Search + let locationData = null; + + if (apiKey) { + try { + const nearbyUrl = `https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=${coordinate.latitude},${coordinate.longitude}&radius=50&key=${apiKey}`; + const nearbyResponse = await fetch(nearbyUrl); + const nearbyData = await nearbyResponse.json(); + + if (nearbyData.results && nearbyData.results.length > 0) { + const place = nearbyData.results[0]; + + // Get full place details + const detailsUrl = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${place.place_id}&key=${apiKey}`; + const detailsResponse = await fetch(detailsUrl); + const detailsData = await detailsResponse.json(); + + if (detailsData.result) { + locationData = { + latitude: place.geometry.location.lat, + longitude: place.geometry.location.lng, + name: place.name, + address: detailsData.result.formatted_address || place.vicinity, + }; + } + } + } catch (error) { + console.error("Places API error:", error); + } + } + + // Fallback to reverse geocoding if no place found + if (!locationData) { + const reverseGeocode = await Location.reverseGeocodeAsync({ + latitude: coordinate.latitude, + longitude: coordinate.longitude, + }); + + if (reverseGeocode.length > 0) { + const geo = reverseGeocode[0]; + const addressParts = [ + geo.street, + geo.city, + geo.region, + geo.country, + ].filter(Boolean); + + const addressName = + addressParts.join(", ") || + `Location (${coordinate.latitude.toFixed( + 4 + )}, ${coordinate.longitude.toFixed(4)})`; + + locationData = { + latitude: coordinate.latitude, + longitude: coordinate.longitude, + name: geo.street || geo.city || addressName, + address: + addressParts.join(", ") || + `${coordinate.latitude.toFixed( + 6 + )}, ${coordinate.longitude.toFixed(6)}`, + }; + } + } + + if (locationData) { + setSearchResult(locationData); + setDrawerVisible(true); + } + } catch (error) { + console.error("Map press error:", error); + } + }; + + const initialRegion = searchResult + ? { + latitude: searchResult.latitude, + longitude: searchResult.longitude, + latitudeDelta: 0.05, + longitudeDelta: 0.05, + } + : { + latitude: location?.coords.latitude || 37.78825, + longitude: location?.coords.longitude || -122.4324, + latitudeDelta: 0.0922, + longitudeDelta: 0.0421, + }; + return ( - + + {location && ( + + )} + {searchResult && ( + + )} + + + - }> - - Welcome! - - - - Step 1: Try it - - Edit app/(tabs)/index.tsx to see changes. - Press{' '} - - {Platform.select({ - ios: 'cmd + d', - android: 'cmd + m', - web: 'F12', - })} - {' '} - to open developer tools. - - - - - - Step 2: Explore - - - - alert('Action pressed')} /> - alert('Share pressed')} - /> - - alert('Delete pressed')} - /> - - - - - - {`Tap the Explore tab to learn more about what's included in this starter app.`} - - - - Step 3: Get a fresh start - - {`When you're ready, run `} - npm run reset-project to get a fresh{' '} - app directory. This will move the current{' '} - app to{' '} - app-example. - - - + + setDrawerVisible(false)} + /> + ); } const styles = StyleSheet.create({ - titleContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, + container: { + flex: 1, }, - stepContainer: { - gap: 8, - marginBottom: 8, + map: { + flex: 1, }, - reactLogo: { - height: 178, - width: 290, - bottom: 0, - left: 0, - position: 'absolute', + searchContainer: { + position: "absolute", + left: 10, + right: 10, + zIndex: 1, }, }); diff --git a/mobile/components/google-places-autocomplete.tsx b/mobile/components/google-places-autocomplete.tsx new file mode 100644 index 0000000..5836aa2 --- /dev/null +++ b/mobile/components/google-places-autocomplete.tsx @@ -0,0 +1,238 @@ +import { useEffect, useRef, useState } from "react"; +import { + ActivityIndicator, + FlatList, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; + +interface GooglePlacesAutocompleteProps { + placeholder?: string; + onSelect: (data: any, details: any) => void; + apiKey?: string; + styles?: any; +} + +// For Expo, environment variables need to be prefixed with EXPO_PUBLIC_ +// They're available at runtime via process.env +const getApiKey = () => { + // Check if running in Expo and has environment variable + if (typeof process !== "undefined" && process.env) { + return ( + process.env.EXPO_PUBLIC_GOOGLE_PLACES_API_KEY || + process.env.EXPO_PUBLIC_GOOGLE_MAPS_IOS_API_KEY || + "" + ); + } + return ""; +}; + +export default function GooglePlacesAutocomplete({ + placeholder = "Search for a location...", + onSelect, + apiKey, + styles: customStyles = {}, +}: GooglePlacesAutocompleteProps) { + const [searchText, setSearchText] = useState(""); + const [predictions, setPredictions] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [showSuggestions, setShowSuggestions] = useState(false); + + const fetchPredictions = async (input: string) => { + console.log("fetchPredictions called with:", input); + if (input.length < 2) { + setPredictions([]); + setShowSuggestions(false); + return; + } + + setIsLoading(true); + try { + const key = apiKey || getApiKey(); + console.log("API Key:", key ? "Found" : "NOT FOUND"); + if (!key) { + console.warn( + "Google Places API key not configured. Add EXPO_PUBLIC_GOOGLE_PLACES_API_KEY to your .env.local or pass as prop." + ); + setIsLoading(false); + return; + } + + const url = `https://maps.googleapis.com/maps/api/place/autocomplete/json?input=${encodeURIComponent( + input + )}&key=${key}`; + console.log("Fetching from URL:", url); + + const response = await fetch(url); + const data = await response.json(); + console.log("API Response:", data); + + if (data.predictions) { + console.log("Found predictions:", data.predictions.length); + setPredictions(data.predictions); + setShowSuggestions(true); + } else { + console.log("No predictions in response:", data); + } + } catch (error) { + console.error("Autocomplete error:", error); + } finally { + setIsLoading(false); + } + }; + + const debounceTimer = useRef | null>(null); + + const handleTextChange = (text: string) => { + console.log("Text changed:", text); + setSearchText(text); + + // Debounce the API call + if (debounceTimer.current) { + clearTimeout(debounceTimer.current); + } + + debounceTimer.current = setTimeout(() => { + console.log("Debounced - fetching predictions for:", text); + if (text.length >= 2) { + fetchPredictions(text); + } else { + setPredictions([]); + setShowSuggestions(false); + } + }, 300); + }; + + useEffect(() => { + return () => { + if (debounceTimer.current) { + clearTimeout(debounceTimer.current); + } + }; + }, []); + + const handleSelect = async (placeId: string, description: string) => { + try { + // Get place details to get coordinates + const key = apiKey || getApiKey(); + const url = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&key=${key}`; + const response = await fetch(url); + const data = await response.json(); + + if (data.result) { + const location = data.result.geometry.location; + onSelect( + { description }, + { + geometry: { + location: { + lat: location.lat, + lng: location.lng, + }, + }, + formatted_address: data.result.formatted_address || description, + } + ); + } + + setSearchText(description); + setShowSuggestions(false); + setPredictions([]); + } catch (error) { + console.error("Place details error:", error); + } + }; + + return ( + + + { + if (predictions.length > 0) setShowSuggestions(true); + }} + /> + {isLoading && ( + + )} + + {showSuggestions && predictions.length > 0 && ( + + item.place_id} + renderItem={({ item }) => ( + handleSelect(item.place_id, item.description)} + > + {item.description} + + )} + keyboardShouldPersistTaps="handled" + /> + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + position: "relative", + zIndex: 10, + }, + inputContainer: { + position: "relative", + }, + input: { + backgroundColor: "white", + paddingHorizontal: 15, + paddingVertical: 12, + borderRadius: 999, + fontSize: 16, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + loader: { + position: "absolute", + right: 15, + top: 12, + }, + listContainer: { + backgroundColor: "white", + borderRadius: 8, + marginTop: 5, + maxHeight: 200, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + listItem: { + paddingHorizontal: 15, + paddingVertical: 12, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: "#e0e0e0", + }, + listItemText: { + fontSize: 14, + color: "#333", + }, +}); diff --git a/mobile/components/location-drawer.tsx b/mobile/components/location-drawer.tsx new file mode 100644 index 0000000..2cfa9a1 --- /dev/null +++ b/mobile/components/location-drawer.tsx @@ -0,0 +1,380 @@ +import React from "react"; +import { + Animated, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +interface LocationInfo { + name: string; + address: string; + latitude: number; + longitude: number; +} + +interface LocationDrawerProps { + location: LocationInfo | null; + visible: boolean; + onClose: () => void; +} + +export default function LocationDrawer({ + location, + visible, + onClose, +}: LocationDrawerProps) { + const insets = useSafeAreaInsets(); + const translateY = React.useRef(new Animated.Value(600)).current; + + React.useEffect(() => { + if (visible) { + Animated.spring(translateY, { + toValue: 0, + useNativeDriver: true, + tension: 50, + friction: 10, + }).start(); + } else { + Animated.spring(translateY, { + toValue: 600, + useNativeDriver: true, + tension: 50, + friction: 10, + }).start(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visible]); + + if (!location) return null; + + return ( + + + {/* Drag handle */} + + + {/* Header with actions */} + + + + 🔖 + + + 📤 + + + + + + + + + {/* Title */} + {location.name} + {location.address} + + {/* Rating and details */} + + + 4.5 + ⭐⭐⭐⭐⭐ + (127) + + + + {/* Distance/Time */} + + 📍 6 min + + Restaurant + + $$ + + + {/* Status */} + + Closed + Opens 7 AM + + + {/* Action Buttons */} + + + Directions + + + + + Start + + + + Ask + + + 🍽️ + Order + + + + {/* Photos Section */} + + Photos + + + 📷 + + + 📷 + + + 📷 + + + + + {/* Coordinates (for debugging) */} + + + Coordinates + + + Lat: + {location.latitude.toFixed(6)} + + + Lng: + {location.longitude.toFixed(6)} + + + + + + ); +} + +const styles = StyleSheet.create({ + overlay: { + position: "absolute", + bottom: 0, + left: 0, + right: 0, + height: "60%", + backgroundColor: "transparent", + }, + hidden: { + display: "none", + }, + drawer: { + flex: 1, + backgroundColor: "white", + borderTopLeftRadius: 15, + borderTopRightRadius: 15, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: -2, + }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 8, + }, + handle: { + width: 36, + height: 4, + backgroundColor: "#ccc", + borderRadius: 2, + alignSelf: "center", + marginVertical: 8, + }, + header: { + paddingHorizontal: 16, + paddingTop: 8, + }, + headerActions: { + flexDirection: "row", + justifyContent: "flex-end", + alignItems: "center", + gap: 8, + }, + iconButton: { + width: 36, + height: 36, + justifyContent: "center", + alignItems: "center", + }, + iconText: { + fontSize: 20, + }, + content: { + flex: 1, + }, + title: { + fontSize: 24, + fontWeight: "bold", + paddingHorizontal: 16, + paddingBottom: 4, + color: "#212121", + }, + address: { + fontSize: 15, + paddingHorizontal: 16, + paddingBottom: 12, + color: "#757575", + }, + detailsRow: { + paddingHorizontal: 16, + marginBottom: 8, + }, + rating: { + flexDirection: "row", + alignItems: "center", + gap: 6, + }, + ratingValue: { + fontSize: 18, + fontWeight: "500", + color: "#212121", + }, + stars: { + fontSize: 16, + }, + ratingCount: { + fontSize: 14, + color: "#757575", + }, + metaInfo: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 16, + marginBottom: 8, + gap: 8, + }, + metaText: { + fontSize: 14, + color: "#757575", + }, + divider: { + fontSize: 14, + color: "#757575", + }, + statusContainer: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 16, + marginBottom: 16, + gap: 8, + }, + statusClosed: { + fontSize: 14, + color: "#d32f2f", + fontWeight: "500", + }, + statusHours: { + fontSize: 14, + color: "#757575", + }, + actionButtons: { + flexDirection: "row", + paddingHorizontal: 16, + gap: 8, + marginBottom: 20, + }, + actionButtonPrimary: { + flex: 2.5, + backgroundColor: "#14a085", + borderRadius: 20, + paddingVertical: 12, + paddingHorizontal: 20, + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: 8, + }, + actionButtonSecondary: { + flex: 1, + backgroundColor: "#e0e0e0", + borderRadius: 20, + paddingVertical: 12, + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + gap: 4, + }, + actionButtonText: { + fontSize: 13, + fontWeight: "500", + color: "#212121", + }, + actionButtonTextPrimary: { + fontSize: 13, + fontWeight: "500", + color: "white", + }, + actionButtonIcon: { + fontSize: 16, + }, + actionButtonIconPrimary: { + fontSize: 16, + color: "white", + }, + photosSection: { + paddingHorizontal: 16, + marginBottom: 20, + }, + sectionTitle: { + fontSize: 16, + fontWeight: "600", + color: "#212121", + marginBottom: 12, + }, + photoPlaceholder: { + width: 200, + height: 150, + backgroundColor: "#e0e0e0", + borderRadius: 8, + marginRight: 12, + justifyContent: "center", + alignItems: "center", + }, + photoPlaceholderText: { + fontSize: 48, + }, + coordsSection: { + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: "#f5f5f5", + borderRadius: 8, + marginBottom: 20, + }, + infoRow: { + flexDirection: "row", + justifyContent: "space-between", + paddingVertical: 6, + }, + label: { + fontSize: 12, + color: "#757575", + fontWeight: "500", + }, + value: { + fontSize: 12, + color: "#212121", + fontFamily: "monospace", + }, +}); diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 66ce321..c59bda0 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -18,6 +18,7 @@ "expo-haptics": "~15.0.7", "expo-image": "~3.0.10", "expo-linking": "~8.0.8", + "expo-location": "~19.0.7", "expo-router": "~6.0.13", "expo-splash-screen": "~31.0.10", "expo-status-bar": "~3.0.8", @@ -28,6 +29,7 @@ "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", + "react-native-maps": "1.20.1", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", @@ -3293,6 +3295,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://artifactory.rbx.com/artifactory/api/npm/npm-all/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://artifactory.rbx.com/artifactory/api/npm/npm-all/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -6301,6 +6309,15 @@ "react-native": "*" } }, + "node_modules/expo-location": { + "version": "19.0.7", + "resolved": "https://artifactory.rbx.com/artifactory/api/npm/npm-all/expo-location/-/expo-location-19.0.7.tgz", + "integrity": "sha512-YNkh4r9E6ECbPkBCAMG5A5yHDgS0pw+Rzyd0l2ZQlCtjkhlODB55nMCKr5CZnUI0mXTkaSm8CwfoCO8n2MpYfg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "3.0.19", "resolved": "https://artifactory.rbx.com/artifactory/api/npm/npm-all/expo-modules-autolinking/-/expo-modules-autolinking-3.0.19.tgz", @@ -10565,6 +10582,28 @@ "react-native": "*" } }, + "node_modules/react-native-maps": { + "version": "1.20.1", + "resolved": "https://artifactory.rbx.com/artifactory/api/npm/npm-all/react-native-maps/-/react-native-maps-1.20.1.tgz", + "integrity": "sha512-NZI3B5Z6kxAb8gzb2Wxzu/+P2SlFIg1waHGIpQmazDSCRkNoHNY4g96g+xS0QPSaG/9xRBbDNnd2f2/OW6t6LQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.13" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": ">= 17.0.1", + "react-native": ">= 0.64.3", + "react-native-web": ">= 0.11" + }, + "peerDependenciesMeta": { + "react-native-web": { + "optional": true + } + } + }, "node_modules/react-native-reanimated": { "version": "4.1.3", "resolved": "https://artifactory.rbx.com/artifactory/api/npm/npm-all/react-native-reanimated/-/react-native-reanimated-4.1.3.tgz", diff --git a/mobile/package.json b/mobile/package.json index 2e19a6b..1eda01f 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -21,6 +21,7 @@ "expo-haptics": "~15.0.7", "expo-image": "~3.0.10", "expo-linking": "~8.0.8", + "expo-location": "~19.0.7", "expo-router": "~6.0.13", "expo-splash-screen": "~31.0.10", "expo-status-bar": "~3.0.8", @@ -31,11 +32,12 @@ "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", - "react-native-worklets": "0.5.1", + "react-native-maps": "1.20.1", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", - "react-native-web": "~0.21.0" + "react-native-web": "~0.21.0", + "react-native-worklets": "0.5.1" }, "devDependencies": { "@types/react": "~19.1.0",