Files
waseel/components/pin-adjuster.tsx
T
KrikoriosandClaude Opus 5 8807ff41c5 Waseel: driver capture, chat/calls, dispatch, and session fixes
Driver onboarding now photographs the licence, ID card and vehicle
registration and reads the credential fields off them, plus a camera-only
profile selfie riders check the arriving driver against. Adds in-app chat
and WebRTC calls, push-backed ride offers, ratings, cancellation and
payment sheets, settlement, and the owner dashboard endpoints behind them.

Camera permission on Android:
  - Declare CAMERA and READ_MEDIA_IMAGES in the manifest. expo-image-picker's
    own plugin never declares CAMERA, and Android denies a request for an
    undeclared permission instantly and silently — no dialog is ever shown,
    which is indistinguishable from the app not asking at all.
  - Handle canAskAgain: once Android stops showing the dialog, repeating why
    we need it is a dead end, so offer Open Settings instead (lib/capture-
    permission.ts), matching what the location flow already did.

Session: a 401 on a request that carried a token now ends the session
instead of being reinterpreted per-screen — driver-home had been reading it
as "this user has no driver profile" and showing an onboarding form to an
already-onboarded driver. Requests without a token are exempt so a failed
sign-in doesn't sign you out, and the notification is latched per token so
concurrent polls tear the session down once. (root) gains the auth guard
that turns that into the sign-in screen; app/index.tsx only guarded the way
in, leaving a session that ended mid-screen with nowhere to go.

Also ignore .uploads/ — it holds driver licence, ID and vehicle scans plus
profile photos, which are personal data and must not be committed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 02:17:55 +03:00

100 lines
3.2 KiB
TypeScript

import { useRef } from "react";
import { Image, StyleSheet, View } from "react-native";
import MapView, { PROVIDER_DEFAULT, type Region } from "react-native-maps";
import { icons } from "@/constants";
import { useTheme } from "@/lib/theme";
// Fine-tuning a pickup or drop-off point.
//
// The pin does NOT move — the map moves under it. Dragging a marker means
// fighting for a few pixels with the same thumb that pans the map, and on a
// phone the marker spends most of the gesture hidden under the finger holding
// it. Anchoring the pin to the centre of the screen and sliding the map
// underneath makes the target the one thing always visible, which is why every
// ride-hailing app converged on it.
//
// The component is deliberately dumb: it reports the centre when the map
// settles and nothing else. Reverse geocoding, debouncing and confirmation all
// live on the screen, so this stays reusable for the origin and the
// destination alike.
export type PinAdjusterProps = {
initial: { latitude: number; longitude: number };
/** Fired when the map stops moving, with the coordinate under the pin. */
onSettled: (coords: { latitude: number; longitude: number }) => void;
/** Fired as soon as a drag starts, to clear a now-stale address label. */
onMoveStart?: () => void;
};
// Tight enough that the rider is choosing a doorway, not a district.
const ZOOM_DELTA = 0.004;
const styles = StyleSheet.create({
map: StyleSheet.absoluteFillObject,
// Sits above the map and ignores touches, so panning still reaches the map.
pinLayer: {
...StyleSheet.absoluteFillObject,
alignItems: "center",
justifyContent: "center",
},
pin: {
width: 36,
height: 36,
// The pin's point is at its bottom edge, but the coordinate we report is
// the centre of the screen — so lift it by its own height to put the tip,
// not the middle of the graphic, on the spot being chosen.
marginBottom: 36,
},
// A small ground marker under the tip: without it, on a busy map, it is
// genuinely hard to tell which pixel the pin is pointing at.
dot: {
position: "absolute",
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: "rgba(2,134,255,0.9)",
borderWidth: 1,
borderColor: "#ffffff",
},
});
export const PinAdjuster = ({
initial,
onSettled,
onMoveStart,
}: PinAdjusterProps) => {
const { isDark } = useTheme();
const mapRef = useRef<MapView>(null);
const region: Region = {
latitude: initial.latitude,
longitude: initial.longitude,
latitudeDelta: ZOOM_DELTA,
longitudeDelta: ZOOM_DELTA,
};
return (
<View style={StyleSheet.absoluteFill}>
<MapView
ref={mapRef}
provider={PROVIDER_DEFAULT}
style={styles.map}
initialRegion={region}
showsUserLocation
showsMyLocationButton={false}
userInterfaceStyle={isDark ? "dark" : "light"}
onPanDrag={onMoveStart}
onRegionChangeComplete={(next) =>
onSettled({ latitude: next.latitude, longitude: next.longitude })
}
/>
<View style={styles.pinLayer} pointerEvents="none">
<Image source={icons.pin} style={styles.pin} resizeMode="contain" />
<View style={styles.dot} />
</View>
</View>
);
};