|
1 | | -import * as Location from 'expo-location'; |
2 | | -import React, { useCallback, useEffect, useRef, useState } from 'react'; |
3 | | -import { Modal, Pressable, StyleSheet, Text, View } from 'react-native'; |
4 | | -import { WebView, type WebViewMessageEvent } from 'react-native-webview'; |
5 | | -import { PrimaryButton, colors, space, type as typo } from '@wash-and-go/ui'; |
| 1 | +import React from 'react'; |
| 2 | +import { MapPicker as UIMapPicker, resolveTiles } from '@wash-and-go/ui'; |
6 | 3 | import { api } from '../lib/api'; |
7 | 4 |
|
8 | | -// Center-pin map picker (Grab/Uber style): the map moves under a fixed pin; the |
9 | | -// map centre is the chosen point. TomTom tiles via Leaflet in a WebView (reuses |
10 | | -// the TomTom key, cross-platform, no Google key). Replaces the unreliable |
11 | | -// type-an-address flow. |
12 | | -const KEY = process.env.EXPO_PUBLIC_TOMTOM_MAP_KEY ?? ''; |
13 | | -const ZAMBOANGA: [number, number] = [6.9214, 122.079]; |
14 | | - |
15 | | -function mapHtml(lat: number, lng: number): string { |
16 | | - return `<!doctype html><html><head> |
17 | | -<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> |
18 | | -<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/> |
19 | | -<style> |
20 | | - html,body,#map{height:100%;margin:0;padding:0;background:#e7eef5} |
21 | | - #pin{position:absolute;top:50%;left:50%;transform:translate(-50%,-100%);font-size:40px;z-index:1000;pointer-events:none;filter:drop-shadow(0 3px 3px rgba(0,0,0,.35))} |
22 | | -</style></head><body> |
23 | | -<div id="map"></div><div id="pin">📍</div> |
24 | | -<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> |
25 | | -<script> |
26 | | - var map = L.map('map',{zoomControl:false,attributionControl:false}).setView([${lat},${lng}],16); |
27 | | - L.tileLayer('https://api.tomtom.com/map/1/tile/basic/main/{z}/{x}/{y}.png?key=${KEY}',{maxZoom:22,minZoom:5}).addTo(map); |
28 | | - function post(){var c=map.getCenter();window.ReactNativeWebView.postMessage(JSON.stringify({lat:c.lat,lng:c.lng}));} |
29 | | - map.on('moveend', post); |
30 | | - setTimeout(post, 300); |
31 | | -</script></body></html>`; |
32 | | -} |
| 5 | +// App-local wrapper: injects this app's API client (reverse-geocode) and the |
| 6 | +// tile provider resolved from env into the shared UI picker. Tile provider is |
| 7 | +// pluggable — set EXPO_PUBLIC_MAP_TILE_PROVIDER=maptiler|mapbox|tomtom|osm and |
| 8 | +// the matching key; falls back to the sharpest key present, then keyless OSM. |
| 9 | +const tiles = resolveTiles({ |
| 10 | + provider: process.env.EXPO_PUBLIC_MAP_TILE_PROVIDER, |
| 11 | + mapboxToken: process.env.EXPO_PUBLIC_MAPBOX_TOKEN, |
| 12 | + mapTilerKey: process.env.EXPO_PUBLIC_MAPTILER_KEY, |
| 13 | + tomtomKey: process.env.EXPO_PUBLIC_TOMTOM_MAP_KEY, |
| 14 | +}); |
33 | 15 |
|
34 | | -export function MapPicker({ |
35 | | - visible, |
36 | | - initial, |
37 | | - onClose, |
38 | | - onPick, |
39 | | -}: { |
| 16 | +export function MapPicker(props: { |
40 | 17 | visible: boolean; |
41 | 18 | initial?: { lat: number; lng: number } | null; |
42 | 19 | onClose: () => void; |
43 | 20 | onPick: (p: { lat: number; lng: number; address: string }) => void; |
44 | 21 | }) { |
45 | | - const [center, setCenter] = useState<[number, number] | null>(null); |
46 | | - const [coords, setCoords] = useState<{ lat: number; lng: number } | null>(null); |
47 | | - const [label, setLabel] = useState<string>('Move the map to your pickup point'); |
48 | | - const [resolving, setResolving] = useState(false); |
49 | | - const revTimer = useRef<ReturnType<typeof setTimeout> | null>(null); |
50 | | - |
51 | | - // Decide the starting centre once the sheet opens: passed-in point → device |
52 | | - // GPS → Zamboanga fallback. |
53 | | - useEffect(() => { |
54 | | - if (!visible) { |
55 | | - setCenter(null); |
56 | | - return; |
57 | | - } |
58 | | - let cancelled = false; |
59 | | - (async () => { |
60 | | - if (initial) { |
61 | | - if (!cancelled) setCenter([initial.lat, initial.lng]); |
62 | | - return; |
63 | | - } |
64 | | - try { |
65 | | - const perm = await Location.getForegroundPermissionsAsync(); |
66 | | - if (perm.status === 'granted') { |
67 | | - const pos = await Location.getCurrentPositionAsync({ |
68 | | - accuracy: Location.Accuracy.Balanced, |
69 | | - }); |
70 | | - if (!cancelled) { |
71 | | - setCenter([pos.coords.latitude, pos.coords.longitude]); |
72 | | - return; |
73 | | - } |
74 | | - } |
75 | | - } catch { |
76 | | - // fall through to default |
77 | | - } |
78 | | - if (!cancelled) setCenter(ZAMBOANGA); |
79 | | - })(); |
80 | | - return () => { |
81 | | - cancelled = true; |
82 | | - }; |
83 | | - }, [visible, initial]); |
84 | | - |
85 | | - const onMessage = useCallback((e: WebViewMessageEvent) => { |
86 | | - let c: { lat: number; lng: number }; |
87 | | - try { |
88 | | - c = JSON.parse(e.nativeEvent.data); |
89 | | - } catch { |
90 | | - return; |
91 | | - } |
92 | | - setCoords(c); |
93 | | - // Debounce the reverse-geocode — one call after the map settles. |
94 | | - if (revTimer.current) clearTimeout(revTimer.current); |
95 | | - setResolving(true); |
96 | | - revTimer.current = setTimeout(async () => { |
97 | | - try { |
98 | | - const { label: l } = await api.reverseGeocode(c.lat, c.lng); |
99 | | - setLabel(l ?? `${c.lat.toFixed(5)}, ${c.lng.toFixed(5)}`); |
100 | | - } catch { |
101 | | - setLabel(`${c.lat.toFixed(5)}, ${c.lng.toFixed(5)}`); |
102 | | - } finally { |
103 | | - setResolving(false); |
104 | | - } |
105 | | - }, 450); |
106 | | - }, []); |
107 | | - |
108 | | - const confirm = () => { |
109 | | - if (!coords) return; |
110 | | - onPick({ lat: coords.lat, lng: coords.lng, address: label }); |
111 | | - }; |
112 | | - |
113 | 22 | return ( |
114 | | - <Modal visible={visible} animationType="slide" onRequestClose={onClose}> |
115 | | - <View style={styles.root}> |
116 | | - {center ? ( |
117 | | - <WebView |
118 | | - style={{ flex: 1 }} |
119 | | - originWhitelist={['*']} |
120 | | - source={{ html: mapHtml(center[0], center[1]) }} |
121 | | - onMessage={onMessage} |
122 | | - scrollEnabled={false} |
123 | | - /> |
124 | | - ) : ( |
125 | | - <View style={styles.loading}> |
126 | | - <Text style={typo.body}>Getting your location…</Text> |
127 | | - </View> |
128 | | - )} |
129 | | - |
130 | | - <Pressable onPress={onClose} style={styles.close} accessibilityRole="button"> |
131 | | - <Text style={styles.closeText}>✕</Text> |
132 | | - </Pressable> |
133 | | - |
134 | | - <View style={styles.sheet}> |
135 | | - <Text style={styles.sheetLabel} numberOfLines={2}> |
136 | | - {resolving ? 'Locating…' : label} |
137 | | - </Text> |
138 | | - <PrimaryButton |
139 | | - label="Use this location" |
140 | | - onPress={confirm} |
141 | | - disabled={!coords} |
142 | | - testID="map-confirm" |
143 | | - /> |
144 | | - </View> |
145 | | - </View> |
146 | | - </Modal> |
| 23 | + <UIMapPicker |
| 24 | + {...props} |
| 25 | + tiles={tiles} |
| 26 | + reverseGeocode={(lat, lng) => api.reverseGeocode(lat, lng)} |
| 27 | + /> |
147 | 28 | ); |
148 | 29 | } |
149 | | - |
150 | | -const styles = StyleSheet.create({ |
151 | | - root: { flex: 1, backgroundColor: colors.bg }, |
152 | | - loading: { flex: 1, alignItems: 'center', justifyContent: 'center' }, |
153 | | - close: { |
154 | | - position: 'absolute', |
155 | | - top: 52, |
156 | | - left: 16, |
157 | | - width: 40, |
158 | | - height: 40, |
159 | | - borderRadius: 20, |
160 | | - backgroundColor: colors.surface, |
161 | | - alignItems: 'center', |
162 | | - justifyContent: 'center', |
163 | | - shadowColor: '#000', |
164 | | - shadowOpacity: 0.18, |
165 | | - shadowRadius: 6, |
166 | | - shadowOffset: { width: 0, height: 2 }, |
167 | | - elevation: 4, |
168 | | - }, |
169 | | - closeText: { fontSize: 18, color: colors.text, fontWeight: '700' }, |
170 | | - sheet: { |
171 | | - padding: space.lg, |
172 | | - paddingBottom: space.xl, |
173 | | - gap: space.md, |
174 | | - backgroundColor: colors.surface, |
175 | | - borderTopLeftRadius: 20, |
176 | | - borderTopRightRadius: 20, |
177 | | - }, |
178 | | - sheetLabel: { ...typo.title, color: colors.text }, |
179 | | -}); |
0 commit comments