Gracefully handle missing MapLibre native module

Use try/catch require() for MapLibre so the app works without it
(shows fallback UI). Needed when running on builds that don't include
MapLibre native module (old EAS builds, Expo Go).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-15 07:28:25 +02:00
parent d74a19afb6
commit 58bff065ff
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9

View file

@ -1,9 +1,18 @@
import { useRef, useCallback } from "react"; import { useRef, useCallback } from "react";
import { StyleSheet, View, Text } from "react-native"; import { StyleSheet, View, Text } from "react-native";
import MapLibreGL from "@maplibre/maplibre-react-native";
import type { Waypoint } from "@trails-cool/types"; import type { Waypoint } from "@trails-cool/types";
import type { RouteSegment } from "./use-route-editor"; import type { RouteSegment } from "./use-route-editor";
import type MapLibreRN from "@maplibre/maplibre-react-native";
let MapLibreGL: typeof MapLibreRN | null = null;
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
MapLibreGL = require("@maplibre/maplibre-react-native").default;
} catch {
// Native module not available — will show fallback UI
}
const OSM_STYLE = { const OSM_STYLE = {
version: 8 as const, version: 8 as const,
sources: { sources: {
@ -40,7 +49,32 @@ export function RouteMap({
onWaypointDragEnd: _onWaypointDragEnd, onWaypointDragEnd: _onWaypointDragEnd,
onWaypointPress, onWaypointPress,
}: RouteMapProps) { }: RouteMapProps) {
const cameraRef = useRef<MapLibreGL.CameraRef>(null); if (!MapLibreGL) {
return (
<View style={styles.fallback}>
<Text style={styles.fallbackText}>Map</Text>
<Text style={styles.fallbackHint}>Requires a dev build with MapLibre</Text>
</View>
);
}
return <RouteMapInner
waypoints={waypoints} segments={segments} computing={computing}
onLongPress={onLongPress} onWaypointDragEnd={_onWaypointDragEnd}
onWaypointPress={onWaypointPress}
/>;
}
function RouteMapInner({
waypoints,
segments,
computing,
onLongPress,
onWaypointDragEnd: _onWaypointDragEnd,
onWaypointPress,
}: RouteMapProps) {
const ML = MapLibreGL!;
const cameraRef = useRef<MapLibreRN.CameraRef>(null);
const handleLongPress = useCallback( const handleLongPress = useCallback(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -71,7 +105,7 @@ export function RouteMap({
return ( return (
<View style={styles.container}> <View style={styles.container}>
<MapLibreGL.MapView <ML.MapView
style={styles.map} style={styles.map}
mapStyle={OSM_STYLE} mapStyle={OSM_STYLE}
onLongPress={handleLongPress} onLongPress={handleLongPress}
@ -79,7 +113,7 @@ export function RouteMap({
logoEnabled={false} logoEnabled={false}
> >
{bounds && ( {bounds && (
<MapLibreGL.Camera <ML.Camera
ref={cameraRef} ref={cameraRef}
defaultSettings={{ defaultSettings={{
bounds: { bounds: {
@ -95,7 +129,7 @@ export function RouteMap({
)} )}
{!bounds && ( {!bounds && (
<MapLibreGL.Camera <ML.Camera
defaultSettings={{ defaultSettings={{
centerCoordinate: [10.0, 50.1], centerCoordinate: [10.0, 50.1],
zoomLevel: 6, zoomLevel: 6,
@ -104,8 +138,8 @@ export function RouteMap({
)} )}
{/* Route line */} {/* Route line */}
<MapLibreGL.ShapeSource id="route" shape={routeGeojson}> <ML.ShapeSource id="route" shape={routeGeojson}>
<MapLibreGL.LineLayer <ML.LineLayer
id="route-line" id="route-line"
style={{ style={{
lineColor: "#2563eb", lineColor: "#2563eb",
@ -113,11 +147,11 @@ export function RouteMap({
lineOpacity: computing ? 0.4 : 1, lineOpacity: computing ? 0.4 : 1,
}} }}
/> />
</MapLibreGL.ShapeSource> </ML.ShapeSource>
{/* Waypoint markers */} {/* Waypoint markers */}
{waypoints.map((wp, i) => ( {waypoints.map((wp, i) => (
<MapLibreGL.MarkerView <ML.MarkerView
key={`wp-${i}`} key={`wp-${i}`}
coordinate={[wp.lon, wp.lat]} coordinate={[wp.lon, wp.lat]}
> >
@ -126,9 +160,9 @@ export function RouteMap({
isDayBreak={wp.isDayBreak} isDayBreak={wp.isDayBreak}
onPress={() => onWaypointPress(i)} onPress={() => onWaypointPress(i)}
/> />
</MapLibreGL.MarkerView> </ML.MarkerView>
))} ))}
</MapLibreGL.MapView> </ML.MapView>
{computing && ( {computing && (
<View style={styles.computingBanner}> <View style={styles.computingBanner}>
@ -186,6 +220,14 @@ function computeBounds(
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
fallback: {
flex: 1,
backgroundColor: "#f3f4f6",
justifyContent: "center",
alignItems: "center",
},
fallbackText: { fontSize: 16, color: "#9ca3af", fontWeight: "600" },
fallbackHint: { fontSize: 12, color: "#9ca3af", marginTop: 4 },
container: { flex: 1 }, container: { flex: 1 },
map: { flex: 1 }, map: { flex: 1 },
computingBanner: { computingBanner: {