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