From 6cc18648b64267f623f962b3000c53442ee06e0b Mon Sep 17 00:00:00 2001 From: Dennis Falling Date: Sun, 24 May 2026 22:51:21 +0100 Subject: [PATCH 1/3] Add MapLibre map with user location Replace the React Native template view with a MapLibre-powered map using OpenFreeMap's hosted vector tiles. Requests foreground-only location permissions on mount and flies to the user's first fix at zoom 14, with a UserLocation puck + accuracy circle. Co-Authored-By: Claude Opus 4.7 (1M context) --- App.tsx | 7 +-- android/app/src/main/AndroidManifest.xml | 2 + bun.lock | 32 +++++++++++- package.json | 1 + src/map/MapScreen.tsx | 62 ++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 src/map/MapScreen.tsx diff --git a/App.tsx b/App.tsx index 0684b92..eed2d1a 100644 --- a/App.tsx +++ b/App.tsx @@ -5,7 +5,6 @@ */ import {ApolloProvider} from '@apollo/client'; -import {NewAppScreen} from '@react-native/new-app-screen'; import {useEffect} from 'react'; import { ActivityIndicator, @@ -24,6 +23,7 @@ import {apolloClient, logout} from './src/auth/authClient'; import {useDeepLinkListener} from './src/auth/deepLinks'; import {LoginScreen} from './src/auth/LoginScreen'; import {tokenStore, useAuth, useAuthHydrated} from './src/auth/tokenStore'; +import {MapScreen} from './src/map/MapScreen'; function App() { const isDarkMode = useColorScheme() === 'dark'; @@ -63,10 +63,7 @@ function AppContent() { return ( - + Signed in as {auth.user.email} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ee65c85..b0d02b6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + =54.0.0", "@types/geojson": "^7946.0.0", "@types/react": ">=19.1.0", "react": ">=19.1.0", "react-native": ">=0.80.0" }, "optionalPeers": ["@expo/config-plugins", "@types/geojson", "@types/react"] }, "sha512-I3AqN3JUkgT1QzRBsqtsS+123uny86lEsye9+Q180rRlaoGSiRRUhiyYbF48eQcyehcn9GvoazCBNQ1dKHKJAA=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -519,6 +527,18 @@ "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], + "@turf/distance": ["@turf/distance@7.3.5", "", { "dependencies": { "@turf/helpers": "7.3.5", "@turf/invariant": "7.3.5", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-uQAC63zg/l91KUxzfhqio7Ii3+UXTrPOVJScIdRj6EO6+9XHI4kC+AdyIS4cPAv14sZfJLIBxzMnzcGrss+kEA=="], + + "@turf/helpers": ["@turf/helpers@7.3.5", "", { "dependencies": { "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-E/NMGV5MwbjjP7AJXBtsanC3yY8N2MQ87IGdIgkB2ji5AtBpwnH4L3gEqpYN4RlCJJWbLbzO91BbKv2waUd0eg=="], + + "@turf/invariant": ["@turf/invariant@7.3.5", "", { "dependencies": { "@turf/helpers": "7.3.5", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-ZVIvsBvjr8lO7WxC5zYNjRsjSDvyGvWkJMjuWaJjTU8x+1tmfNnw3gDX/TI2Sit83gcRYLYkNo23lB/udqx/Hg=="], + + "@turf/length": ["@turf/length@7.3.5", "", { "dependencies": { "@turf/distance": "7.3.5", "@turf/helpers": "7.3.5", "@turf/meta": "7.3.5", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-Bi+vEP54wt1ly3BRcCOP0nd2kGTYEhGk6haQxTpkrqr3XtmqDh8c3NowSgseN2cegIZRjwCOEC8eSsZ0JemJdA=="], + + "@turf/meta": ["@turf/meta@7.3.5", "", { "dependencies": { "@turf/helpers": "7.3.5", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-r+ohqxoyqeigFB0oFrQx/YEHIkOKqcKpCjvZkvZs7Tkv+IFco5MezAd2zd4rzK+0DfFgDP3KpJc7HqrYjvEjhg=="], + + "@turf/nearest-point-on-line": ["@turf/nearest-point-on-line@7.3.5", "", { "dependencies": { "@turf/distance": "7.3.5", "@turf/helpers": "7.3.5", "@turf/invariant": "7.3.5", "@turf/meta": "7.3.5", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-MZn6OkEFZpjS6BNUANfqiHMIbQSivu7TNji3a+OAIrnPJ71vp8cbz0N2aVEa5M7I8ipvxoxAPIV3eqg3h280Vg=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -527,6 +547,8 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], @@ -1073,6 +1095,8 @@ "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + "json-stringify-pretty-compact": ["json-stringify-pretty-compact@4.0.0", "", {}, "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q=="], + "json-to-pretty-yaml": ["json-to-pretty-yaml@1.2.2", "", { "dependencies": { "remedial": "^1.0.7", "remove-trailing-spaces": "^1.0.6" } }, "sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], @@ -1175,6 +1199,8 @@ "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1285,6 +1311,8 @@ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "quickselect": ["quickselect@3.0.0", "", {}, "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], @@ -1453,6 +1481,8 @@ "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + "tinyqueue": ["tinyqueue@3.0.0", "", {}, "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="], + "title-case": ["title-case@3.0.3", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA=="], "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], diff --git a/package.json b/package.json index f78582f..f29a25d 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@apollo/client": "^3.11.0", + "@maplibre/maplibre-react-native": "^11.2.1", "@react-native/new-app-screen": "0.85.3", "graphql": "^16.9.0", "react": "19.2.3", diff --git a/src/map/MapScreen.tsx b/src/map/MapScreen.tsx new file mode 100644 index 0000000..d6e1160 --- /dev/null +++ b/src/map/MapScreen.tsx @@ -0,0 +1,62 @@ +import { + Camera, + type CameraRef, + LocationManager, + Map as MapLibreMap, + UserLocation, + useCurrentPosition, +} from '@maplibre/maplibre-react-native'; +import {useEffect, useRef, useState} from 'react'; +import {StyleSheet, View} from 'react-native'; + +const MAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty'; +const USER_ZOOM = 14; + +export function MapScreen() { + const cameraRef = useRef(null); + const hasCenteredRef = useRef(false); + const [permissionGranted, setPermissionGranted] = useState(false); + + useEffect(() => { + let cancelled = false; + (async () => { + const granted = await LocationManager.requestPermissions(); + if (!cancelled && granted) { + setPermissionGranted(true); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + const position = useCurrentPosition({enabled: permissionGranted}); + + useEffect(() => { + if (hasCenteredRef.current || !position) return; + hasCenteredRef.current = true; + cameraRef.current?.flyTo({ + center: [position.coords.longitude, position.coords.latitude], + zoom: USER_ZOOM, + duration: 1500, + }); + }, [position]); + + return ( + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + map: { + flex: 1, + }, +}); From c8d99ed34c80e47405c7491af539a8a8c540c363 Mon Sep 17 00:00:00 2001 From: Dennis Falling Date: Sun, 24 May 2026 22:56:28 +0100 Subject: [PATCH 2/3] Mock @maplibre/maplibre-react-native in jest setup The package ships ESM that jest can't parse with the default transform config, causing App.test.tsx to fail. Mock it instead of widening transformIgnorePatterns so tests stay fast and isolated from native code. Co-Authored-By: Claude Opus 4.7 (1M context) --- jest.setup.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/jest.setup.js b/jest.setup.js index 776fdbb..9f886d1 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -7,3 +7,24 @@ jest.mock('react-native-encrypted-storage', () => ({ clear: jest.fn(() => Promise.resolve()), }, })); + +jest.mock('@maplibre/maplibre-react-native', () => { + const React = require('react'); + const passthrough = (name) => { + const Component = React.forwardRef(({children}, _ref) => + React.createElement('View', {testID: name}, children), + ); + Component.displayName = name; + return Component; + }; + return { + __esModule: true, + Map: passthrough('Map'), + Camera: passthrough('Camera'), + UserLocation: passthrough('UserLocation'), + LocationManager: { + requestPermissions: jest.fn(() => Promise.resolve(false)), + }, + useCurrentPosition: () => null, + }; +}); From 811f4869fcf7755835f0d48dfc4fea8d4a12b71f Mon Sep 17 00:00:00 2001 From: Dennis Falling Date: Sun, 24 May 2026 23:01:01 +0100 Subject: [PATCH 3/3] Add pre-push hook running tsc + biome Catches the same TypeScript and lint failures CI surfaces, before the push. Hook lives in `.githooks/` and is wired up via a postinstall script that sets `core.hooksPath` (skipped silently outside a git repo). No new dependency. Co-Authored-By: Claude Opus 4.7 (1M context) --- .githooks/pre-push | 8 ++++++++ jest.setup.js | 2 +- package.json | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100755 .githooks/pre-push diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..39dcedc --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "→ tsc --noEmit" +bunx tsc --noEmit + +echo "→ biome check" +bun run lint diff --git a/jest.setup.js b/jest.setup.js index 9f886d1..cccc7d2 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -10,7 +10,7 @@ jest.mock('react-native-encrypted-storage', () => ({ jest.mock('@maplibre/maplibre-react-native', () => { const React = require('react'); - const passthrough = (name) => { + const passthrough = name => { const Component = React.forwardRef(({children}, _ref) => React.createElement('View', {testID: name}, children), ); diff --git a/package.json b/package.json index f29a25d..6447b2e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "lint": "biome check", "lint:fix": "biome check --write", "format": "biome format --write", - "codegen": "graphql-codegen --config codegen.ts" + "codegen": "graphql-codegen --config codegen.ts", + "postinstall": "git rev-parse --git-dir > /dev/null 2>&1 && git config core.hooksPath .githooks || true" }, "dependencies": { "@apollo/client": "^3.11.0",