Compare commits
27 commits
Author | SHA1 | Date | |
---|---|---|---|
ac84ae707b | |||
a2f476f8ee | |||
cbcee9f35f | |||
f3cca8182c | |||
ce11db9863 | |||
30aeb2a0e4 | |||
c30c0b0482 | |||
ca3a2c8fcc | |||
991b65d990 | |||
3e70ff23c9 | |||
eac1c1d5bd | |||
4810cade9f | |||
52505eb457 | |||
8867a95fb8 | |||
25241589ba | |||
2e35c41e0f | |||
2ba8a37ed0 | |||
8b5d0621fe | |||
bf344a1f27 | |||
21495617bb | |||
42bf5c5995 | |||
af477ce835 | |||
2f3db5adc0 | |||
5398f94f49 | |||
40b27bff29 | |||
91c374756b | |||
882e6105f4 |
53 changed files with 2172 additions and 74 deletions
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
liberapay: alerte-secours
|
||||
github: alerte-secours
|
||||
buy_me_a_coffee: alertesecours
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -95,3 +95,5 @@ android/app/google-services.json
|
|||
!ios/AlerteSecours/GoogleService-Info.example.plist
|
||||
!ios/AlerteSecours/Supporting/Expo.example.plist
|
||||
!android/app/google-services.example.json
|
||||
|
||||
screenshot-*.png
|
41
CHANGELOG.md
41
CHANGELOG.md
|
@ -2,6 +2,47 @@
|
|||
|
||||
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
||||
|
||||
## [1.10.1](https://github.com/alerte-secours/as-app/compare/v1.10.0...v1.10.1) (2025-06-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove deprecated ([a2f476f](https://github.com/alerte-secours/as-app/commit/a2f476f8ee9670461dc7babc7f534c43ba4708e9))
|
||||
|
||||
## [1.10.0](https://github.com/alerte-secours/as-app/compare/v1.9.2...v1.10.0) (2025-06-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* contribute ([3e70ff2](https://github.com/alerte-secours/as-app/commit/3e70ff23c9361a5d331e15160adbb44a8bf773ac))
|
||||
* **follow-location:** init ([ca3a2c8](https://github.com/alerte-secours/as-app/commit/ca3a2c8fcce2aaa613817bde047f143915ef8fe0))
|
||||
* **follow-location:** map bubble ([f3cca81](https://github.com/alerte-secours/as-app/commit/f3cca8182ca52a89f7f6d0785821185db277bbf0))
|
||||
* **follow-location:** map wip ([ce11db9](https://github.com/alerte-secours/as-app/commit/ce11db9863a7aaa14b28e104645d8ce0b23a235b))
|
||||
* **follow-location:** overview initial position ([30aeb2a](https://github.com/alerte-secours/as-app/commit/30aeb2a0e41196719977900e08b12d8ae813a69a))
|
||||
* **follow-location:** wip ([c30c0b0](https://github.com/alerte-secours/as-app/commit/c30c0b0482d946b6b4d6a2421416a59f9d57bfeb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* call emergency by default on yellow ([991b65d](https://github.com/alerte-secours/as-app/commit/991b65d990b909fb02f9941fedf84637d0a8b71e))
|
||||
|
||||
## [1.9.2](https://github.com/alerte-secours/as-app/compare/v1.9.1...v1.9.2) (2025-05-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ios-reported-bug:** app only displayed the splash screen after enabling access to location ([2e35c41](https://github.com/alerte-secours/as-app/commit/2e35c41e0f3e968df6dc07e7656cf50509557a7c))
|
||||
|
||||
## [1.9.1](https://github.com/alerte-secours/as-app/compare/v1.9.0...v1.9.1) (2025-05-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* default call emergency on yellow ([8b5d062](https://github.com/alerte-secours/as-app/commit/8b5d0621fed5fafaa366fd16b9280509e3923ca4))
|
||||
* **geoloc:** server switch + proper auth throttling ([af477ce](https://github.com/alerte-secours/as-app/commit/af477ce8352599124e20894bd1fbdb297317e4cb))
|
||||
* staging persistence + disconnect and geo auth reload loop ([40b27bf](https://github.com/alerte-secours/as-app/commit/40b27bff297cdf70f45c66009275110c91a60269))
|
||||
* wording ([42bf5c5](https://github.com/alerte-secours/as-app/commit/42bf5c599520cdd55943fde5f5fc052c72b7991a))
|
||||
|
||||
## 1.9.0 (2025-04-24)
|
||||
|
||||
|
||||
|
|
226
DEVELOPER.md
Normal file
226
DEVELOPER.md
Normal file
|
@ -0,0 +1,226 @@
|
|||
# Alerte Secours Mobile App - Developer Documentation
|
||||
|
||||
This document contains technical information for developers working on the Alerte Secours mobile application.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Project Overview](#project-overview)
|
||||
- [Technical Stack](#technical-stack)
|
||||
- [Development Quick Start](#development-quick-start)
|
||||
- [Installation](#installation)
|
||||
- [Android](#android)
|
||||
- [iOS](#ios)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Project Overview
|
||||
|
||||
Alerte Secours is a mobile application built with React Native that handles alerts and emergency-related functionality. The app supports both iOS and Android platforms and includes features such as:
|
||||
|
||||
- Alert creation and management with real-time updates
|
||||
- Location-based features with mapping integration
|
||||
- Chat/Messaging system with alert-specific chat rooms
|
||||
- Authentication via SMS verification
|
||||
- Deep linking for alert sharing
|
||||
- Push notifications
|
||||
|
||||
## Technical Stack
|
||||
|
||||
- React Native
|
||||
- Expo framework
|
||||
- GraphQL with Hasura
|
||||
- Apollo Client for frontend
|
||||
- Federated remote schemas
|
||||
- Real-time subscriptions support
|
||||
- Firebase Cloud Messaging (FCM) for push notifications only
|
||||
- Sentry for error tracking
|
||||
- MapLibre for mapping functionality
|
||||
- Zustand for state management
|
||||
- i18next for internationalization
|
||||
- React Navigation for navigation
|
||||
- Expo Updates for OTA updates
|
||||
- Background Geolocation for location tracking
|
||||
- Lottie for animations
|
||||
- React Hook Form for form handling
|
||||
- Axios for HTTP requests
|
||||
- Yarn Berry as package manager
|
||||
- ESLint and Prettier for code quality
|
||||
- Fastlane for deployment automation
|
||||
|
||||
## Development Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (version specified in `.node-version`)
|
||||
- Yarn package manager
|
||||
- Android Studio (for Android development)
|
||||
- Xcode (for iOS development)
|
||||
- Physical device or emulator/simulator
|
||||
|
||||
### Environment Setup
|
||||
|
||||
1. Clone the repository
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
3. Copy the staging environment file:
|
||||
```bash
|
||||
cp .env.staging.example .env.staging
|
||||
```
|
||||
4. Start the development server with staging environment:
|
||||
```bash
|
||||
yarn start:staging
|
||||
```
|
||||
|
||||
### Running on Devices
|
||||
|
||||
#### Android
|
||||
```bash
|
||||
yarn android:staging
|
||||
```
|
||||
|
||||
#### iOS
|
||||
```bash
|
||||
yarn ios:staging
|
||||
```
|
||||
|
||||
### Staging URLs
|
||||
|
||||
The staging environment uses the following URLs:
|
||||
|
||||
- GraphQL API: `https://hasura-staging.alertesecours.fr/v1/graphql`
|
||||
- WebSocket: `wss://hasura-staging.alertesecours.fr/v1/graphql`
|
||||
- Files API: `https://files-staging.alertesecours.fr/api/v1/oas`
|
||||
- Minio: `https://minio-staging.alertesecours.fr`
|
||||
- Geolocation Sync: `https://api-staging.alertesecours.fr/api/v1/oas/geoloc/sync`
|
||||
|
||||
## Installation
|
||||
|
||||
### Android
|
||||
|
||||
#### Using the Yarn Script
|
||||
|
||||
The easiest way to install the app is to use the provided yarn script:
|
||||
|
||||
```bash
|
||||
# Set the device ID (emulator or physical device)
|
||||
export DEVICE=emulator-5554
|
||||
|
||||
# Run the installation script
|
||||
yarn install:android
|
||||
```
|
||||
|
||||
This script (`install-android.sh`) handles the entire installation process, including building APKs with signing, extracting them, and installing on the device.
|
||||
|
||||
#### Manual Installation
|
||||
|
||||
If you need to install the app manually, you can examine the `install-android.sh` script in the project root to see the detailed steps involved.
|
||||
|
||||
### iOS
|
||||
|
||||
#### Authentication Key Setup
|
||||
|
||||
1. Go to https://appstoreconnect.apple.com/access/integrations/api
|
||||
2. Click the "+" button to generate a new API key
|
||||
3. Give it a name (e.g., "AlerteSecours Build Key")
|
||||
4. Download the .p8 file when prompted
|
||||
5. Store the .p8 file in a secure location
|
||||
6. Note down the Key ID and Issuer ID shown on the website
|
||||
7. Set up environment variables:
|
||||
```sh
|
||||
export ASC_API_KEY_ID="YOUR_KEY_ID"
|
||||
export ASC_API_ISSUER_ID="YOUR_ISSUER_ID"
|
||||
export ASC_API_KEY_PATH="/path/to/your/AuthKey.p8"
|
||||
```
|
||||
|
||||
#### Building and Running
|
||||
|
||||
To build and run the iOS app:
|
||||
|
||||
```bash
|
||||
# Run in development mode with staging environment
|
||||
yarn ios:staging
|
||||
|
||||
# Build for production (uses scripts/ios-archive.sh and scripts/ios-export.sh)
|
||||
yarn bundle:ios
|
||||
```
|
||||
|
||||
The `bundle:ios` command uses the scripts in the `scripts` directory:
|
||||
- `ios-archive.sh` - Archives the iOS app
|
||||
- `ios-export.sh` - Exports the archived app
|
||||
- `ios-upload.sh` - Uploads the app to App Store Connect (used by `bundle:ios:upload`)
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `/android` - Android-specific code and configuration
|
||||
- `/ios` - iOS-specific code and configuration
|
||||
- `/src` - Main application source code
|
||||
- `/app` - App initialization and configuration
|
||||
- `/assets` - Static assets (images, fonts, animations)
|
||||
- `/auth` - Authentication-related code
|
||||
- `/biz` - Business logic and constants
|
||||
- `/components` - Reusable UI components
|
||||
- `/containers` - Container components
|
||||
- `/data` - Data management
|
||||
- `/events` - Event handling
|
||||
- `/finders` - Search and finder utilities
|
||||
- `/gql` - GraphQL queries and mutations
|
||||
- `/hoc` - Higher-order components
|
||||
- `/hooks` - Custom React hooks
|
||||
- `/i18n` - Internationalization
|
||||
- `/layout` - Layout components
|
||||
- `/lib` - Library code
|
||||
- `/location` - Location-related functionality
|
||||
- `/misc` - Miscellaneous utilities
|
||||
- `/navigation` - Navigation configuration
|
||||
- `/network` - Network-related code
|
||||
- `/notifications` - Notification handling
|
||||
- `/permissions` - Permission handling
|
||||
- `/scenes` - Scene components
|
||||
- `/screens` - Screen components
|
||||
- `/sentry` - Sentry error tracking configuration
|
||||
- `/stores` - State management stores
|
||||
- `/theme` - Styling and theming
|
||||
- `/updates` - Update handling
|
||||
- `/utils` - Utility functions
|
||||
- `/docs` - Documentation files
|
||||
- `/scripts` - Utility scripts for building, deployment, and development
|
||||
- `/e2e` - End-to-end tests
|
||||
|
||||
## Contributing
|
||||
|
||||
Guidelines for contributing to the project:
|
||||
|
||||
1. Follow the code style and conventions used in the project
|
||||
2. Write tests for new features
|
||||
3. Update documentation as needed
|
||||
4. Use the ESLint and Prettier configurations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Development Environment
|
||||
|
||||
- **Clearing Yarn Cache**: Use `yarn clean` (removes node_modules and reinstalls dependencies)
|
||||
- **Cleaning Gradle**: Run `cd android && ./gradlew clean`
|
||||
- **Clearing Gradle Cache**: Remove gradle caches with `rm -rf ~/.gradle/caches/ android/.gradle/`
|
||||
- **Stopping Gradle Daemons**: Run `cd android && ./gradlew --stop`
|
||||
- **Clearing ADB Cache**: Run `adb shell pm clear com.alertesecours`
|
||||
- **Rebuilding Gradle**: Use `yarn expo run:android`
|
||||
- **Rebuilding Expo React Native**: Use `yarn expo prebuild`
|
||||
- **Clearing Metro Cache**: Use `yarn expo start --dev-client --clear`
|
||||
|
||||
#### Screenshots and Testing
|
||||
|
||||
- For Android screenshots: Use `scripts/screenshot-android.sh`
|
||||
- For iOS screenshots: Use `scripts/screenshot-ios.sh`
|
||||
- For Android emulator: Use `scripts/android-emulator`
|
||||
|
||||
#### Emulator Issues
|
||||
- Clear cache / uninstall the app
|
||||
- Check emulator datetime
|
||||
- Check network connectivity
|
||||
|
||||
For more troubleshooting tips, see the documentation in the `/docs` directory.
|
82
README.md
Normal file
82
README.md
Normal file
|
@ -0,0 +1,82 @@
|
|||
# Alerte Secours - Le Réflexe qui Sauve
|
||||
|
||||
[](https://liberapay.com/alerte-secours)
|
||||
[](https://buymeacoffee.com/alertesecours)
|
||||
[](https://github.com/sponsors/alerte-secours)
|
||||
|
||||
Une application mobile pour la gestion des alertes et des fonctionnalités liées aux urgences, supportant les plateformes iOS et Android.
|
||||
|
||||
**Site Web Officiel :** [alerte-secours.fr](https://alerte-secours.fr)
|
||||
|
||||
## Aperçu du Projet
|
||||
|
||||
Alerte Secours est une application mobile construite avec React Native qui gère les alertes et les fonctionnalités liées aux urgences. L'application supporte les plateformes iOS et Android et inclut des fonctionnalités telles que :
|
||||
|
||||
- Création et gestion d'alertes avec mises à jour en temps réel
|
||||
- Fonctionnalités basées sur la localisation avec intégration cartographique
|
||||
- Système de chat/messagerie avec salles de discussion spécifiques aux alertes
|
||||
- Authentification via vérification SMS
|
||||
- Liens profonds pour le partage d'alertes
|
||||
- Notifications push
|
||||
|
||||
## Documentation Développeur
|
||||
|
||||
Pour les développeurs souhaitant contribuer au projet ou déployer l'application, consultez la [documentation technique complète](DEVELOPER.md) qui contient :
|
||||
|
||||
- Aperçu du projet et fonctionnalités
|
||||
- Stack technique détaillé
|
||||
- Guide de démarrage rapide
|
||||
- Instructions d'installation (Android/iOS)
|
||||
- Structure du projet
|
||||
- Guide de dépannage
|
||||
- Instructions de build et déploiement
|
||||
|
||||
## Licence
|
||||
|
||||
Alerte Secours est sous licence **DevTheFuture Ethical Use License (DEF License)**. Points clés :
|
||||
|
||||
### Usage à but non lucratif
|
||||
- Licence perpétuelle, libre de redevances et non exclusive pour usage à but non lucratif
|
||||
- Permet l'utilisation, la modification et la distribution à des fins non lucratives
|
||||
|
||||
### Usage commercial
|
||||
- Nécessite l'obtention d'une licence payante
|
||||
- Conditions déterminées par le Concédant (DevTheFuture.org)
|
||||
|
||||
### Restrictions sur les données personnelles
|
||||
- Ne doit pas être utilisé pour monétiser, vendre ou exploiter les données personnelles
|
||||
- Les données personnelles ne peuvent pas être utilisées pour le marketing, la publicité ou l'influence politique
|
||||
- L'agrégation de données n'est autorisée que si c'est une fonctionnalité explicite divulguée aux utilisateurs
|
||||
|
||||
### Restriction concurrentielle
|
||||
- Les concurrents sont interdits d'utiliser le logiciel sans consentement explicite
|
||||
|
||||
Pour le texte complet de la licence, voir [LICENSE.md](LICENSE.md).
|
||||
|
||||
## 💙 Soutenir le projet
|
||||
|
||||
Alerte-Secours est une application mobile citoyenne, librement accessible, sans publicité ni exploitation de données.
|
||||
|
||||
Si vous souhaitez contribuer à son développement, sa maintenance et son indépendance :
|
||||
|
||||
- 🟡 **[Liberapay – Soutien régulier](https://liberapay.com/alerte-secours)**
|
||||
Pour un soutien **récurrent et engagé**. Chaque don contribue à assurer la stabilité du service sur le long terme.
|
||||
|
||||
- ☕ **[Buy Me a Coffee – Don ponctuel](https://buymeacoffee.com/alertesecours)**
|
||||
Pour un **coup de pouce ponctuel**, un café virtuel pour encourager le travail accompli !
|
||||
|
||||
- 🧑💻 **[GitHub Sponsors](https://github.com/sponsors/alerte-secours)**
|
||||
Pour les développeurs et utilisateurs de GitHub : soutenez le projet directement via votre compte.
|
||||
|
||||
## Contribuer
|
||||
|
||||
Directives pour contribuer au projet :
|
||||
|
||||
1. Suivez le style de code et les conventions utilisées dans le projet
|
||||
2. Écrivez des tests pour les nouvelles fonctionnalités
|
||||
3. Mettez à jour la documentation si nécessaire
|
||||
4. Utilisez les configurations ESLint et Prettier
|
||||
|
||||
## Support
|
||||
|
||||
Pour obtenir de l'aide, veuillez ouvrir un ticket sur notre tracker d'issues ou consulter la documentation dans le répertoire `/docs`.
|
|
@ -83,8 +83,8 @@ Project background_fetch = project(':react-native-background-fetch')
|
|||
applicationId 'com.alertesecours'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 177
|
||||
versionName "1.9.0"
|
||||
versionCode 181
|
||||
versionName "1.10.1"
|
||||
multiDexEnabled true
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
|
99
docs/android-install.md
Normal file
99
docs/android-install.md
Normal file
|
@ -0,0 +1,99 @@
|
|||
# Android App Installation Guide
|
||||
|
||||
This guide explains how to install the Android app on an emulator or physical device.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Android emulator or physical device connected via ADB
|
||||
- Java installed (for bundletool)
|
||||
- ADB installed and configured
|
||||
|
||||
## Installation
|
||||
|
||||
### Using the Yarn Script
|
||||
|
||||
The easiest way to install the app is to use the provided yarn script:
|
||||
|
||||
```bash
|
||||
# Set the device ID (emulator or physical device)
|
||||
export DEVICE=emulator-5554
|
||||
|
||||
# Run the installation script
|
||||
yarn install:android
|
||||
```
|
||||
|
||||
### Manual Installation
|
||||
|
||||
If you need to install the app manually, follow these steps:
|
||||
|
||||
1. Navigate to the bundle release directory:
|
||||
```bash
|
||||
cd android/app/build/outputs/bundle/release
|
||||
```
|
||||
|
||||
2. Build APKs with signing:
|
||||
```bash
|
||||
java -jar /opt/bundletool-all-1.17.1.jar build-apks \
|
||||
--mode universal \
|
||||
--bundle ./app-release.aab \
|
||||
--output ./app.apks \
|
||||
--ks /home/jo/lab/alerte-secours/as-app/android/app/debug.keystore \
|
||||
--ks-pass pass:android \
|
||||
--ks-key-alias androiddebugkey \
|
||||
--key-pass pass:android
|
||||
```
|
||||
|
||||
3. Convert .apks to .zip and extract:
|
||||
```bash
|
||||
mv app.apks app.zip
|
||||
unzip -o app.zip
|
||||
```
|
||||
|
||||
4. Install the APK on the device:
|
||||
```bash
|
||||
adb -s $DEVICE install universal.apk
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Finding Available Devices
|
||||
|
||||
To list all available devices:
|
||||
|
||||
```bash
|
||||
adb devices
|
||||
```
|
||||
|
||||
Example output:
|
||||
```
|
||||
List of devices attached
|
||||
emulator-5554 device
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **No devices found**: Make sure your emulator is running or your physical device is connected and has USB debugging enabled.
|
||||
|
||||
2. **Installation fails**: Check if the app is already installed. You might need to uninstall it first:
|
||||
```bash
|
||||
adb -s $DEVICE uninstall com.alertesecours
|
||||
```
|
||||
|
||||
3. **Signing issues**: If you encounter signing problems, make sure the keystore path is correct and the keystore passwords match.
|
||||
|
||||
## Custom Installation Script
|
||||
|
||||
A custom installation script (`install-android.sh`) has been created to simplify the installation process. This script:
|
||||
|
||||
1. Checks if the DEVICE environment variable is set
|
||||
2. Navigates to the bundle release directory
|
||||
3. Builds APKs with signing
|
||||
4. Converts .apks to .zip and extracts it
|
||||
5. Installs the APK on the device
|
||||
|
||||
You can run this script directly:
|
||||
|
||||
```bash
|
||||
export DEVICE=emulator-5554
|
||||
./install-android.sh
|
||||
```
|
30
install-android.sh
Executable file
30
install-android.sh
Executable file
|
@ -0,0 +1,30 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Check if DEVICE environment variable is set
|
||||
if [ -z "$DEVICE" ]; then
|
||||
echo "Error: DEVICE environment variable is not set."
|
||||
echo "Usage: DEVICE=emulator-5554 ./install-android.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Navigate to the bundle release directory
|
||||
cd android/app/build/outputs/bundle/release
|
||||
|
||||
# Build APKs with signing
|
||||
java -jar /opt/bundletool-all-1.17.1.jar build-apks \
|
||||
--mode universal \
|
||||
--bundle ./app-release.aab \
|
||||
--output ./app.apks \
|
||||
--ks $HOME/lab/alerte-secours/as-app/android/app/debug.keystore \
|
||||
--ks-pass pass:android \
|
||||
--ks-key-alias androiddebugkey \
|
||||
--key-pass pass:android
|
||||
|
||||
# Convert .apks to .zip and extract
|
||||
mv app.apks app.zip
|
||||
unzip -o app.zip
|
||||
|
||||
# Install the APK on the device
|
||||
adb -s $DEVICE install universal.apk
|
||||
|
||||
echo "Installation complete!"
|
|
@ -19,7 +19,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.9.0</string>
|
||||
<string>1.10.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
@ -42,7 +42,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>177</string>
|
||||
<string>181</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
|
|
12
package.json
12
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "alerte-secours",
|
||||
"version": "1.9.0",
|
||||
"version": "1.10.1",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "expo start --dev-client --private-key-path ./keys/private-key.pem",
|
||||
|
@ -36,7 +36,7 @@
|
|||
"e2e:start": "adb -s emulator-5554 shell am start -n com.alertesecours/.MainActivity",
|
||||
"e2e:test": "detox test --configuration android.emu.debug",
|
||||
"e2e:run": "yarn start & yarn e2e:build && yarn e2e:deploy && yarn e2e:test",
|
||||
"install:android": "cd android/app/build/outputs/bundle/release && java -jar /opt/bundletool-all-1.17.1.jar build-apks --mode universal --bundle ./app-release.aab --output ./app.apks && mv app.apks app.zip && unzip -o app.zip && adb -s $DEVICE install universal.apk",
|
||||
"install:android": "./install-android.sh",
|
||||
"log:android": "adb -s $DEVICE logcat | grep -E 'ReactNativeJS: '",
|
||||
"log:ios:simulator": "xcrun simctl spawn booted log stream --level debug --predicate 'subsystem contains \"com.facebook.react.log\" and processImagePath contains \"AlerteSecours\"'",
|
||||
"log:ios": "idevicesyslog | grep -i 'AlerteSecours\\|ReactNative'",
|
||||
|
@ -44,11 +44,13 @@
|
|||
"log-ios": "react-native log-ios",
|
||||
"open:deeplink:android": "yarn open:deeplink --android",
|
||||
"open:deeplink:ios": "yarn open:deeplink --ios",
|
||||
"open:deeplink": "npx uri-scheme open --android"
|
||||
"open:deeplink": "npx uri-scheme open --android",
|
||||
"screenshot:ios": "scripts/screenshot-ios.sh",
|
||||
"screenshot:android": "scripts/screenshot-android.sh"
|
||||
},
|
||||
"customExpoVersioning": {
|
||||
"versionCode": 177,
|
||||
"buildNumber": 177
|
||||
"versionCode": 181,
|
||||
"buildNumber": 181
|
||||
},
|
||||
"commit-and-tag-version": {
|
||||
"scripts": {
|
||||
|
|
7
scripts/screenshot-android.sh
Executable file
7
scripts/screenshot-android.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
if [ -z "$DEVICE" ]; then
|
||||
echo "Error: DEVICE environment variable is not set."
|
||||
echo "Usage: DEVICE=emulator-5554 ./screenshot-android.sh"
|
||||
exit 1
|
||||
fi
|
||||
exec adb -s $DEVICE exec-out screencap -p > screenshot-emulator-$(date +%s).png
|
2
scripts/screenshot-ios.sh
Executable file
2
scripts/screenshot-ios.sh
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/env bash
|
||||
exec xcrun simctl io booted screenshot ~/Desktop/screenshot-sim-$(date +%s).png
|
|
@ -9,11 +9,15 @@ const ALERT_FIELDS_FRAGMENT = gql`
|
|||
radius
|
||||
alertTag
|
||||
location
|
||||
initialLocation
|
||||
createdAt
|
||||
closedAt
|
||||
address
|
||||
what3Words
|
||||
nearestPlace
|
||||
lastAddress
|
||||
lastWhat3Words
|
||||
lastNearestPlace
|
||||
username
|
||||
code
|
||||
notifiedCount # deprecated
|
||||
|
@ -25,6 +29,7 @@ const ALERT_FIELDS_FRAGMENT = gql`
|
|||
acknowledgedRelativeCount
|
||||
acknowledgedAroundCount
|
||||
acknowledgedConnectCount
|
||||
followLocation
|
||||
accessCode
|
||||
userId
|
||||
keepOpenAt
|
||||
|
|
BIN
src/assets/img/marker-origin.png
Normal file
BIN
src/assets/img/marker-origin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
|
@ -13,6 +13,9 @@ import { getDeviceUuid } from "./deviceUuid";
|
|||
export async function registerUser() {
|
||||
const { data } = await network.apolloClient.mutate({
|
||||
mutation: REGISTER_USER_MUTATION,
|
||||
context: {
|
||||
skipAuth: true, // Skip adding Authorization header
|
||||
},
|
||||
});
|
||||
const authToken = data.addOneAuthInitToken.authTokenJwt;
|
||||
return { authToken };
|
||||
|
@ -27,6 +30,9 @@ export async function loginUserToken({ authToken }) {
|
|||
phoneModel: Device.modelName,
|
||||
deviceUuid,
|
||||
},
|
||||
context: {
|
||||
skipAuth: true, // Skip adding Authorization header
|
||||
},
|
||||
});
|
||||
const userToken = data.doAuthLoginToken.userBearerJwt;
|
||||
return { userToken };
|
||||
|
|
|
@ -7,17 +7,15 @@ import useTimeDisplay from "~/hooks/useTimeDisplay";
|
|||
export default function AlertInfoLineClosedTime({ alert, ...props }) {
|
||||
const { closedAt } = alert;
|
||||
const closedAtText = useTimeDisplay(closedAt);
|
||||
|
||||
if (!closedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertInfoLine
|
||||
icon={() => (
|
||||
<MaterialCommunityIcons name="clock-time-four-outline" size={24} />
|
||||
)}
|
||||
text={`Terminée ${closedAtText}`}
|
||||
iconName={"clock-time-four-outline"}
|
||||
labelText={`Terminée`}
|
||||
valueText={closedAtText}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
51
src/containers/LocationInfoSection/index.js
Normal file
51
src/containers/LocationInfoSection/index.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Text from "~/components/Text";
|
||||
import AlertInfoLineAddress from "~/containers/AlertInfoLines/Address";
|
||||
import AlertInfoLineNear from "~/containers/AlertInfoLines/Near";
|
||||
import AlertInfoLineW3w from "~/containers/AlertInfoLines/W3w";
|
||||
|
||||
/**
|
||||
* LocationInfoSection component displays location information with a title
|
||||
* and optional address, nearby location, and what3words information.
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.title - Title to display for the location section
|
||||
* @param {Object} props.alert - Alert object containing location information
|
||||
* @param {boolean} [props.showAddress=true] - Whether to show the address information
|
||||
* @param {boolean} [props.showNear=true] - Whether to show the nearby location information
|
||||
* @param {boolean} [props.showW3w=true] - Whether to show the what3words information
|
||||
* @param {Object} props.styles - Styles object containing locationSectionTitle and locationTitle styles
|
||||
* @param {boolean} [props.useLastLocation=false] - Whether to use the last known location instead of current location
|
||||
* @returns {React.ReactElement} The rendered component
|
||||
*/
|
||||
export default function LocationInfoSection({
|
||||
title,
|
||||
alert,
|
||||
showAddress = true,
|
||||
showNear = true,
|
||||
showW3w = true,
|
||||
styles,
|
||||
useLastLocation = false,
|
||||
}) {
|
||||
// Create a modified alert object with the appropriate properties
|
||||
const displayAlert = useLastLocation
|
||||
? {
|
||||
...alert,
|
||||
address: alert.lastAddress,
|
||||
nearestPlace: alert.lastNearLocation,
|
||||
what3Words: alert.lastWhat3Words,
|
||||
}
|
||||
: alert;
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.locationSectionTitle}>
|
||||
<Text style={styles.locationTitle}>{title}</Text>
|
||||
</View>
|
||||
{showAddress && <AlertInfoLineAddress alert={displayAlert} />}
|
||||
{showNear && <AlertInfoLineNear alert={displayAlert} />}
|
||||
{showW3w && <AlertInfoLineW3w alert={displayAlert} />}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -8,6 +8,7 @@ import markerGreen from "~/assets/img/marker-green.png";
|
|||
import markerRedDisabled from "~/assets/img/marker-red-disabled.png";
|
||||
import markerYellowDisabled from "~/assets/img/marker-yellow-disabled.png";
|
||||
import markerGreenDisabled from "~/assets/img/marker-green-disabled.png";
|
||||
import markerOrigin from "~/assets/img/marker-origin.png";
|
||||
|
||||
const images = {
|
||||
red: markerRed,
|
||||
|
@ -16,6 +17,7 @@ const images = {
|
|||
redDisabled: markerRedDisabled,
|
||||
yellowDisabled: markerYellowDisabled,
|
||||
greenDisabled: markerGreenDisabled,
|
||||
origin: markerOrigin,
|
||||
};
|
||||
|
||||
export default function FeatureImages() {
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import React from "react";
|
||||
|
||||
import SelectedFeatureBubbleAlert from "./SelectedFeatureBubbleAlert";
|
||||
import SelectedFeatureBubbleAlertInitial from "./SelectedFeatureBubbleAlertInitial";
|
||||
|
||||
export default function SelectedFeatureBubble(props) {
|
||||
const { properties = {} } = props.feature;
|
||||
if (properties.alert) {
|
||||
// Check if this is an initial location marker
|
||||
if (properties.isInitialLocation) {
|
||||
return <SelectedFeatureBubbleAlertInitial {...props} />;
|
||||
}
|
||||
return <SelectedFeatureBubbleAlert {...props} />;
|
||||
}
|
||||
return null;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useCallback } from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { deepEqual } from "fast-equals";
|
||||
import { View } from "react-native";
|
||||
import { IconButton, TouchableRipple } from "react-native-paper";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
|
@ -63,6 +64,31 @@ export default function SelectedFeatureBubbleAlert({ feature, close }) {
|
|||
/>
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
{useMemo(() => {
|
||||
const isLocationsDifferent =
|
||||
alert.initialLocation &&
|
||||
alert.location &&
|
||||
!deepEqual(alert.initialLocation, alert.location);
|
||||
|
||||
if (isLocationsDifferent) {
|
||||
return (
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.titleText}>
|
||||
{alert.followLocation
|
||||
? "Localisation actuelle"
|
||||
: "Dernière position connue"}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
alert.initialLocation,
|
||||
alert.location,
|
||||
alert.followLocation,
|
||||
styles.titleContainer,
|
||||
styles.titleText,
|
||||
])}
|
||||
<View style={styles.contentLine}>
|
||||
<Text style={styles.contentText}>Sujet :</Text>
|
||||
<Text style={[styles.contentTextValue, { color: levelColor }]}>
|
||||
|
@ -123,6 +149,18 @@ const useStyles = createStyles(({ wp, fontSize, theme: { colors } }) => ({
|
|||
content: {
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
titleContainer: {
|
||||
marginBottom: 8,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.grey,
|
||||
paddingBottom: 4,
|
||||
},
|
||||
titleText: {
|
||||
color: colors.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
},
|
||||
contentLine: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
|
|
185
src/containers/Map/SelectedFeatureBubbleAlertInitial.js
Normal file
185
src/containers/Map/SelectedFeatureBubbleAlertInitial.js
Normal file
|
@ -0,0 +1,185 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { View } from "react-native";
|
||||
import { IconButton, TouchableRipple } from "react-native-paper";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
|
||||
import Maplibre from "@maplibre/maplibre-react-native";
|
||||
import Text from "~/components/Text";
|
||||
import useTimeDisplay from "~/hooks/useTimeDisplay";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { createStyles, useTheme } from "~/theme";
|
||||
import { alertActions } from "~/stores";
|
||||
|
||||
export default function SelectedFeatureBubbleAlertInitial({ feature, close }) {
|
||||
const { properties = {} } = feature;
|
||||
const { alert } = properties;
|
||||
const styles = useStyles();
|
||||
const { colors, custom } = useTheme();
|
||||
const createdAtText = useTimeDisplay(alert.createdAt);
|
||||
const navigation = useNavigation();
|
||||
const goToAlert = useCallback(() => {
|
||||
alertActions.setNavAlertCur({ alert });
|
||||
navigation.navigate({
|
||||
name: "AlertCur",
|
||||
params: {
|
||||
screen: "AlertCurTab",
|
||||
params: {
|
||||
screen: "AlertCurOverview",
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [alert, navigation]);
|
||||
|
||||
const { level } = alert;
|
||||
const levelColor = custom.appColors[level];
|
||||
|
||||
return (
|
||||
<Maplibre.MarkerView
|
||||
key={feature.properties.id}
|
||||
id="selectedFeaturePointAnnotation"
|
||||
aboveLayerID="lineLayer"
|
||||
coordinate={feature.geometry.coordinates}
|
||||
anchor={{ x: 0, y: 1 }}
|
||||
>
|
||||
<View style={styles.bubbleContainer}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size={14}
|
||||
style={styles.closeButton}
|
||||
icon={() => (
|
||||
<MaterialCommunityIcons
|
||||
name="close"
|
||||
size={22}
|
||||
style={styles.closeButtonIcon}
|
||||
/>
|
||||
)}
|
||||
onPress={() => close()}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.titleText}>
|
||||
Localisation initiale de l'Alerte
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.contentLine}>
|
||||
<Text style={styles.contentText}>Sujet :</Text>
|
||||
<Text style={[styles.contentTextValue, { color: levelColor }]}>
|
||||
{alert.subject || "non indiqué"}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.contentLine}>
|
||||
<Text style={[styles.contentText]}>Code :</Text>
|
||||
<Text style={[styles.contentTextValue]}>#{alert.code}</Text>
|
||||
</View>
|
||||
<View style={styles.contentLine}>
|
||||
<Text style={styles.contentText}>Envoyée par :</Text>
|
||||
<Text style={styles.contentTextValue}>{alert.username}</Text>
|
||||
<Text style={styles.contentTextValue}>{createdAtText}</Text>
|
||||
</View>
|
||||
<View style={styles.contentLine}>
|
||||
<Text style={styles.contentText}>Depuis l'adresse :</Text>
|
||||
<Text style={styles.contentTextValue}>{alert.address}</Text>
|
||||
</View>
|
||||
<View style={styles.contentLine}>
|
||||
<Text style={styles.contentText}>À proximité de :</Text>
|
||||
<Text style={styles.contentTextValue}>{alert.nearestPlace}</Text>
|
||||
</View>
|
||||
<View style={styles.contentLine}>
|
||||
<Text style={styles.contentText}>Localisation en 3 mots :</Text>
|
||||
<Text style={styles.contentTextValue}>{alert.what3Words}</Text>
|
||||
</View>
|
||||
<TouchableRipple style={styles.alertLinkButton} onPress={goToAlert}>
|
||||
<View style={styles.alertLinkContent}>
|
||||
<MaterialCommunityIcons
|
||||
name="adjust"
|
||||
style={styles.alertLinkButtonIcon}
|
||||
/>
|
||||
<Text style={styles.alertLinkText}>Situation</Text>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</View>
|
||||
</View>
|
||||
</Maplibre.MarkerView>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = createStyles(({ wp, fontSize, theme: { colors } }) => ({
|
||||
bubbleContainer: {
|
||||
backgroundColor: colors.surface,
|
||||
justifyContent: "center",
|
||||
alignItems: "flex-start",
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 5,
|
||||
width: wp(75),
|
||||
},
|
||||
bubbleText: {
|
||||
color: colors.onSurface,
|
||||
fontSize: 16,
|
||||
},
|
||||
closeButton: {},
|
||||
closeButtonIcon: {
|
||||
color: colors.grey,
|
||||
},
|
||||
content: {
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
titleContainer: {
|
||||
marginBottom: 8,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.grey,
|
||||
paddingBottom: 4,
|
||||
},
|
||||
titleText: {
|
||||
color: colors.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
},
|
||||
contentLine: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
contentText: {
|
||||
color: colors.onSurface,
|
||||
fontSize: 15,
|
||||
paddingRight: 5,
|
||||
},
|
||||
contentTextValue: {
|
||||
color: colors.onSurfaceVariant,
|
||||
fontSize: 15,
|
||||
paddingRight: 5,
|
||||
textAlign: "right",
|
||||
flexGrow: 1,
|
||||
},
|
||||
alertLinkButton: {
|
||||
marginTop: 15,
|
||||
marginBottom: 5,
|
||||
borderRadius: 0,
|
||||
paddingVertical: 0,
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
alertLinkContent: {
|
||||
height: 28,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
alertLinkText: {
|
||||
color: colors.surface,
|
||||
fontSize: 15,
|
||||
},
|
||||
alertLinkButtonIcon: {
|
||||
color: colors.surface,
|
||||
marginRight: 5,
|
||||
fontSize: 15,
|
||||
},
|
||||
}));
|
|
@ -12,6 +12,11 @@ const hitbox = {
|
|||
height: HITBOX_SIZE,
|
||||
};
|
||||
|
||||
const iconStyle = {
|
||||
iconImage: ["get", "icon"],
|
||||
iconSize: 0.5,
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ theme: { colors } }) => ({
|
||||
clusterCount: {
|
||||
textField: "{point_count_abbreviated}",
|
||||
|
@ -46,6 +51,13 @@ export default function ShapePoints({ shape, children, ...shapeSourceProps }) {
|
|||
<AlertSymbolLayer level="yellow" isDisabled />
|
||||
<AlertSymbolLayer level="green" isDisabled />
|
||||
|
||||
<Maplibre.SymbolLayer
|
||||
filter={["==", ["get", "icon"], "origin"]}
|
||||
key="points-origin"
|
||||
id="points-origin"
|
||||
style={iconStyle}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</Maplibre.ShapeSource>
|
||||
);
|
||||
|
|
40
src/env.js
40
src/env.js
|
@ -1,4 +1,8 @@
|
|||
import { Platform } from "react-native";
|
||||
import { secureStore } from "~/lib/secureStore";
|
||||
|
||||
// Key for storing staging setting in secureStore
|
||||
const STAGING_SETTING_KEY = "env.isStaging";
|
||||
|
||||
// Logging configuration
|
||||
const LOG_SCOPES = process.env.APP_LOG_SCOPES;
|
||||
|
@ -85,16 +89,50 @@ const stagingMap = {
|
|||
IS_STAGING: true,
|
||||
};
|
||||
|
||||
export const setStaging = (enabled) => {
|
||||
export const setStaging = async (enabled) => {
|
||||
for (const key of Object.keys(env)) {
|
||||
if (stagingMap[key] !== undefined) {
|
||||
env[key] = enabled ? stagingMap[key] : envMap[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the staging setting
|
||||
await secureStore.setItemAsync(STAGING_SETTING_KEY, String(enabled));
|
||||
};
|
||||
|
||||
// Initialize with default values
|
||||
const env = { ...envMap };
|
||||
|
||||
// Load the staging setting from secureStore
|
||||
export const initializeEnv = async () => {
|
||||
try {
|
||||
const storedStaging = await secureStore.getItemAsync(STAGING_SETTING_KEY);
|
||||
if (storedStaging !== null) {
|
||||
const isStaging = storedStaging === "true";
|
||||
if (isStaging) {
|
||||
// Apply staging settings without persisting again
|
||||
for (const key of Object.keys(env)) {
|
||||
if (stagingMap[key] !== undefined) {
|
||||
env[key] = stagingMap[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load staging setting from secureStore:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize environment settings
|
||||
// We use an IIFE to handle the async initialization
|
||||
(async () => {
|
||||
try {
|
||||
await initializeEnv();
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize environment settings:", error);
|
||||
}
|
||||
})();
|
||||
|
||||
export default env;
|
||||
|
||||
// +1
|
||||
|
|
|
@ -2,7 +2,11 @@ import { useEffect, useRef, useState } from "react";
|
|||
import { createLogger } from "~/lib/logger";
|
||||
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||
|
||||
import { usePermissionWizardState, usePermissionsState } from "~/stores";
|
||||
import {
|
||||
usePermissionWizardState,
|
||||
usePermissionsState,
|
||||
useTreeState,
|
||||
} from "~/stores";
|
||||
|
||||
import trackLocation from "~/location/trackLocation";
|
||||
|
||||
|
@ -12,6 +16,8 @@ const locationLogger = createLogger({
|
|||
});
|
||||
|
||||
export default function useTrackLocation() {
|
||||
const { splashScreenHidden } = useTreeState(["splashScreenHidden"]);
|
||||
|
||||
const { currentStep, completed } = usePermissionWizardState([
|
||||
"completed",
|
||||
"currentStep",
|
||||
|
@ -34,7 +40,8 @@ export default function useTrackLocation() {
|
|||
if (
|
||||
locationBackground &&
|
||||
motion &&
|
||||
(currentStep === "tracking" || currentStep === "success" || completed)
|
||||
(currentStep === "tracking" || currentStep === "success" || completed) &&
|
||||
splashScreenHidden
|
||||
) {
|
||||
locationLogger.info("Enabling location tracking", {
|
||||
step: currentStep,
|
||||
|
@ -48,7 +55,7 @@ export default function useTrackLocation() {
|
|||
step: currentStep,
|
||||
});
|
||||
}
|
||||
}, [locationBackground, motion, currentStep, completed]);
|
||||
}, [locationBackground, motion, currentStep, completed, splashScreenHidden]);
|
||||
|
||||
useEffect(() => {
|
||||
if (trackLocationEnabled) {
|
||||
|
|
|
@ -9,6 +9,8 @@ import LayoutProviders from "~/layout/LayoutProviders";
|
|||
import loadRessources from "~/layout/loadRessources";
|
||||
import useMount from "~/hooks/useMount";
|
||||
|
||||
import { treeActions } from "~/stores";
|
||||
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
export default function AppView() {
|
||||
|
@ -30,6 +32,7 @@ export default function AppView() {
|
|||
const onLayoutRootView = useCallback(async () => {
|
||||
if (appIsReady) {
|
||||
await SplashScreen.hideAsync();
|
||||
treeActions.splashScreenHidden();
|
||||
}
|
||||
}, [appIsReady]);
|
||||
|
||||
|
|
|
@ -31,3 +31,12 @@ export const LOG_LEVEL_PRIORITY = {
|
|||
[LOG_LEVELS.WARN]: 2,
|
||||
[LOG_LEVELS.ERROR]: 3,
|
||||
};
|
||||
|
||||
// Function to update the minimum log level
|
||||
export const setMinLogLevel = (level) => {
|
||||
if (LOG_LEVELS[level] || Object.values(LOG_LEVELS).includes(level)) {
|
||||
config.minLevel = level;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -93,4 +93,4 @@ export const logger = new Logger();
|
|||
export const createLogger = (scopes) => logger.withScopes(scopes);
|
||||
|
||||
// Export types and config for external use
|
||||
export { LOG_LEVELS } from "./config";
|
||||
export { LOG_LEVELS, setMinLogLevel } from "./config";
|
||||
|
|
110
src/location/emulatorService.js
Normal file
110
src/location/emulatorService.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { createLogger } from "~/lib/logger";
|
||||
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||
|
||||
const EMULATOR_MODE_KEY = "emulator_mode_enabled";
|
||||
|
||||
// Global variables
|
||||
let emulatorIntervalId = null;
|
||||
let isEmulatorModeEnabled = false;
|
||||
|
||||
// Create a logger for the emulator service
|
||||
const emulatorLogger = createLogger({
|
||||
module: BACKGROUND_SCOPES.GEOLOCATION,
|
||||
feature: "emulator",
|
||||
});
|
||||
|
||||
// Initialize emulator mode based on stored preference
|
||||
export const initEmulatorMode = async () => {
|
||||
try {
|
||||
const storedValue = await AsyncStorage.getItem(EMULATOR_MODE_KEY);
|
||||
emulatorLogger.debug("Initializing emulator mode", { storedValue });
|
||||
|
||||
if (storedValue === "true") {
|
||||
await enableEmulatorMode();
|
||||
}
|
||||
} catch (error) {
|
||||
emulatorLogger.error("Failed to initialize emulator mode", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Enable emulator mode
|
||||
export const enableEmulatorMode = async () => {
|
||||
emulatorLogger.info("Enabling emulator mode");
|
||||
|
||||
// Clear existing interval if any
|
||||
if (emulatorIntervalId) {
|
||||
clearInterval(emulatorIntervalId);
|
||||
}
|
||||
|
||||
try {
|
||||
// Call immediately once
|
||||
await BackgroundGeolocation.changePace(true);
|
||||
emulatorLogger.debug("Initial changePace call successful");
|
||||
|
||||
// Then set up interval
|
||||
emulatorIntervalId = setInterval(
|
||||
() => {
|
||||
BackgroundGeolocation.changePace(true);
|
||||
emulatorLogger.debug("Interval changePace call executed");
|
||||
},
|
||||
30 * 60 * 1000,
|
||||
); // 30 minutes
|
||||
|
||||
isEmulatorModeEnabled = true;
|
||||
|
||||
// Persist the setting
|
||||
await AsyncStorage.setItem(EMULATOR_MODE_KEY, "true");
|
||||
emulatorLogger.debug("Emulator mode setting saved");
|
||||
} catch (error) {
|
||||
emulatorLogger.error("Failed to enable emulator mode", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Disable emulator mode
|
||||
export const disableEmulatorMode = async () => {
|
||||
emulatorLogger.info("Disabling emulator mode");
|
||||
|
||||
if (emulatorIntervalId) {
|
||||
clearInterval(emulatorIntervalId);
|
||||
emulatorIntervalId = null;
|
||||
}
|
||||
|
||||
isEmulatorModeEnabled = false;
|
||||
|
||||
// Persist the setting
|
||||
try {
|
||||
await AsyncStorage.setItem(EMULATOR_MODE_KEY, "false");
|
||||
emulatorLogger.debug("Emulator mode setting saved");
|
||||
} catch (error) {
|
||||
emulatorLogger.error("Failed to save emulator mode setting", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get current emulator mode state
|
||||
export const getEmulatorModeState = () => {
|
||||
return isEmulatorModeEnabled;
|
||||
};
|
||||
|
||||
// Toggle emulator mode
|
||||
export const toggleEmulatorMode = async (enabled) => {
|
||||
emulatorLogger.info("Toggling emulator mode", { enabled });
|
||||
|
||||
if (enabled) {
|
||||
await enableEmulatorMode();
|
||||
} else {
|
||||
await disableEmulatorMode();
|
||||
}
|
||||
|
||||
return isEmulatorModeEnabled;
|
||||
};
|
|
@ -3,6 +3,9 @@ import { TRACK_MOVE } from "~/misc/devicePrefs";
|
|||
import { createLogger } from "~/lib/logger";
|
||||
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||
import jwtDecode from "jwt-decode";
|
||||
import { initEmulatorMode } from "./emulatorService";
|
||||
|
||||
import throttle from "lodash.throttle";
|
||||
|
||||
import {
|
||||
getAuthState,
|
||||
|
@ -64,6 +67,16 @@ export default async function trackLocation() {
|
|||
feature: "tracking",
|
||||
});
|
||||
|
||||
// Log the geolocation sync URL for debugging
|
||||
locationLogger.info("Geolocation sync URL configuration", {
|
||||
url: env.GEOLOC_SYNC_URL,
|
||||
isStaging: env.IS_STAGING,
|
||||
});
|
||||
|
||||
// Throttling configuration for auth reload only
|
||||
const AUTH_RELOAD_THROTTLE = 5000; // 5 seconds throttle
|
||||
|
||||
// Handle auth function - no throttling or cooldown
|
||||
async function handleAuth(userToken) {
|
||||
locationLogger.info("Handling auth token update", {
|
||||
hasToken: !!userToken,
|
||||
|
@ -77,10 +90,40 @@ export default async function trackLocation() {
|
|||
// unsub();
|
||||
locationLogger.debug("Updating background geolocation config");
|
||||
await BackgroundGeolocation.setConfig({
|
||||
url: env.GEOLOC_SYNC_URL, // Update the sync URL for when it's changed for staging
|
||||
headers: {
|
||||
Authorization: `Bearer ${userToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Log the authorization header that was set
|
||||
locationLogger.debug(
|
||||
"Set Authorization header for background geolocation",
|
||||
{
|
||||
headerSet: true,
|
||||
tokenPrefix: userToken ? userToken.substring(0, 10) + "..." : null,
|
||||
},
|
||||
);
|
||||
|
||||
// Verify the current configuration
|
||||
try {
|
||||
const currentConfig = await BackgroundGeolocation.getConfig();
|
||||
locationLogger.debug("Current background geolocation config", {
|
||||
hasHeaders: !!currentConfig.headers,
|
||||
headerKeys: currentConfig.headers
|
||||
? Object.keys(currentConfig.headers)
|
||||
: [],
|
||||
authHeader: currentConfig.headers?.Authorization
|
||||
? currentConfig.headers.Authorization.substring(0, 15) + "..."
|
||||
: "Not set",
|
||||
url: currentConfig.url,
|
||||
});
|
||||
} catch (error) {
|
||||
locationLogger.error("Failed to get background geolocation config", {
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
const state = await BackgroundGeolocation.getState();
|
||||
try {
|
||||
const decodedToken = jwtDecode(userToken);
|
||||
|
@ -135,7 +178,20 @@ export default async function trackLocation() {
|
|||
}
|
||||
});
|
||||
|
||||
// The core auth reload function that will be throttled
|
||||
function _reloadAuth() {
|
||||
locationLogger.info("Refreshing authentication token");
|
||||
authActions.reload(); // should retriger sync in handleAuth via subscribeAuthState when done
|
||||
}
|
||||
|
||||
// Create throttled version of auth reload with lodash
|
||||
const reloadAuth = throttle(_reloadAuth, AUTH_RELOAD_THROTTLE, {
|
||||
leading: true,
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
BackgroundGeolocation.onHttp((response) => {
|
||||
// Log the full response including headers if available
|
||||
locationLogger.debug("HTTP response received", {
|
||||
status: response?.status,
|
||||
success: response?.success,
|
||||
|
@ -143,15 +199,48 @@ export default async function trackLocation() {
|
|||
url: response?.url,
|
||||
method: response?.method,
|
||||
isSync: response?.isSync,
|
||||
requestHeaders:
|
||||
response?.request?.headers || "Headers not available in response",
|
||||
});
|
||||
|
||||
// Log the current auth token for comparison
|
||||
const { userToken } = getAuthState();
|
||||
locationLogger.debug("Current auth state token", {
|
||||
tokenAvailable: !!userToken,
|
||||
tokenPrefix: userToken ? userToken.substring(0, 10) + "..." : null,
|
||||
});
|
||||
|
||||
const statusCode = response?.status;
|
||||
|
||||
switch (statusCode) {
|
||||
case 410:
|
||||
// Token expired, logout
|
||||
locationLogger.info("Auth token expired (410), logging out");
|
||||
authActions.logout();
|
||||
break;
|
||||
case 401:
|
||||
locationLogger.info("Refreshing authentication token");
|
||||
authActions.reload(); // should retriger sync in handleAuth via subscribeAuthState when done
|
||||
// Unauthorized, use throttled reload
|
||||
locationLogger.info("Unauthorized (401), attempting to refresh token");
|
||||
|
||||
// Add more detailed logging of the error response
|
||||
try {
|
||||
const errorBody = response?.responseText
|
||||
? JSON.parse(response.responseText)
|
||||
: null;
|
||||
locationLogger.debug("Unauthorized error details", {
|
||||
errorBody,
|
||||
errorType: errorBody?.error?.type,
|
||||
errorMessage: errorBody?.error?.message,
|
||||
errorPath: errorBody?.error?.errors?.[0]?.path,
|
||||
});
|
||||
} catch (e) {
|
||||
locationLogger.debug("Failed to parse error response", {
|
||||
error: e.message,
|
||||
responseText: response?.responseText,
|
||||
});
|
||||
}
|
||||
|
||||
reloadAuth();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
@ -205,10 +294,14 @@ export default async function trackLocation() {
|
|||
if (count > 0) {
|
||||
locationLogger.info(`Found ${count} pending records, forcing sync`);
|
||||
try {
|
||||
const records = await BackgroundGeolocation.sync();
|
||||
locationLogger.debug("Forced sync result", {
|
||||
recordsCount: records?.length || 0,
|
||||
});
|
||||
const { userToken } = getAuthState();
|
||||
const state = await BackgroundGeolocation.getState();
|
||||
if (userToken && state.enabled) {
|
||||
const records = await BackgroundGeolocation.sync();
|
||||
locationLogger.debug("Forced sync result", {
|
||||
recordsCount: records?.length || 0,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
locationLogger.error("Forced sync failed", {
|
||||
error: error,
|
||||
|
@ -231,4 +324,7 @@ export default async function trackLocation() {
|
|||
|
||||
// Check for pending records after a short delay to ensure everything is initialized
|
||||
setTimeout(checkPendingRecords, 5000);
|
||||
|
||||
// Initialize emulator mode if previously enabled
|
||||
initEmulatorMode();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// related to services/tasks/src/geocode/config.js
|
||||
export const TRACK_MOVE = 100;
|
||||
export const TRACK_MOVE = 10;
|
||||
export const DEFAULT_DEVICE_RADIUS_ALL = 500;
|
||||
export const DEFAULT_DEVICE_RADIUS_REACH = 25000;
|
||||
export const MAX_BASEUSER_DEVICE_TRACKING = 25000;
|
||||
|
|
|
@ -21,6 +21,7 @@ import Relatives from "~/scenes/Relatives";
|
|||
import Sheets from "~/scenes/Sheets";
|
||||
import AlertAggListArchived from "~/scenes/AlertAggListArchived";
|
||||
import About from "~/scenes/About";
|
||||
import Contribute from "~/scenes/Contribute";
|
||||
import Location from "~/scenes/Location";
|
||||
import Developer from "~/scenes/Developer";
|
||||
import HelpSignal from "~/scenes/HelpSignal";
|
||||
|
@ -413,6 +414,22 @@ export default React.memo(function DrawerNav() {
|
|||
}}
|
||||
listeners={{}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="Contribute"
|
||||
component={Contribute}
|
||||
options={{
|
||||
drawerLabel: "Faire un don",
|
||||
drawerIcon: ({ focused }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="hand-heart"
|
||||
{...iconProps}
|
||||
{...(focused ? iconFocusedProps : {})}
|
||||
/>
|
||||
),
|
||||
unmountOnBlur: true,
|
||||
}}
|
||||
listeners={{}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="Sheets"
|
||||
component={Sheets}
|
||||
|
|
|
@ -253,6 +253,12 @@ export default function HeaderRight(props) {
|
|||
}}
|
||||
/>
|
||||
<Divider />
|
||||
<Menu.Item
|
||||
title="Faire un don"
|
||||
onPress={() => {
|
||||
navigateTo({ name: "Contribute" });
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="À Propos"
|
||||
onPress={() => {
|
||||
|
|
|
@ -39,6 +39,8 @@ function getHeaderTitle(route) {
|
|||
return "Alertes archivées";
|
||||
case "About":
|
||||
return "À Propos";
|
||||
case "Contribute":
|
||||
return "Faire un don";
|
||||
case "Location":
|
||||
return "Ma Localisation";
|
||||
case "NotFound":
|
||||
|
|
|
@ -13,15 +13,17 @@ export default function createAuthLink({ store }) {
|
|||
const { getAuthState } = store;
|
||||
|
||||
const authLink = new ApolloLink((operation, forward) => {
|
||||
const { userToken } = getAuthState();
|
||||
const headers = operation.getContext().hasOwnProperty("headers")
|
||||
? operation.getContext().headers
|
||||
: {};
|
||||
if (userToken && headers["X-Hasura-Role"] !== "anonymous") {
|
||||
setBearerHeader(headers, userToken);
|
||||
} else {
|
||||
headers["X-Hasura-Role"] = "anonymous";
|
||||
const context = operation.getContext();
|
||||
const headers = context.hasOwnProperty("headers") ? context.headers : {};
|
||||
|
||||
// Skip adding auth header if skipAuth flag is set
|
||||
if (!context.skipAuth) {
|
||||
const { userToken } = getAuthState();
|
||||
if (userToken) {
|
||||
setBearerHeader(headers, userToken);
|
||||
}
|
||||
}
|
||||
|
||||
// authLinkLogger.debug("Request headers", { headers });
|
||||
operation.setContext({ headers });
|
||||
return forward(operation);
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { View, Image, ScrollView } from "react-native";
|
||||
import {
|
||||
View,
|
||||
Image,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Linking,
|
||||
Alert,
|
||||
} from "react-native";
|
||||
import { Button } from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import * as Updates from "expo-updates";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { MaterialIcons, AntDesign, FontAwesome } from "@expo/vector-icons";
|
||||
import Text from "~/components/Text";
|
||||
|
||||
import { useTheme } from "~/theme";
|
||||
import { useTheme, createStyles } from "~/theme";
|
||||
|
||||
import { paramsActions, useParamsState } from "~/stores";
|
||||
|
||||
|
@ -16,7 +24,24 @@ const logo = require("~/assets/img/logo192.png");
|
|||
|
||||
const version = require("../../../package.json").version;
|
||||
|
||||
const openURL = async (url) => {
|
||||
try {
|
||||
const supported = await Linking.canOpenURL(url);
|
||||
if (supported) {
|
||||
await Linking.openURL(url);
|
||||
} else {
|
||||
Alert.alert("Erreur", "Impossible d'ouvrir ce lien");
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
"Erreur",
|
||||
"Une erreur s'est produite lors de l'ouverture du lien",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function About() {
|
||||
const navigation = useNavigation();
|
||||
const textStyle = {
|
||||
textAlign: "left",
|
||||
fontSize: 16,
|
||||
|
@ -153,6 +178,81 @@ export default function About() {
|
|||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Website and Contribute Buttons */}
|
||||
<View style={{ paddingHorizontal: 15, paddingVertical: 10 }}>
|
||||
{/* Website Button */}
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 10,
|
||||
shadowColor: colors.shadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
}}
|
||||
onPress={() => openURL("https://alerte-secours.fr")}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="language"
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
style={{ marginRight: 10 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: colors.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
marginLeft: 5,
|
||||
}}
|
||||
>
|
||||
Site officiel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Contribute Button */}
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 8,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
shadowColor: colors.shadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
}}
|
||||
onPress={() => navigation.navigate("Contribute")}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="favorite"
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
style={{ marginRight: 10 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: colors.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
marginLeft: 5,
|
||||
}}
|
||||
>
|
||||
Contribuer au projet
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={{ padding: 15, justifyContent: "center" }}>
|
||||
<Button
|
||||
mode="text"
|
||||
|
|
|
@ -3,6 +3,7 @@ import { getDistance } from "geolib";
|
|||
import Supercluster from "supercluster";
|
||||
import useShallowMemo from "~/hooks/useShallowMemo";
|
||||
import useShallowEffect from "~/hooks/useShallowEffect";
|
||||
import { deepEqual } from "fast-equals";
|
||||
|
||||
export default function useFeatures({
|
||||
clusterFeature,
|
||||
|
@ -38,35 +39,67 @@ export default function useFeatures({
|
|||
return computedList;
|
||||
}, [alertingList, userCoords, hasUserCoords]);
|
||||
|
||||
const featureCollection = useShallowMemo(
|
||||
() => ({
|
||||
type: "FeatureCollection",
|
||||
features: list.map((row) => {
|
||||
const { alert } = row;
|
||||
const { level, state } = alert;
|
||||
const [longitude, latitude] = alert.location.coordinates;
|
||||
const featureCollection = useShallowMemo(() => {
|
||||
const features = list.map((row) => {
|
||||
const { alert } = row;
|
||||
const { level, state } = alert;
|
||||
const [longitude, latitude] = alert.location.coordinates;
|
||||
const coordinates = [longitude, latitude];
|
||||
const id = `alert:${alert.id}`;
|
||||
const icon = state === "open" ? level : `${level}Disabled`;
|
||||
return {
|
||||
type: "Feature",
|
||||
id,
|
||||
properties: {
|
||||
id,
|
||||
level,
|
||||
icon,
|
||||
alert,
|
||||
coordinates,
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Add initial location marker if locations are different
|
||||
list.forEach((row) => {
|
||||
const { alert } = row;
|
||||
if (
|
||||
alert.initialLocation &&
|
||||
alert.location &&
|
||||
!deepEqual(alert.initialLocation, alert.location)
|
||||
) {
|
||||
const [longitude, latitude] = alert.initialLocation.coordinates;
|
||||
const coordinates = [longitude, latitude];
|
||||
const id = `alert:${alert.id}`;
|
||||
const icon = state === "open" ? level : `${level}Disabled`;
|
||||
return {
|
||||
const id = `alert:${alert.id}:initial`;
|
||||
|
||||
features.push({
|
||||
type: "Feature",
|
||||
id,
|
||||
properties: {
|
||||
id,
|
||||
level,
|
||||
icon,
|
||||
icon: "origin",
|
||||
level: alert.level,
|
||||
alert,
|
||||
coordinates,
|
||||
isInitialLocation: true,
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates,
|
||||
},
|
||||
};
|
||||
}),
|
||||
}),
|
||||
[list],
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
type: "FeatureCollection",
|
||||
features,
|
||||
};
|
||||
}, [list]);
|
||||
|
||||
const superCluster = useShallowMemo(() => {
|
||||
const cluster = new Supercluster({ radius: 40, maxZoom: 16 });
|
||||
|
|
|
@ -50,7 +50,9 @@ function Chat() {
|
|||
name="information-outline"
|
||||
style={styles.resolvedLabelImage}
|
||||
/>
|
||||
<Text style={styles.resolvedLabelText}>Cette alerte est résolue</Text>
|
||||
<Text style={styles.resolvedLabelText}>
|
||||
Cette alerte est terminée
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{isArchived && (
|
||||
|
|
232
src/scenes/AlertCurOverview/FieldFollowLocation.js
Normal file
232
src/scenes/AlertCurOverview/FieldFollowLocation.js
Normal file
|
@ -0,0 +1,232 @@
|
|||
import { useCallback, useEffect, useState, useRef } from "react";
|
||||
import { View, AppState } from "react-native";
|
||||
import { Switch, Button } from "react-native-paper";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import * as Location from "expo-location";
|
||||
|
||||
import { UPDATE_ALERT_FOLLOW_LOCATION_MUTATION } from "./gql";
|
||||
import Text from "~/components/Text";
|
||||
import { createStyles, useTheme } from "~/theme";
|
||||
import { usePermissionsState, permissionsActions } from "~/stores";
|
||||
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
|
||||
import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground";
|
||||
import openSettings from "~/lib/native/openSettings";
|
||||
|
||||
export default function FieldFollowLocation({ alert }) {
|
||||
const styles = useStyles();
|
||||
const { colors } = useTheme();
|
||||
const { id: alertId } = alert;
|
||||
|
||||
const [updateAlertFollowLocationMutation] = useMutation(
|
||||
UPDATE_ALERT_FOLLOW_LOCATION_MUTATION,
|
||||
);
|
||||
|
||||
const { locationForeground, locationBackground } = usePermissionsState([
|
||||
"locationForeground",
|
||||
"locationBackground",
|
||||
]);
|
||||
|
||||
const [followLocation, setFollowLocation] = useState(
|
||||
alert.followLocation || false,
|
||||
);
|
||||
const [isRequestingPermissions, setIsRequestingPermissions] = useState(false);
|
||||
|
||||
const prevFollowLocation = useRef(alert.followLocation);
|
||||
useEffect(() => {
|
||||
if (prevFollowLocation.current !== alert.followLocation) {
|
||||
prevFollowLocation.current = alert.followLocation;
|
||||
setFollowLocation(alert.followLocation || false);
|
||||
}
|
||||
}, [alert.followLocation]);
|
||||
|
||||
// Check current permission status
|
||||
const checkPermissionStatus = useCallback(async () => {
|
||||
try {
|
||||
const { status: fgStatus } =
|
||||
await Location.getForegroundPermissionsAsync();
|
||||
const { status: bgStatus } =
|
||||
await Location.getBackgroundPermissionsAsync();
|
||||
|
||||
permissionsActions.setLocationForeground(fgStatus === "granted");
|
||||
permissionsActions.setLocationBackground(bgStatus === "granted");
|
||||
} catch (error) {
|
||||
console.error("Error checking location permissions:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Check permissions on mount
|
||||
useEffect(() => {
|
||||
checkPermissionStatus();
|
||||
}, [checkPermissionStatus]);
|
||||
|
||||
// Listen for app state changes to refresh permissions when returning from settings
|
||||
useEffect(() => {
|
||||
const handleAppStateChange = (nextAppState) => {
|
||||
if (nextAppState === "active") {
|
||||
// App came to foreground, check permissions
|
||||
checkPermissionStatus();
|
||||
}
|
||||
};
|
||||
|
||||
const subscription = AppState.addEventListener(
|
||||
"change",
|
||||
handleAppStateChange,
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscription?.remove();
|
||||
};
|
||||
}, [checkPermissionStatus]);
|
||||
|
||||
const requestLocationPermissions = useCallback(async () => {
|
||||
setIsRequestingPermissions(true);
|
||||
try {
|
||||
// Request foreground permission first
|
||||
const foregroundGranted = await requestPermissionLocationForeground();
|
||||
permissionsActions.setLocationForeground(foregroundGranted);
|
||||
|
||||
if (foregroundGranted) {
|
||||
// Request background permission if foreground is granted
|
||||
const backgroundGranted = await requestPermissionLocationBackground();
|
||||
permissionsActions.setLocationBackground(backgroundGranted);
|
||||
return backgroundGranted;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error requesting location permissions:", error);
|
||||
return false;
|
||||
} finally {
|
||||
setIsRequestingPermissions(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleFollowLocation = useCallback(
|
||||
async (value) => {
|
||||
if (value) {
|
||||
// Check if permissions are granted when enabling
|
||||
if (!locationForeground || !locationBackground) {
|
||||
const permissionsGranted = await requestLocationPermissions();
|
||||
if (!permissionsGranted) {
|
||||
// Don't enable if permissions weren't granted
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setFollowLocation(value);
|
||||
updateAlertFollowLocationMutation({
|
||||
variables: { alertId, followLocation: value },
|
||||
});
|
||||
},
|
||||
[
|
||||
alertId,
|
||||
updateAlertFollowLocationMutation,
|
||||
locationForeground,
|
||||
locationBackground,
|
||||
requestLocationPermissions,
|
||||
],
|
||||
);
|
||||
|
||||
const hasRequiredPermissions = locationForeground && locationBackground;
|
||||
const showPermissionWarning = followLocation && !hasRequiredPermissions;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<MaterialCommunityIcons
|
||||
name="crosshairs-gps"
|
||||
style={styles.icon}
|
||||
size={20}
|
||||
/>
|
||||
<Text style={styles.label}>Suivre ma localisation</Text>
|
||||
<Switch
|
||||
value={followLocation}
|
||||
onValueChange={toggleFollowLocation}
|
||||
color={colors.primary}
|
||||
disabled={isRequestingPermissions}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{showPermissionWarning && (
|
||||
<View style={styles.warningContainer}>
|
||||
<View style={styles.warningContent}>
|
||||
<MaterialCommunityIcons
|
||||
name="alert-circle-outline"
|
||||
style={styles.warningIcon}
|
||||
size={16}
|
||||
/>
|
||||
<Text style={styles.warningText}>
|
||||
Permissions de localisation requises
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={openSettings}
|
||||
style={styles.settingsButton}
|
||||
labelStyle={styles.settingsButtonLabel}
|
||||
compact
|
||||
>
|
||||
Paramétrer
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = createStyles(
|
||||
({ wp, hp, scaleText, fontSize, theme: { colors } }) => ({
|
||||
container: {
|
||||
marginVertical: hp(1),
|
||||
paddingHorizontal: wp(4),
|
||||
paddingVertical: hp(1.5),
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.outline,
|
||||
},
|
||||
content: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
icon: {
|
||||
color: colors.primary,
|
||||
marginRight: wp(2),
|
||||
},
|
||||
label: {
|
||||
flex: 1,
|
||||
...scaleText({ fontSize: 16 }),
|
||||
color: colors.onSurface,
|
||||
},
|
||||
warningContainer: {
|
||||
marginTop: hp(1),
|
||||
paddingTop: hp(1),
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.outline,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
warningContent: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flex: 1,
|
||||
},
|
||||
warningIcon: {
|
||||
color: colors.error,
|
||||
marginRight: wp(1),
|
||||
},
|
||||
warningText: {
|
||||
...scaleText({ fontSize: 14 }),
|
||||
color: colors.error,
|
||||
flex: 1,
|
||||
},
|
||||
settingsButton: {
|
||||
marginLeft: wp(2),
|
||||
},
|
||||
settingsButtonLabel: {
|
||||
...scaleText({ fontSize: 12 }),
|
||||
},
|
||||
}),
|
||||
);
|
|
@ -60,3 +60,17 @@ export const UPDATE_ALERT_SUBJECT_MUTATION = gql`
|
|||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_ALERT_FOLLOW_LOCATION_MUTATION = gql`
|
||||
mutation updateAlertFollowLocation(
|
||||
$alertId: Int!
|
||||
$followLocation: Boolean!
|
||||
) {
|
||||
updateOneAlert(
|
||||
pk_columns: { id: $alertId }
|
||||
_set: { followLocation: $followLocation }
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useCallback, useState } from "react";
|
||||
import React, { useCallback, useState, useMemo } from "react";
|
||||
import { View, ImageBackground, ScrollView } from "react-native";
|
||||
import { TouchableRipple, Button, Title } from "react-native-paper";
|
||||
import { useIsFocused, useNavigation } from "@react-navigation/native";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { deepEqual } from "fast-equals";
|
||||
|
||||
import withConnectivity from "~/hoc/withConnectivity";
|
||||
|
||||
|
@ -21,6 +22,7 @@ import alertBigButtonBgMessagesGrey from "~/assets/img/alert-big-button-bg-messa
|
|||
|
||||
import Text from "~/components/Text";
|
||||
import AlertInfoLineLevel from "~/containers/AlertInfoLines/Level";
|
||||
import LocationInfoSection from "~/containers/LocationInfoSection";
|
||||
import AlertInfoLineCode from "~/containers/AlertInfoLines/Code";
|
||||
import AlertInfoLineDistance from "~/containers/AlertInfoLines/Distance";
|
||||
import AlertInfoLineCreatedTime from "~/containers/AlertInfoLines/CreatedTime";
|
||||
|
@ -50,6 +52,7 @@ import { useMutation } from "@apollo/client";
|
|||
|
||||
import FieldLevel from "./FieldLevel";
|
||||
import FieldSubject from "./FieldSubject";
|
||||
import FieldFollowLocation from "./FieldFollowLocation";
|
||||
import MapLinksButton from "~/containers/MapLinksButton";
|
||||
import { useTheme } from "~/theme";
|
||||
|
||||
|
@ -197,6 +200,14 @@ export default withConnectivity(
|
|||
|
||||
const [parentScrollEnabled, setParentScrollEnabled] = useState(true);
|
||||
|
||||
const isLocationsDifferent = useMemo(() => {
|
||||
return (
|
||||
alert.initialLocation &&
|
||||
alert.location &&
|
||||
!deepEqual(alert.initialLocation, alert.location)
|
||||
);
|
||||
}, [alert.initialLocation, alert.location]);
|
||||
|
||||
// if (!isFocused) {
|
||||
// return null;
|
||||
// }
|
||||
|
@ -497,6 +508,8 @@ export default withConnectivity(
|
|||
alert={alert}
|
||||
setParentScrollEnabled={setParentScrollEnabled}
|
||||
/>
|
||||
|
||||
<FieldFollowLocation alert={alert} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
@ -505,15 +518,49 @@ export default withConnectivity(
|
|||
<AlertInfoLineLevel alert={alert} isFirst />
|
||||
)}
|
||||
<AlertInfoLineCode alert={alert} />
|
||||
<AlertInfoLineDistance alert={alert} />
|
||||
{!isSent && <AlertInfoLineDistance alert={alert} />}
|
||||
<AlertInfoLineCreatedTime alert={alert} />
|
||||
<AlertInfoLineClosedTime alert={alert} />
|
||||
{(!isSent || !isOpen) && <AlertInfoLineSubject alert={alert} />}
|
||||
<AlertInfoLineAddress alert={alert} />
|
||||
<AlertInfoLineNear alert={alert} />
|
||||
<AlertInfoLineW3w alert={alert} />
|
||||
<AlertInfoLineRadius alert={alert} />
|
||||
<AlertInfoLineSentBy alert={alert} />
|
||||
|
||||
{useMemo(() => {
|
||||
if (isLocationsDifferent) {
|
||||
return (
|
||||
<>
|
||||
<LocationInfoSection
|
||||
title="Position initiale"
|
||||
alert={alert}
|
||||
styles={styles}
|
||||
/>
|
||||
<LocationInfoSection
|
||||
title={
|
||||
alert.followLocation
|
||||
? "Position actuelle"
|
||||
: "Dernière position connue"
|
||||
}
|
||||
alert={alert}
|
||||
useLastLocation={true}
|
||||
styles={styles}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<AlertInfoLineAddress alert={alert} />
|
||||
<AlertInfoLineNear alert={alert} />
|
||||
<AlertInfoLineW3w alert={alert} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}, [alert, styles, isLocationsDifferent])}
|
||||
|
||||
<View
|
||||
style={isLocationsDifferent ? styles.locationSeparator : null}
|
||||
>
|
||||
<AlertInfoLineRadius alert={alert} />
|
||||
<AlertInfoLineSentBy alert={alert} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
|
|
@ -153,4 +153,20 @@ export default createStyles(({ wp, hp, scaleText, theme: { colors } }) => ({
|
|||
lineHeight: 18,
|
||||
textAlign: "center",
|
||||
},
|
||||
locationSectionTitle: {
|
||||
backgroundColor: colors.background,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 10,
|
||||
marginTop: 5,
|
||||
},
|
||||
locationTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
color: colors.primary,
|
||||
},
|
||||
locationSeparator: {
|
||||
marginTop: 15,
|
||||
borderTopWidth: 5,
|
||||
borderColor: colors.background,
|
||||
},
|
||||
}));
|
||||
|
|
282
src/scenes/Contribute/index.js
Normal file
282
src/scenes/Contribute/index.js
Normal file
|
@ -0,0 +1,282 @@
|
|||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
ScrollView,
|
||||
Linking,
|
||||
Alert,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
import { MaterialIcons, AntDesign } from "@expo/vector-icons";
|
||||
import Text from "~/components/Text";
|
||||
|
||||
import { createStyles, useTheme } from "~/theme";
|
||||
|
||||
const openURL = async (url) => {
|
||||
try {
|
||||
const supported = await Linking.canOpenURL(url);
|
||||
if (supported) {
|
||||
await Linking.openURL(url);
|
||||
} else {
|
||||
Alert.alert("Erreur", "Impossible d'ouvrir ce lien");
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
"Erreur",
|
||||
"Une erreur s'est produite lors de l'ouverture du lien",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function Contribute() {
|
||||
const styles = useStyles();
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<ScrollView style={{ flex: 1 }}>
|
||||
<View style={styles.container}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<MaterialIcons name="favorite" size={28} color={colors.primary} />
|
||||
<Text style={styles.title}>Contribuer au projet</Text>
|
||||
</View>
|
||||
|
||||
{/* Description */}
|
||||
<Text style={styles.description}>
|
||||
Alerte-Secours est une application mobile citoyenne, gratuite, sans
|
||||
publicité ni exploitation de données.
|
||||
{"\n\n"}
|
||||
Si vous souhaitez contribuer à son développement, sa maintenance et
|
||||
son indépendance :
|
||||
</Text>
|
||||
|
||||
{/* Liberapay Button */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.donationButton,
|
||||
styles.buttonContent,
|
||||
styles.liberapayButton,
|
||||
]}
|
||||
onPress={() => openURL("https://liberapay.com/alerte-secours")}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={styles.iconContainer}>
|
||||
<MaterialIcons name="circle" style={styles.iconDonation} />
|
||||
</View>
|
||||
<View style={styles.buttonTextContainer}>
|
||||
<Text style={styles.buttonLabel}>Liberapay – Soutien régulier</Text>
|
||||
<Text style={styles.buttonDescription}>
|
||||
Pour un soutien récurrent et engagé. Chaque don contribue à
|
||||
assurer la stabilité du service sur le long terme.
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Buy Me a Coffee Button */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.donationButton,
|
||||
styles.buttonContent,
|
||||
styles.buymeacoffeeButton,
|
||||
]}
|
||||
onPress={() => openURL("https://buymeacoffee.com/alertesecours")}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={styles.iconContainer}>
|
||||
<MaterialIcons name="local-cafe" style={styles.iconDonation} />
|
||||
</View>
|
||||
<View style={styles.buttonTextContainer}>
|
||||
<Text style={styles.buttonLabel}>
|
||||
Buy Me a Coffee – Don ponctuel
|
||||
</Text>
|
||||
<Text style={styles.buttonDescription}>
|
||||
Pour un coup de pouce ponctuel, un café virtuel pour encourager le
|
||||
travail accompli !
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* GitHub Sponsors Button */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.donationButton,
|
||||
styles.buttonContent,
|
||||
styles.githubButton,
|
||||
]}
|
||||
onPress={() => openURL("https://github.com/sponsors/alerte-secours")}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={styles.iconContainer}>
|
||||
<AntDesign name="github" style={styles.iconDonation} />
|
||||
</View>
|
||||
<View style={styles.buttonTextContainer}>
|
||||
<Text style={styles.buttonLabel}>GitHub Sponsors</Text>
|
||||
<Text style={styles.buttonDescription}>
|
||||
Pour les développeurs et utilisateurs de GitHub : soutenez le
|
||||
projet directement via votre compte.
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Collaboration Section */}
|
||||
<View style={styles.collaborationSection}>
|
||||
<Text style={styles.collaborationTitle}>
|
||||
Vous souhaitez nous rejoindre
|
||||
</Text>
|
||||
<Text style={styles.collaborationDescription}>
|
||||
Ce projet fait sens pour vous et vous souhaitez vous engager dans un
|
||||
projet d'avenir ?{"\n\n"}
|
||||
Alerte-Secours est à la recherche de collaborateurs avec des
|
||||
compétences dans le développement, la gestion de projet, le
|
||||
business, la com, le graphisme. Contactez-nous si vous souhaitez
|
||||
faire partie de l'aventure.
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.contactButton}
|
||||
onPress={() => openURL("mailto:contact@alertesecours.fr")}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<MaterialIcons name="email" style={styles.contactIcon} />
|
||||
<Text style={styles.contactText}>contact@alertesecours.fr</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = createStyles(({ theme: { colors, custom } }) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 24,
|
||||
},
|
||||
title: {
|
||||
fontSize: 26,
|
||||
fontWeight: "bold",
|
||||
color: colors.primary,
|
||||
marginLeft: 12,
|
||||
},
|
||||
description: {
|
||||
fontSize: 16,
|
||||
lineHeight: 26,
|
||||
color: colors.onBackground,
|
||||
marginBottom: 36,
|
||||
textAlign: "left",
|
||||
},
|
||||
donationButton: {
|
||||
marginVertical: 10,
|
||||
borderRadius: 16,
|
||||
elevation: 3,
|
||||
shadowColor: colors.shadow,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
buttonContent: {
|
||||
minHeight: 64,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 20,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
buttonLabel: {
|
||||
fontSize: 17,
|
||||
fontWeight: "700",
|
||||
textAlign: "left",
|
||||
marginBottom: 4,
|
||||
color: custom.donation.onDonation,
|
||||
},
|
||||
buttonDescription: {
|
||||
fontSize: 14,
|
||||
opacity: 0.9,
|
||||
textAlign: "left",
|
||||
lineHeight: 20,
|
||||
color: custom.donation.onDonation,
|
||||
},
|
||||
iconContainer: {
|
||||
marginRight: 18,
|
||||
width: 32,
|
||||
height: 32,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 16,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
||||
},
|
||||
buttonTextContainer: {
|
||||
flex: 1,
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
// Icon styles
|
||||
iconDonation: {
|
||||
fontSize: 22,
|
||||
color: custom.donation.onDonation,
|
||||
},
|
||||
// Button background styles
|
||||
liberapayButton: {
|
||||
backgroundColor: custom.donation.liberapay,
|
||||
},
|
||||
buymeacoffeeButton: {
|
||||
backgroundColor: custom.donation.buymeacoffee,
|
||||
},
|
||||
githubButton: {
|
||||
backgroundColor: custom.donation.github,
|
||||
},
|
||||
// Collaboration section styles
|
||||
collaborationSection: {
|
||||
marginTop: 40,
|
||||
padding: 20,
|
||||
backgroundColor: colors.surfaceVariant,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.outline,
|
||||
},
|
||||
collaborationTitle: {
|
||||
fontSize: 22,
|
||||
fontWeight: "bold",
|
||||
color: colors.primary,
|
||||
marginBottom: 16,
|
||||
textAlign: "center",
|
||||
},
|
||||
collaborationDescription: {
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
color: colors.onSurfaceVariant,
|
||||
marginBottom: 20,
|
||||
textAlign: "left",
|
||||
},
|
||||
contactButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 12,
|
||||
elevation: 2,
|
||||
shadowColor: colors.shadow,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 1,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
contactIcon: {
|
||||
fontSize: 20,
|
||||
color: colors.onPrimary,
|
||||
marginRight: 8,
|
||||
},
|
||||
contactText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: colors.onPrimary,
|
||||
},
|
||||
}));
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { View, StyleSheet, ScrollView } from "react-native";
|
||||
import * as Sentry from "@sentry/react-native";
|
||||
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
|
@ -8,10 +9,18 @@ import {
|
|||
Text,
|
||||
useTheme,
|
||||
Divider,
|
||||
RadioButton,
|
||||
} from "react-native-paper";
|
||||
import { createStyles } from "~/theme";
|
||||
import env, { setStaging } from "~/env";
|
||||
import { authActions } from "~/stores";
|
||||
import {
|
||||
getEmulatorModeState,
|
||||
toggleEmulatorMode as toggleEmulatorModeService,
|
||||
initEmulatorMode,
|
||||
} from "~/location/emulatorService";
|
||||
import { LOG_LEVELS, setMinLogLevel } from "~/lib/logger";
|
||||
import { config as loggerConfig } from "~/lib/logger/config";
|
||||
|
||||
const reset = async () => {
|
||||
await authActions.logout();
|
||||
|
@ -29,7 +38,60 @@ const Section = ({ title, children }) => {
|
|||
|
||||
export default function Developer() {
|
||||
const styles = useStyles();
|
||||
const { colors } = useTheme();
|
||||
const [isStaging, setIsStaging] = useState(env.IS_STAGING);
|
||||
const [emulatorMode, setEmulatorMode] = useState(false);
|
||||
const [syncStatus, setSyncStatus] = useState(null); // null, 'syncing', 'success', 'error'
|
||||
const [syncResult, setSyncResult] = useState("");
|
||||
const [logLevel, setLogLevel] = useState(LOG_LEVELS.DEBUG);
|
||||
|
||||
// Initialize emulator mode and log level when component mounts
|
||||
useEffect(() => {
|
||||
// Initialize the emulator service
|
||||
initEmulatorMode();
|
||||
|
||||
// Set the initial state based on the global service
|
||||
setEmulatorMode(getEmulatorModeState());
|
||||
|
||||
// Set the initial log level from config
|
||||
setLogLevel(loggerConfig.minLevel);
|
||||
}, []);
|
||||
|
||||
// Handle log level change
|
||||
const handleLogLevelChange = (level) => {
|
||||
setLogLevel(level);
|
||||
setMinLogLevel(level);
|
||||
};
|
||||
|
||||
// Handle toggling emulator mode
|
||||
const handleEmulatorModeToggle = async (enabled) => {
|
||||
const newState = await toggleEmulatorModeService(enabled);
|
||||
setEmulatorMode(newState);
|
||||
};
|
||||
|
||||
// Function to trigger geolocation sync
|
||||
const triggerGeolocSync = async () => {
|
||||
try {
|
||||
setSyncStatus("syncing");
|
||||
setSyncResult("");
|
||||
|
||||
// Get the count of pending records first
|
||||
const count = await BackgroundGeolocation.getCount();
|
||||
|
||||
// Perform the sync
|
||||
const records = await BackgroundGeolocation.sync();
|
||||
|
||||
const result = `Synced ${
|
||||
records?.length || 0
|
||||
} records (${count} pending)`;
|
||||
setSyncResult(result);
|
||||
setSyncStatus("success");
|
||||
} catch (error) {
|
||||
console.error("Geolocation sync failed:", error);
|
||||
setSyncResult(`Sync failed: ${error.message}`);
|
||||
setSyncStatus("error");
|
||||
}
|
||||
};
|
||||
const triggerNullError = () => {
|
||||
try {
|
||||
// Wrap the null error in try-catch
|
||||
|
@ -95,12 +157,46 @@ export default function Developer() {
|
|||
<Switch
|
||||
value={isStaging}
|
||||
onValueChange={async (value) => {
|
||||
setStaging(value);
|
||||
setIsStaging(value);
|
||||
await reset();
|
||||
setIsStaging(value); // Update UI immediately
|
||||
await setStaging(value); // Persist the change
|
||||
await reset(); // Reset auth state
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.settingRow}>
|
||||
<Text variant="bodyLarge">Emulator Mode</Text>
|
||||
<Switch
|
||||
value={emulatorMode}
|
||||
onValueChange={handleEmulatorModeToggle}
|
||||
/>
|
||||
</View>
|
||||
</Section>
|
||||
|
||||
<Section title="Logging Controls">
|
||||
<Text variant="bodyLarge" style={styles.sectionLabel}>
|
||||
Log Level
|
||||
</Text>
|
||||
<RadioButton.Group
|
||||
onValueChange={handleLogLevelChange}
|
||||
value={logLevel}
|
||||
>
|
||||
<View style={styles.radioRow}>
|
||||
<RadioButton value={LOG_LEVELS.DEBUG} />
|
||||
<Text variant="bodyMedium">DEBUG</Text>
|
||||
</View>
|
||||
<View style={styles.radioRow}>
|
||||
<RadioButton value={LOG_LEVELS.INFO} />
|
||||
<Text variant="bodyMedium">INFO</Text>
|
||||
</View>
|
||||
<View style={styles.radioRow}>
|
||||
<RadioButton value={LOG_LEVELS.WARN} />
|
||||
<Text variant="bodyMedium">WARN</Text>
|
||||
</View>
|
||||
<View style={styles.radioRow}>
|
||||
<RadioButton value={LOG_LEVELS.ERROR} />
|
||||
<Text variant="bodyMedium">ERROR</Text>
|
||||
</View>
|
||||
</RadioButton.Group>
|
||||
</Section>
|
||||
|
||||
<Section title="Environment URLs">
|
||||
|
@ -150,6 +246,35 @@ export default function Developer() {
|
|||
|
||||
<Divider style={styles.divider} />
|
||||
|
||||
<Section title="Location Controls">
|
||||
<Button
|
||||
onPress={triggerGeolocSync}
|
||||
style={styles.button}
|
||||
contentStyle={styles.buttonContent}
|
||||
labelStyle={styles.buttonLabel}
|
||||
loading={syncStatus === "syncing"}
|
||||
disabled={syncStatus === "syncing"}
|
||||
>
|
||||
Trigger Geolocation Sync
|
||||
</Button>
|
||||
|
||||
{syncStatus && syncResult && (
|
||||
<Text
|
||||
style={[
|
||||
styles.statusText,
|
||||
{
|
||||
color: syncStatus === "success" ? colors.primary : colors.error,
|
||||
marginTop: 8,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{syncResult}
|
||||
</Text>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Divider style={styles.divider} />
|
||||
|
||||
<Section title="Error Testing">
|
||||
<Button
|
||||
onPress={triggerNullError}
|
||||
|
@ -254,4 +379,12 @@ const useStyles = createStyles(({ wp, hp, scaleText, theme: { colors } }) => ({
|
|||
flex: 1,
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
radioRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginVertical: 2,
|
||||
},
|
||||
sectionLabel: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
}));
|
||||
|
|
58
src/scenes/SendAlert/ContributeButton.js
Normal file
58
src/scenes/SendAlert/ContributeButton.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import React from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { createStyles } from "~/theme";
|
||||
import Text from "~/components/Text";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
|
||||
export default function ContributeButton() {
|
||||
const navigation = useNavigation();
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={() => navigation.navigate("Contribute")}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="favorite"
|
||||
size={24}
|
||||
color={styles.icon.color}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text style={styles.buttonText}>Contribuer au projet</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = createStyles(({ theme: { colors } }) => ({
|
||||
button: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
alignSelf: "center",
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: 8,
|
||||
width: "100%",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
shadowColor: colors.shadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
},
|
||||
buttonText: {
|
||||
color: colors.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
marginLeft: 5,
|
||||
},
|
||||
icon: {
|
||||
color: colors.primary,
|
||||
marginRight: 10,
|
||||
},
|
||||
}));
|
|
@ -12,6 +12,7 @@ import { createStyles, fontFamily } from "~/theme";
|
|||
import HelpBlock from "./HelpBlock";
|
||||
import RegisterRelativesButton from "./RegisterRelativesButton";
|
||||
import NotificationsButton from "./NotificationsButton";
|
||||
import ContributeButton from "./ContributeButton";
|
||||
|
||||
export default function SendAlert() {
|
||||
const navigation = useNavigation();
|
||||
|
@ -215,6 +216,8 @@ export default function SendAlert() {
|
|||
{capitalize(levelLabel.call)}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<ContributeButton />
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
|
|
|
@ -14,7 +14,7 @@ export default function SACFieldAlert(props) {
|
|||
setValue("notifyRelatives", true);
|
||||
break;
|
||||
case "yellow":
|
||||
setValue("callEmergency", false);
|
||||
setValue("callEmergency", true);
|
||||
setValue("notifyRelatives", true);
|
||||
break;
|
||||
case "green":
|
||||
|
|
|
@ -22,6 +22,7 @@ export default function FieldNotifySelector() {
|
|||
const callEmergency = watch("callEmergency");
|
||||
const notifyAround = watch("notifyAround");
|
||||
const notifyRelatives = watch("notifyRelatives");
|
||||
const followLocation = watch("followLocation");
|
||||
const level = watch("level");
|
||||
|
||||
const checkedColor = colors.primary;
|
||||
|
@ -106,6 +107,29 @@ export default function FieldNotifySelector() {
|
|||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.followLocationContainer}>
|
||||
<CheckboxItem
|
||||
status={followLocation ? "checked" : "unchecked"}
|
||||
style={styles.checkboxItem}
|
||||
labelStyle={styles.checkboxLabel}
|
||||
size={styleOptions.checkboxItem.size}
|
||||
icon={() => (
|
||||
<MaterialCommunityIcons
|
||||
name="crosshairs-gps"
|
||||
style={styles.checkboxIcon}
|
||||
onPress={() => {
|
||||
setValue("followLocation", !followLocation);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
color={checkedColor}
|
||||
uncheckedColor={uncheckedColor}
|
||||
label="Suivre ma localisation"
|
||||
onPress={() => {
|
||||
setValue("followLocation", !followLocation);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -122,6 +146,7 @@ const useStyles = createStyles(
|
|||
({ wp, hp, scaleText, fontSize, theme: { colors, textShadowForWhite } }) => ({
|
||||
container: {
|
||||
marginTop: hp(2),
|
||||
marginBottom: hp(3),
|
||||
},
|
||||
checkboxItemContainer: {
|
||||
borderRadius: 4,
|
||||
|
@ -130,6 +155,13 @@ const useStyles = createStyles(
|
|||
marginVertical: hp(0.2),
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
followLocationContainer: {
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.outline,
|
||||
marginVertical: hp(4),
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
checkboxItem: {
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
|
|
|
@ -9,7 +9,8 @@ export default function SendAlertConfirm({ route }) {
|
|||
|
||||
const { alert } = params;
|
||||
const level = alert?.level || params.level;
|
||||
const callEmergency = params.forceCallEmergency || level === "red";
|
||||
const callEmergency =
|
||||
params.forceCallEmergency || level === "red" || level === "yellow";
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues: {
|
||||
|
@ -18,6 +19,7 @@ export default function SendAlertConfirm({ route }) {
|
|||
callEmergency,
|
||||
notifyAround: true,
|
||||
notifyRelatives: true,
|
||||
followLocation: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -33,8 +33,14 @@ async function onSubmit(args, context) {
|
|||
const [alertInput] = args;
|
||||
const { navigation } = context;
|
||||
|
||||
const { subject, level, callEmergency, notifyAround, notifyRelatives } =
|
||||
alertInput;
|
||||
const {
|
||||
subject,
|
||||
level,
|
||||
callEmergency,
|
||||
notifyAround,
|
||||
notifyRelatives,
|
||||
followLocation,
|
||||
} = alertInput;
|
||||
|
||||
const coords = await getCurrentLocation();
|
||||
|
||||
|
@ -62,6 +68,7 @@ async function onSubmit(args, context) {
|
|||
callEmergency,
|
||||
notifyAround,
|
||||
notifyRelatives,
|
||||
followLocation: !!followLocation,
|
||||
location,
|
||||
accuracy,
|
||||
altitude,
|
||||
|
|
|
@ -132,14 +132,43 @@ export default createAtom(({ get, merge, getActions }) => {
|
|||
|
||||
const reload = async () => {
|
||||
authLogger.info("Reloading auth state");
|
||||
|
||||
// Check if we're already reloading or in a loading state
|
||||
const { isReloading, lastReloadTime } = get();
|
||||
const now = Date.now();
|
||||
const timeSinceLastReload = now - lastReloadTime;
|
||||
const RELOAD_COOLDOWN = 2000; // 2 seconds cooldown
|
||||
|
||||
if (isReloading) {
|
||||
authLogger.info("Auth reload already in progress, skipping");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (timeSinceLastReload < RELOAD_COOLDOWN) {
|
||||
authLogger.info("Auth reload requested too soon, skipping", {
|
||||
timeSinceLastReload,
|
||||
cooldown: RELOAD_COOLDOWN,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isLoading()) {
|
||||
await loadingPromise;
|
||||
return true;
|
||||
}
|
||||
startLoading();
|
||||
await secureStore.deleteItemAsync("userToken");
|
||||
await init();
|
||||
return true;
|
||||
|
||||
// Set reloading state
|
||||
merge({ isReloading: true, lastReloadTime: now });
|
||||
|
||||
try {
|
||||
startLoading();
|
||||
await secureStore.deleteItemAsync("userToken");
|
||||
await init();
|
||||
return true;
|
||||
} finally {
|
||||
// Clear reloading state even if there was an error
|
||||
merge({ isReloading: false });
|
||||
}
|
||||
};
|
||||
|
||||
const onReload = async () => {
|
||||
|
@ -247,6 +276,8 @@ export default createAtom(({ get, merge, getActions }) => {
|
|||
onReload: false,
|
||||
onReloadAuthToken: null,
|
||||
userOffMode: false,
|
||||
isReloading: false,
|
||||
lastReloadTime: 0,
|
||||
},
|
||||
actions: {
|
||||
init,
|
||||
|
|
|
@ -49,15 +49,21 @@ export default createAtom(({ merge, getActions }) => {
|
|||
merge({ suspend: true });
|
||||
};
|
||||
|
||||
const splashScreenHidden = () => {
|
||||
merge({ splashScreenHidden: true });
|
||||
};
|
||||
|
||||
return {
|
||||
default: {
|
||||
triggerReload: false,
|
||||
suspend: false,
|
||||
splashScreenHidden: false,
|
||||
},
|
||||
actions: {
|
||||
triggerReload,
|
||||
suspendTree,
|
||||
onReload,
|
||||
splashScreenHidden,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -96,6 +96,13 @@ const ThemeDark = {
|
|||
call: "#4c6ef5",
|
||||
onColor: "#FFFFFF",
|
||||
},
|
||||
|
||||
donation: {
|
||||
liberapay: "#f59f00", // Same as colors.warn
|
||||
buymeacoffee: "#40c057", // Same as colors.ok
|
||||
github: "#b3c4ff", // Same as colors.primary (dark theme)
|
||||
onDonation: "#FFFFFF",
|
||||
},
|
||||
},
|
||||
};
|
||||
export default ThemeDark;
|
||||
|
|
|
@ -95,6 +95,13 @@ const ThemeLight = {
|
|||
call: "#4c6ef5",
|
||||
onColor: "#FFFFFF",
|
||||
},
|
||||
|
||||
donation: {
|
||||
liberapay: "#f59f00", // Same as colors.warn
|
||||
buymeacoffee: "#40c057", // Same as colors.ok
|
||||
github: "#1864ab", // Same as colors.primary
|
||||
onDonation: "#FFFFFF",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue