diff --git a/App.tsx b/App.tsx index 0329d0c..38cebdd 100644 --- a/App.tsx +++ b/App.tsx @@ -1,20 +1,12 @@ import { StatusBar } from 'expo-status-bar'; -import { StyleSheet, Text, View } from 'react-native'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { AppNavigator } from './src/navigation/AppNavigator'; export default function App() { return ( - - Open up App.tsx to start working on your app! - - + + + + ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, -}); diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5ff840 --- /dev/null +++ b/README.md @@ -0,0 +1,206 @@ +# bincio-rec + +GPS activity recorder for cycling, running, hiking, and walking. +Records GPS tracks + BLE sensor data (HR, power, cadence) and saves them as GPX files. +Part of the [bincio ecosystem](#ecosystem). + +--- + +## Prerequisites + +### All platforms + +| Tool | Version | Notes | +|---|---|---| +| Node.js | 22+ | [nodejs.org](https://nodejs.org) | +| npm | 10+ | Comes with Node | +| Expo CLI | latest | `npm install -g expo-cli` (optional, `npx expo` works too) | + +### Android + +| Tool | Notes | +|---|---| +| Android Studio | [developer.android.com/studio](https://developer.android.com/studio) | +| Android SDK | Install via Android Studio SDK Manager | +| JDK 17 | Bundled with Android Studio, or install separately | +| `ANDROID_HOME` env var | Set to your SDK path, e.g. `~/Library/Android/sdk` | + +Minimum supported Android version: **API 26 (Android 8.0)**. + +### iOS (macOS only) + +| Tool | Notes | +|---|---| +| Xcode 16+ | Install from the Mac App Store | +| Xcode Command Line Tools | `xcode-select --install` | +| CocoaPods | `sudo gem install cocoapods` | +| Apple Developer account | Free account works for device sideloading | + +Minimum supported iOS version: **16.4**. + +--- + +## Setup + +```bash +# 1. Install JS dependencies +npm install + +# 2. Prebuild was already run — android/ and ios/ directories exist. +# Re-run only when you add/remove native modules: +npx expo prebuild --clean +``` + +--- + +## Running in development + +### Android + +```bash +# Start Metro bundler + launch on a connected device or emulator +npm run android + +# Or target a specific device +npx expo run:android --device +``` + +**Android emulator setup:** In Android Studio → Device Manager, create an AVD with API 34+, x86_64 image. Start it before running the command above. + +**Physical device:** Enable Developer Options → USB Debugging, then connect via USB. + +> **Battery optimization:** On first launch, the app will prompt you to whitelist it in battery settings. This is required for background GPS recording to survive on Xiaomi / Samsung / Huawei devices. + +### iOS + +```bash +# Install CocoaPods dependencies (first time and after native changes) +cd ios && pod install && cd .. + +# Start Metro bundler + launch on a connected device or simulator +npm run ios + +# Or target a specific simulator +npx expo run:ios --simulator "iPhone 16" + +# Or target a physical device (requires Apple Developer account) +npx expo run:ios --device +``` + +**Location permission:** iOS will prompt for location access. Choose **Always Allow** — background GPS recording requires it. + +--- + +## Building release APK / IPA + +### Android — local release APK + +```bash +cd android +./gradlew assembleRelease +# Output: android/app/build/outputs/apk/release/app-release.apk +``` + +To build a release AAB (required for Play Store): + +```bash +./gradlew bundleRelease +# Output: android/app/build/outputs/bundle/release/app-release.aab +``` + +**Signing:** Create a keystore and configure `android/app/build.gradle` with your signing config before a production release. See [React Native signing docs](https://reactnative.dev/docs/signed-apk-android). + +### iOS — local release archive + +Open Xcode: + +```bash +open ios/binciorec.xcworkspace +``` + +1. Select your device or **Any iOS Device (arm64)** as the target. +2. Set your Team in **Signing & Capabilities**. +3. **Product → Archive**. +4. In the Organizer window, click **Distribute App**. + +--- + +## Building with EAS Build (recommended for CI / TestFlight) + +EAS Build runs in Expo's cloud so you don't need Android Studio or Xcode locally. + +```bash +# Install EAS CLI +npm install -g eas-cli + +# Log in to your Expo account +eas login + +# Configure the project (first time only — creates eas.json) +eas build:configure + +# Build Android APK for local testing +eas build --platform android --profile preview + +# Build Android AAB for Play Store +eas build --platform android --profile production + +# Build iOS IPA for TestFlight / App Store +eas build --platform ios --profile production +``` + +Builds appear at [expo.dev](https://expo.dev) and a download link is printed when done. + +--- + +## Project structure + +``` +bincio-rec/ +├── App.tsx # Entry point +├── app.json # Expo config (permissions, plugins, bundle IDs) +├── android/ # Generated Android project (do not edit manually) +├── ios/ # Generated iOS project (do not edit manually) +└── src/ + ├── types/index.ts # Shared TypeScript types + ├── store/recording.ts # Zustand recording state + haversine stats + ├── services/ + │ ├── gps.ts # Background GPS via expo-location + expo-task-manager + │ ├── ble.ts # BLE scan / connect / HR + power subscriptions + │ ├── gpx.ts # GPX file builder (Garmin trackpoint extensions) + │ ├── upload.ts # Upload GPX to bincio-activity /api/upload/raw + │ └── db.ts # SQLite CRUD for saved recordings list + ├── screens/ + │ ├── RecordingScreen.tsx + │ ├── PostRecordingScreen.tsx + │ ├── SensorPairingScreen.tsx + │ ├── SavedRecordingsScreen.tsx + │ └── SettingsScreen.tsx + └── navigation/ + └── AppNavigator.tsx # Root stack + bottom tabs +``` + +--- + +## Ecosystem + +``` +bincio-rec → writes GPX files to device storage +bincio-autarchive → imports GPX, stores locally, syncs to server +bincio-activity → server: parses GPX, stores activities, serves API +bincio-auth → auth service: issues JWTs for all bincio apps +``` + +Configure your bincio instance URL and API token in **Settings** to enable direct upload from the app. + +--- + +## Adding native modules + +Any new Expo or React Native library with native code requires a prebuild + reinstall cycle: + +```bash +npx expo install +npx expo prebuild --clean # regenerates android/ and ios/ +cd ios && pod install && cd .. +``` diff --git a/app.json b/app.json index f935524..0b23c10 100644 --- a/app.json +++ b/app.json @@ -1,25 +1,60 @@ { "expo": { - "name": "bincio_rec_scaffold", - "slug": "bincio_rec_scaffold", + "name": "bincio-rec", + "slug": "bincio-rec", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", - "userInterfaceStyle": "light", + "userInterfaceStyle": "dark", "ios": { - "supportsTablet": true + "supportsTablet": false, + "bundleIdentifier": "com.bincio.rec", + "infoPlist": { + "NSLocationWhenInUseUsageDescription": "bincio-rec uses your location to record your activity track.", + "NSLocationAlwaysAndWhenInUseUsageDescription": "bincio-rec uses your location in the background to record your activity track.", + "UIBackgroundModes": ["location"] + } }, "android": { + "package": "com.bincio.rec", "adaptiveIcon": { - "backgroundColor": "#E6F4FE", + "backgroundColor": "#111111", "foregroundImage": "./assets/android-icon-foreground.png", "backgroundImage": "./assets/android-icon-background.png", "monochromeImage": "./assets/android-icon-monochrome.png" }, - "predictiveBackGestureEnabled": false + "predictiveBackGestureEnabled": false, + "permissions": [ + "ACCESS_FINE_LOCATION", + "ACCESS_BACKGROUND_LOCATION", + "FOREGROUND_SERVICE", + "FOREGROUND_SERVICE_LOCATION", + "BLUETOOTH", + "BLUETOOTH_ADMIN", + "BLUETOOTH_SCAN", + "BLUETOOTH_CONNECT" + ] }, "web": { "favicon": "./assets/favicon.png" - } + }, + "plugins": [ + "expo-sqlite", + [ + "expo-location", + { + "locationAlwaysAndWhenInUsePermission": "bincio-rec records your GPS track during activities.", + "isAndroidBackgroundLocationEnabled": true, + "isAndroidForegroundServiceEnabled": true + } + ], + [ + "expo-notifications", + { + "icon": "./assets/icon.png", + "color": "#3b82f6" + } + ] + ] } } diff --git a/package-lock.json b/package-lock.json index e314f9f..c6d08c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,29 @@ "name": "bincio_rec_scaffold", "version": "1.0.0", "dependencies": { + "@maplibre/maplibre-react-native": "^11.3.2", + "@react-native-async-storage/async-storage": "^3.1.1", + "@react-navigation/bottom-tabs": "^7.16.2", + "@react-navigation/native": "^7.2.5", + "@react-navigation/native-stack": "^7.16.0", "expo": "~56.0.8", + "expo-av": "^16.0.8", + "expo-crypto": "^56.0.4", + "expo-file-system": "~56.0.7", + "expo-keep-awake": "~56.0.3", + "expo-location": "~56.0.15", + "expo-notifications": "~56.0.15", + "expo-sharing": "^56.0.15", + "expo-sqlite": "~56.0.4", "expo-status-bar": "~56.0.4", + "expo-task-manager": "~56.0.16", "react": "19.2.3", - "react-native": "0.85.3" + "react-native": "0.85.3", + "react-native-ble-plx": "^3.5.1", + "react-native-gesture-handler": "~2.31.1", + "react-native-safe-area-context": "~5.7.0", + "react-native-screens": "4.25.2", + "zustand": "^5.0.14" }, "devDependencies": { "@types/react": "~19.2.2", @@ -1063,6 +1082,18 @@ "node": ">=6.9.0" } }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "license": "MIT", + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@expo/code-signing-certificates": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", @@ -1496,6 +1527,70 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "24.8.5", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.8.5.tgz", + "integrity": "sha512-EzEJmMt6thioRH7GI9LWS7ahXTcAhAPGWCe6oTP2Ps4YnsXOOAfeqx854lZaiDnwURfHmcCKV1mr6oo0i23x6w==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/maplibre-react-native": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-react-native/-/maplibre-react-native-11.3.2.tgz", + "integrity": "sha512-wiATUec6sIMqFbesaR9UeQLvVP8/PDg0DbTdKt2DWyNgoqVKnZesp6y64P1s68GbgwMYcvNS5QorzIfFUVIybQ==", + "license": "MIT", + "dependencies": { + "@maplibre/maplibre-gl-style-spec": "24.8.5", + "@turf/distance": "^7.3.5", + "@turf/helpers": "^7.3.5", + "@turf/length": "^7.3.5", + "@turf/nearest-point-on-line": "^7.3.5" + }, + "peerDependencies": { + "@expo/config-plugins": ">=54.0.0", + "@types/geojson": "^7946.0.0", + "@types/react": ">=19.1.0", + "react": ">=19.1.0", + "react-native": ">=0.80.0" + }, + "peerDependenciesMeta": { + "@expo/config-plugins": { + "optional": true + }, + "@types/geojson": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", @@ -1574,6 +1669,19 @@ "win32" ] }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-3.1.1.tgz", + "integrity": "sha512-z+PnLz1n6ECKhgoHZHkfc+dijXZEyZnNFSajbtE0NEbsJhmX8x9GlOeiMQIKX2E4DUqPSgfIh4FYBv1M49KgPQ==", + "license": "MIT", + "dependencies": { + "idb": "8.0.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.85.3", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.85.3.tgz", @@ -1755,12 +1863,224 @@ } } }, + "node_modules/@react-navigation/bottom-tabs": { + "version": "7.16.2", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.16.2.tgz", + "integrity": "sha512-Lbp++BGMc7SQXnyKuO/JrQJIhFH0zyB5v4kIEbnzDJLJfgubd5hoSe+QfCqy4YHfLA4phC4Xf/6Q2Ic8x7datQ==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.9.19", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0" + }, + "peerDependencies": { + "@react-navigation/native": "^7.2.5", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/core": { + "version": "7.17.5", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.17.5.tgz", + "integrity": "sha512-6fDCwDTWC7DJn0SDb9DJGRlipaygHIc+2elpZBJI6Crl/2Pu+Z1d6W4jMJ2gZO6iHKf+Pe5sUiQ/uwepGprZtg==", + "license": "MIT", + "dependencies": { + "@react-navigation/routers": "^7.5.5", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "query-string": "^7.1.3", + "react-is": "^19.1.0", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "react": ">= 18.2.0" + } + }, + "node_modules/@react-navigation/core/node_modules/react-is": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "license": "MIT" + }, + "node_modules/@react-navigation/elements": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.19.tgz", + "integrity": "sha512-gBUvCZuUkOGw1KpLQEZIkByUz8RYPwXeoA6mZFJy9K1mxd8GdqHDMFCIoB0lfPz9rgrHj99RvtdlGZ/ZzkZv2A==", + "license": "MIT", + "dependencies": { + "color": "^4.2.3", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@react-native-masked-view/masked-view": ">= 0.2.0", + "@react-navigation/native": "^7.2.5", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0" + }, + "peerDependenciesMeta": { + "@react-native-masked-view/masked-view": { + "optional": true + } + } + }, + "node_modules/@react-navigation/native": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.5.tgz", + "integrity": "sha512-01AAUQiiHQAfTabq+ZyU1/ZWq+AbB/J3v0CB0UTJSON6M6cuadWNsbChzrZUdqQvHrXvg96U5i2PQLJzK3+zpg==", + "license": "MIT", + "dependencies": { + "@react-navigation/core": "^7.17.5", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*" + } + }, + "node_modules/@react-navigation/native-stack": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.16.0.tgz", + "integrity": "sha512-wM21rHYR2XifjDnKLrr3HeHUeGsWQZJRwPqEzy1Vp/a9k3ieiwTGpmpDItD/jtERH9qkYESwDPO6oEtrVBEpQg==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.9.19", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0", + "warn-once": "^0.1.1" + }, + "peerDependencies": { + "@react-navigation/native": "^7.2.5", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/routers": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.5.tgz", + "integrity": "sha512-9/hhMte12Kgu+pMnLfA4EWJ0OQmIEAMVMX06FPH2yGkEQSQ3JhhCN/GkcRikzQhtEi97VYYQA15umptBUShcOQ==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", "license": "MIT" }, + "node_modules/@turf/distance": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/distance/-/distance-7.3.5.tgz", + "integrity": "sha512-uQAC63zg/l91KUxzfhqio7Ii3+UXTrPOVJScIdRj6EO6+9XHI4kC+AdyIS4cPAv14sZfJLIBxzMnzcGrss+kEA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.3.5.tgz", + "integrity": "sha512-E/NMGV5MwbjjP7AJXBtsanC3yY8N2MQ87IGdIgkB2ji5AtBpwnH4L3gEqpYN4RlCJJWbLbzO91BbKv2waUd0eg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.3.5.tgz", + "integrity": "sha512-ZVIvsBvjr8lO7WxC5zYNjRsjSDvyGvWkJMjuWaJjTU8x+1tmfNnw3gDX/TI2Sit83gcRYLYkNo23lB/udqx/Hg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/length": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/length/-/length-7.3.5.tgz", + "integrity": "sha512-Bi+vEP54wt1ly3BRcCOP0nd2kGTYEhGk6haQxTpkrqr3XtmqDh8c3NowSgseN2cegIZRjwCOEC8eSsZ0JemJdA==", + "license": "MIT", + "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" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/meta": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-7.3.5.tgz", + "integrity": "sha512-r+ohqxoyqeigFB0oFrQx/YEHIkOKqcKpCjvZkvZs7Tkv+IFco5MezAd2zd4rzK+0DfFgDP3KpJc7HqrYjvEjhg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-point-on-line": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/nearest-point-on-line/-/nearest-point-on-line-7.3.5.tgz", + "integrity": "sha512-MZn6OkEFZpjS6BNUANfqiHMIbQSivu7TNji3a+OAIrnPJ71vp8cbz0N2aVEa5M7I8ipvxoxAPIV3eqg3h280Vg==", + "license": "MIT", + "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" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1798,12 +2118,20 @@ "version": "19.2.16", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.16.tgz", "integrity": "sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" } }, + "node_modules/@types/react-test-renderer": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz", + "integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -1955,6 +2283,12 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, + "node_modules/await-lock": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", + "license": "MIT" + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.17", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", @@ -2051,6 +2385,12 @@ "@babel/plugin-syntax-flow": "^7.12.1" } }, + "node_modules/badgin": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz", + "integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -2335,6 +2675,19 @@ "node": ">=0.8" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2353,6 +2706,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -2483,7 +2846,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -2503,6 +2865,15 @@ } } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -2710,6 +3081,86 @@ } } }, + "node_modules/expo-application": { + "version": "56.0.3", + "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-56.0.3.tgz", + "integrity": "sha512-DdGGPlMuM6cSTeKhbvh6OeLr2O/+EI5BHKYrD+Do8sJPYgLwzGrgESELfyjJCpEhFzT+TgKIdmLmWXhNUQnHiw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-av": { + "version": "16.0.8", + "resolved": "https://registry.npmjs.org/expo-av/-/expo-av-16.0.8.tgz", + "integrity": "sha512-cmVPftGR/ca7XBgs7R6ky36lF3OC0/MM/lpgX/yXqfv0jASTsh7AYX9JxHCwFmF+Z6JEB1vne9FDx4GiLcGreQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native-web": { + "optional": true + } + } + }, + "node_modules/expo-constants": { + "version": "56.0.16", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-56.0.16.tgz", + "integrity": "sha512-6tsiN+gmTUPp/atyA+uY9Tg8VOdXdmb4s/3TVGolfn6A/oCAraw1pcPZX5XllyD+xUguxB6eBSFAT8494hZVMA==", + "license": "MIT", + "dependencies": { + "@expo/env": "~2.3.0" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo-crypto": { + "version": "56.0.4", + "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-56.0.4.tgz", + "integrity": "sha512-fRNEhoXRXgAWBpe3/hq5X+KXTit3OZqdiAGts1YvNEUHQb+H5591mpPac0Yw+sZg9pXcrjRnzo5AxvZaENpc7g==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-file-system": { + "version": "56.0.7", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-56.0.7.tgz", + "integrity": "sha512-dcKzo8ShPloM7jgfnMcJStgQebhP8owVjCkNI/aX6NMFV1CYB8bxKGMdnzJ3mXk5nfaiW+F/lSKr2UIJ02WAUA==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo-keep-awake": { + "version": "56.0.3", + "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-56.0.3.tgz", + "integrity": "sha512-CLMJXtEiMKknD3Rpm8CRwE6ZJUzu2yCEmRk1sgfHAJ1zIbuEWY3dpPDubtsnuzWm+2k6Sru+yaFbYsvPWmTiBA==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo-location": { + "version": "56.0.15", + "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-56.0.15.tgz", + "integrity": "sha512-CM5+1untDxsuN0NIgsBS9cRel5xh8UXstQS6KtQw/run5PiArqCl51cnTuG+aqjYgE+9gweSG70PI6A1Ax1XTA==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.10.1" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "56.0.14", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-56.0.14.tgz", @@ -2755,6 +3206,24 @@ "react-native": "*" } }, + "node_modules/expo-notifications": { + "version": "56.0.15", + "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-56.0.15.tgz", + "integrity": "sha512-F+OasAePiVnHaPNKI9JAYV8fg8bdBwo7Mh9R3ydBp8S21fRQyxKOSgJvj8fX/HoPFFIC6V2B+y1LJbG5Ovh/Fg==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.10.1", + "abort-controller": "^3.0.0", + "badgin": "^1.1.5", + "expo-application": "~56.0.3", + "expo-constants": "~56.0.16" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-server": { "version": "56.0.4", "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-56.0.4.tgz", @@ -2764,6 +3233,36 @@ "node": ">=20.16.0" } }, + "node_modules/expo-sharing": { + "version": "56.0.15", + "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-56.0.15.tgz", + "integrity": "sha512-6Hy1+Mjy4UYXkFiDK3Ea934NUmA71i8dmZkDe+rrUHRzZAv4FR+q/VyiT7LzNFEqpT4wn4wcI66lc2QY526RsA==", + "license": "MIT", + "dependencies": { + "@expo/config-plugins": "^56.0.8", + "@expo/config-types": "^56.0.5", + "@expo/plist": "^0.7.0" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-sqlite": { + "version": "56.0.4", + "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-56.0.4.tgz", + "integrity": "sha512-Ak8TUyrvK7C/J4BHBfcb8BacFrH8I+b+zqeSTKg5B02Z13lxljvuqI8UvKbRNa5BKprlxrqabZickGwacRkM9g==", + "license": "MIT", + "dependencies": { + "await-lock": "^2.2.2" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-status-bar": { "version": "56.0.4", "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-56.0.4.tgz", @@ -2775,6 +3274,19 @@ "react-native": "*" } }, + "node_modules/expo-task-manager": { + "version": "56.0.16", + "resolved": "https://registry.npmjs.org/expo-task-manager/-/expo-task-manager-56.0.16.tgz", + "integrity": "sha512-wh5DOzUkQfpXs2fmm9QYlPoNiJRgnCI926m2hoVDFYD8yENnDYYXQEON8uYgnepYmActr/KAMBxmw6BOmTky/Q==", + "license": "MIT", + "dependencies": { + "unimodules-app-loader": "~56.0.0" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo/node_modules/@expo/cli": { "version": "56.1.13", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.13.tgz", @@ -3068,29 +3580,6 @@ "react-native": "*" } }, - "node_modules/expo/node_modules/expo-constants": { - "version": "56.0.16", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-56.0.16.tgz", - "integrity": "sha512-6tsiN+gmTUPp/atyA+uY9Tg8VOdXdmb4s/3TVGolfn6A/oCAraw1pcPZX5XllyD+xUguxB6eBSFAT8494hZVMA==", - "license": "MIT", - "dependencies": { - "@expo/env": "~2.3.0" - }, - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, - "node_modules/expo/node_modules/expo-file-system": { - "version": "56.0.7", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-56.0.7.tgz", - "integrity": "sha512-dcKzo8ShPloM7jgfnMcJStgQebhP8owVjCkNI/aX6NMFV1CYB8bxKGMdnzJ3mXk5nfaiW+F/lSKr2UIJ02WAUA==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, "node_modules/expo/node_modules/expo-font": { "version": "56.0.5", "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-56.0.5.tgz", @@ -3105,16 +3594,6 @@ "react-native": "*" } }, - "node_modules/expo/node_modules/expo-keep-awake": { - "version": "56.0.3", - "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-56.0.3.tgz", - "integrity": "sha512-CLMJXtEiMKknD3Rpm8CRwE6ZJUzu2yCEmRk1sgfHAJ1zIbuEWY3dpPDubtsnuzWm+2k6Sru+yaFbYsvPWmTiBA==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react": "*" - } - }, "node_modules/expo/node_modules/hermes-estree": { "version": "0.33.3", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", @@ -3199,6 +3678,12 @@ "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "license": "Apache-2.0" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fb-dotslash": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz", @@ -3238,6 +3723,15 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -3393,6 +3887,21 @@ "hermes-estree": "0.35.0" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -3453,6 +3962,12 @@ "node": ">= 14" } }, + "node_modules/idb": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3492,6 +4007,12 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -3698,6 +4219,12 @@ "node": ">=6" } }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4508,6 +5035,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -5034,6 +5570,24 @@ "node": ">= 6" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -5043,6 +5597,12 @@ "inherits": "~2.0.3" } }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -5071,6 +5631,18 @@ "ws": "^7" } }, + "node_modules/react-freeze": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", + "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=17.0.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5136,6 +5708,59 @@ } } }, + "node_modules/react-native-ble-plx": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/react-native-ble-plx/-/react-native-ble-plx-3.5.1.tgz", + "integrity": "sha512-SxksmrUt9jG6DOarrrdkb5c/HBLSfZOKauo/9VQSSi3WJA4bmF78GkrtXrgSoGNk0m1ksacFTjB5DuL39xZq/g==", + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-gesture-handler": { + "version": "2.31.2", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.31.2.tgz", + "integrity": "sha512-rw5q74i2AfS7YGYdbxQDhOU7xqgY6WRM1132/CCm3erqjblhECZDZFHIm0tteHoC9ih24wogVBVVzcTBQtZ+5A==", + "license": "MIT", + "dependencies": { + "@egjs/hammerjs": "^2.0.17", + "@types/react-test-renderer": "^19.1.0", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-safe-area-context": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz", + "integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.25.2.tgz", + "integrity": "sha512-1Nj1fusFd+rIMKU/qC9yGKVG+3ofh11d3OdBQKL1iVvQfKvcB8vhvTGQf2TkfxW3bamxN+hCZIXmNuU0mRkyDg==", + "license": "MIT", + "dependencies": { + "react-freeze": "^1.0.0", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": ">=0.82.0" + } + }, "node_modules/react-native/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -5426,6 +6051,15 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sf-symbols-typescript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz", + "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5476,6 +6110,15 @@ "plist": "^3.0.5" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -5528,6 +6171,15 @@ "node": ">=0.10.0" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -5564,6 +6216,15 @@ "node": ">= 0.10.0" } }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5724,6 +6385,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -5757,6 +6424,12 @@ "integrity": "sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==", "license": "MIT" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-fest": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", @@ -5826,6 +6499,12 @@ "node": ">=4" } }, + "node_modules/unimodules-app-loader": { + "version": "56.0.1", + "resolved": "https://registry.npmjs.org/unimodules-app-loader/-/unimodules-app-loader-56.0.1.tgz", + "integrity": "sha512-Z801jeBOQMUF/ExklxT1BqhEV/oF2/Bii7PFYAj/8Sauxl7oKvZbf70peRzzAU0mG7UQ3yU/UO/EpD1JyJ2WcA==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5865,6 +6544,24 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-latest-callback": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz", + "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5917,6 +6614,12 @@ "makeerror": "1.0.12" } }, + "node_modules/warn-once": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz", + "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", + "license": "MIT" + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -6100,6 +6803,35 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 1c86cc2..774fedb 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,29 @@ "version": "1.0.0", "main": "index.ts", "dependencies": { + "@maplibre/maplibre-react-native": "^11.3.2", + "@react-native-async-storage/async-storage": "^3.1.1", + "@react-navigation/bottom-tabs": "^7.16.2", + "@react-navigation/native": "^7.2.5", + "@react-navigation/native-stack": "^7.16.0", "expo": "~56.0.8", + "expo-av": "^16.0.8", + "expo-crypto": "^56.0.4", + "expo-file-system": "~56.0.7", + "expo-keep-awake": "~56.0.3", + "expo-location": "~56.0.15", + "expo-notifications": "~56.0.15", + "expo-sharing": "^56.0.15", + "expo-sqlite": "~56.0.4", "expo-status-bar": "~56.0.4", + "expo-task-manager": "~56.0.16", "react": "19.2.3", - "react-native": "0.85.3" + "react-native": "0.85.3", + "react-native-ble-plx": "^3.5.1", + "react-native-gesture-handler": "~2.31.1", + "react-native-safe-area-context": "~5.7.0", + "react-native-screens": "4.25.2", + "zustand": "^5.0.14" }, "devDependencies": { "@types/react": "~19.2.2", @@ -14,8 +33,8 @@ }, "scripts": { "start": "expo start", - "android": "expo start --android", - "ios": "expo start --ios", + "android": "expo run:android", + "ios": "expo run:ios", "web": "expo start --web" }, "private": true diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx new file mode 100644 index 0000000..d0893e4 --- /dev/null +++ b/src/navigation/AppNavigator.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { NavigationContainer } from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { Text } from 'react-native'; + +import { RecordingScreen } from '../screens/RecordingScreen'; +import { PostRecordingScreen } from '../screens/PostRecordingScreen'; +import { SensorPairingScreen } from '../screens/SensorPairingScreen'; +import { SavedRecordingsScreen } from '../screens/SavedRecordingsScreen'; +import { SettingsScreen } from '../screens/SettingsScreen'; +import { RootStackParamList, TabParamList } from '../types'; + +const Stack = createNativeStackNavigator(); +const Tab = createBottomTabNavigator(); + +function Tabs() { + return ( + + }} + /> + 📋 }} + /> + ⚙️ }} + /> + + ); +} + +export function AppNavigator() { + return ( + + + + + + + + ); +} diff --git a/src/screens/PostRecordingScreen.tsx b/src/screens/PostRecordingScreen.tsx new file mode 100644 index 0000000..11255bc --- /dev/null +++ b/src/screens/PostRecordingScreen.tsx @@ -0,0 +1,106 @@ +import React, { useState } from 'react'; +import { View, Text, TextInput, StyleSheet, TouchableOpacity, Alert, ScrollView } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { useRecordingStore } from '../store/recording'; +import { saveGpx } from '../services/gpx'; +import { insertRecording } from '../services/db'; +import { SavedRecording } from '../types'; +import { randomUUID } from 'expo-crypto'; + +export function PostRecordingScreen() { + const nav = useNavigation(); + const { trackPoints, reset, getStats } = useRecordingStore(); + const stats = getStats(); + const [title, setTitle] = useState(''); + const [saving, setSaving] = useState(false); + + const formatDuration = (secs: number) => { + const h = Math.floor(secs / 3600).toString().padStart(2, '0'); + const m = Math.floor((secs % 3600) / 60).toString().padStart(2, '0'); + const s = (secs % 60).toString().padStart(2, '0'); + return `${h}:${m}:${s}`; + }; + + async function handleSave() { + if (!title.trim()) { Alert.alert('Title required', 'Enter a title for this recording.'); return; } + setSaving(true); + try { + const filePath = saveGpx(trackPoints, title.trim()); + const rec: SavedRecording = { + id: randomUUID(), + title: title.trim(), + date: new Date().toISOString(), + durationSeconds: stats.elapsedSeconds, + distanceMeters: stats.distanceMeters, + filePath, + }; + await insertRecording(rec); + reset(); + nav.goBack(); + } catch (e: any) { + Alert.alert('Save failed', e?.message ?? 'Unknown error'); + } finally { + setSaving(false); + } + } + + function handleDiscard() { + Alert.alert('Discard recording?', 'This cannot be undone.', [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Discard', style: 'destructive', onPress: () => { reset(); nav.goBack(); } }, + ]); + } + + return ( + + Recording complete + + + + + + + + + + + + {saving ? 'Saving…' : 'Save'} + + + Discard + + + ); +} + +function Stat({ label, value }: { label: string; value: string }) { + return ( + + {label} + {value} + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#111' }, + content: { padding: 24, gap: 16 }, + heading: { color: '#fff', fontSize: 24, fontWeight: '700', marginBottom: 8 }, + statsRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, marginBottom: 8 }, + stat: { flex: 1, minWidth: '40%', backgroundColor: '#1e1e1e', borderRadius: 12, padding: 16 }, + statLabel: { color: '#888', fontSize: 12, textTransform: 'uppercase' }, + statValue: { color: '#fff', fontSize: 22, fontWeight: '600', marginTop: 4 }, + input: { backgroundColor: '#1e1e1e', color: '#fff', borderRadius: 12, padding: 16, fontSize: 18 }, + btn: { borderRadius: 12, padding: 16, alignItems: 'center' }, + btnSave: { backgroundColor: '#22c55e' }, + btnDiscard: { backgroundColor: '#1e1e1e' }, + btnText: { fontSize: 16, fontWeight: '700', color: '#fff' }, +}); diff --git a/src/screens/RecordingScreen.tsx b/src/screens/RecordingScreen.tsx new file mode 100644 index 0000000..bb62e6e --- /dev/null +++ b/src/screens/RecordingScreen.tsx @@ -0,0 +1,120 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { useKeepAwake } from 'expo-keep-awake'; +import { useRecordingStore } from '../store/recording'; +import { startGpsRecording, stopGpsRecording, requestLocationPermissions } from '../services/gps'; +import { RootStackParamList } from '../types'; + +type Nav = NativeStackNavigationProp; + +export function RecordingScreen() { + const nav = useNavigation