(doc) : ajout et modification de docs après arbitrage
This commit is contained in:
110
TECHNICAL.md
110
TECHNICAL.md
@@ -106,51 +106,77 @@ TTL cache : 5 minutes (le contenu ne bouge pas).
|
||||
|
||||
## Architecture Services
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ OVH VPS (Gravelines, France) │
|
||||
│ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ NGINX Cache │ Cache HLS │
|
||||
│ │ + Let's Encrypt│ SSL, rate limiting │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────┴────────┐ │
|
||||
│ │ API Gateway │ Go + Fiber │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ┌────┴────┬─────────────┬──────────┐ │
|
||||
│ │ │ │ │ │
|
||||
│ ┌───▼───┐ ┌──▼───┐ ┌───────▼────┐ ┌──▼─────┐
|
||||
│ │ Auth │ │ User │ │Content/Geo │ │Zitadel │
|
||||
│ │Service│ │Svc │ │ Service │ │ IdP │
|
||||
│ └───┬───┘ └──┬───┘ └──────┬─────┘ └───┬────┘
|
||||
│ │ │ │ │ │
|
||||
│ └────────┴────────────┴───────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────┴──────────┐ │
|
||||
│ │ │ │
|
||||
│ ┌────▼────┐ ┌──────▼──────┐ │
|
||||
│ │ Redis │ │ PostgreSQL │ │
|
||||
│ │ Cluster │ │ + PostGIS │ │
|
||||
│ └─────────┘ │ │ │
|
||||
│ │ Schémas: │ │
|
||||
│ │ - roadwave │ │
|
||||
│ │ - zitadel │ │
|
||||
│ └─────────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────┴──────────┐
|
||||
│ │
|
||||
┌────────▼────────┐ ┌────────▼────────┐
|
||||
│ OVH Object │ │ Mobile Apps │
|
||||
│ Storage (S3) │ │ iOS/Android │
|
||||
│ Fichiers audio │ │ │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph clients["Clients"]
|
||||
mobile["Mobile Apps<br/>iOS/Android<br/>Flutter"]
|
||||
carplay["CarPlay /<br/>Android Auto"]
|
||||
end
|
||||
|
||||
Souveraineté : 100% données en France
|
||||
subgraph ovh["OVH VPS Essential (Gravelines, France)"]
|
||||
nginx["NGINX Cache<br/>+ Let's Encrypt<br/>TLS 1.3, Rate Limiting"]
|
||||
api["API Gateway<br/>Go + Fiber :8080"]
|
||||
|
||||
subgraph services["Backend Services (Monolithe Modulaire)"]
|
||||
auth["Auth Service<br/>JWT validation"]
|
||||
user["User Service<br/>Profils, Jauges"]
|
||||
content["Content/Geo Service<br/>Recommandations<br/>PostGIS queries"]
|
||||
streaming["Streaming Service<br/>HLS generation"]
|
||||
payment["Payment Service<br/>Mangopay integration"]
|
||||
notif["Notification Service<br/>FCM/APNS"]
|
||||
end
|
||||
|
||||
zitadel["Zitadel IdP<br/>OAuth2 PKCE<br/>:8081"]
|
||||
ip2loc["IP2Location DB<br/>SQLite ~50MB<br/>Mode dégradé"]
|
||||
|
||||
subgraph data["Données"]
|
||||
pgbouncer["PgBouncer<br/>Connection pooling<br/>:6432"]
|
||||
postgres["PostgreSQL 16<br/>+ PostGIS 3.4<br/>Schémas:<br/>- roadwave<br/>- zitadel"]
|
||||
redis["Redis 7 Cluster<br/>Cache + Geospatial<br/>GEORADIUS"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph external["Services Externes"]
|
||||
storage["OVH Object Storage<br/>Fichiers audio HLS"]
|
||||
mangopay["Mangopay<br/>Paiements, KYC"]
|
||||
brevo["Brevo<br/>Emails transactionnels"]
|
||||
fcm["FCM / APNS<br/>Push notifications"]
|
||||
end
|
||||
|
||||
mobile --> nginx
|
||||
carplay --> nginx
|
||||
nginx --> api
|
||||
api --> auth
|
||||
api --> user
|
||||
api --> content
|
||||
api --> streaming
|
||||
api --> payment
|
||||
api --> notif
|
||||
api --> ip2loc
|
||||
|
||||
auth --> zitadel
|
||||
user --> pgbouncer
|
||||
user --> redis
|
||||
content --> pgbouncer
|
||||
content --> redis
|
||||
streaming --> storage
|
||||
payment --> mangopay
|
||||
notif --> fcm
|
||||
|
||||
zitadel --> pgbouncer
|
||||
pgbouncer --> postgres
|
||||
|
||||
brevo -.email.-> mobile
|
||||
fcm -.push.-> mobile
|
||||
|
||||
style ovh fill:#e3f2fd
|
||||
style external fill:#fff3e0
|
||||
style clients fill:#f3e5f5
|
||||
style data fill:#e8f5e9
|
||||
```
|
||||
|
||||
**Souveraineté** : 100% données en France (RGPD compliant)
|
||||
|
||||
---
|
||||
|
||||
## Scaling 10M Utilisateurs
|
||||
|
||||
@@ -14,9 +14,9 @@ Cette analyse a identifié **15 incohérences** entre les décisions d'architect
|
||||
|
||||
| Sévérité | Nombre | % Total | Statut | Action Required |
|
||||
|----------|--------|---------|--------|-----------------|
|
||||
| 🔴 **CRITICAL** | 2 | 13% | ✅ **RÉSOLU** | ~~avant implémentation~~ |
|
||||
| 🟠 **HIGH** | 4 | 27% | ⏳ 3 restants (1 résolu) | Résolution Sprint 1-2 |
|
||||
| 🟡 **MODERATE** | 8 | 53% | ⏳ En cours | Résolution Sprint 3-5 |
|
||||
| 🔴 **CRITICAL** | 2 | 14% | ✅ **RÉSOLU** | ~~avant implémentation~~ |
|
||||
| 🟠 **HIGH** | 2 | 14% | ✅ **RÉSOLU** (2 résolus, 1 annulé) | ~~Résolution Sprint 1-2~~ |
|
||||
| 🟡 **MODERATE** | 6 | 43% | ⏳ 4 restants (3 résolus) | Résolution Sprint 3-5 |
|
||||
| 🟢 **LOW** | 1 | 7% | ⏳ En cours | À clarifier lors du développement |
|
||||
|
||||
### Impact par Domaine
|
||||
@@ -24,7 +24,7 @@ Cette analyse a identifié **15 incohérences** entre les décisions d'architect
|
||||
| Domaine | Nombre d'incohérences | Criticité maximale |
|
||||
|---------|----------------------|-------------------|
|
||||
| Streaming & Géolocalisation | 3 | 🔴 CRITICAL |
|
||||
| Données & Infrastructure | 3 | 🟠 HIGH |
|
||||
| Données & Infrastructure | 2 | 🟠 HIGH |
|
||||
| Authentification & Sécurité | 2 | 🟠 HIGH |
|
||||
| Tests & Qualité | 2 | 🟡 MODERATE |
|
||||
| Coûts & Déploiement | 3 | 🟡 MODERATE |
|
||||
@@ -134,106 +134,94 @@ Résultat: Audio démarre 200m APRÈS le point ❌
|
||||
|
||||
### #4 : ORM sqlc vs Types PostGIS
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (ADR-013 mis à jour)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concernés** | ADR-013 (ORM, lignes 12, 33-40), ADR-005 (BDD, lignes 47-56) |
|
||||
| **ADR concerné** | ADR-013 (section "Gestion des Types PostGIS") |
|
||||
| **Règle métier** | N/A (problème technique pur) |
|
||||
| **Conflit** | sqlc génère types Go depuis SQL, mais PostGIS geography/geometry ne mappent pas proprement |
|
||||
| **Impact** | Risque de type `interface{}` ou `[]byte` pour géographie → perte de type safety revendiquée |
|
||||
|
||||
**Nature du problème** :
|
||||
**Solution implémentée** :
|
||||
|
||||
sqlc génère du code Go depuis SQL, mais les types PostGIS (`geography`, `geometry`) ne sont pas mappés proprement en Go. Résultat : types opaques (`[]byte`, `interface{}`) qui perdent la **type safety** revendiquée dans ADR-013.
|
||||
|
||||
**Solution retenue** :
|
||||
**Wrappers typés + fonctions de conversion PostGIS** :
|
||||
|
||||
1. **Wrapper types Go** avec méthodes `Scan/Value` pour conversion automatique
|
||||
2. **Utiliser les fonctions PostGIS de conversion** :
|
||||
- `ST_AsGeoJSON()` → struct GeoJSON typée
|
||||
- `ST_AsText()` → string WKT
|
||||
- `geography` brut → `pgtype.Point` (lib pgx)
|
||||
3. **Documenter le pattern** dans ADR-013 section "Gestion des Types PostGIS"
|
||||
2. **Patterns SQL recommandés** :
|
||||
- `ST_AsGeoJSON(location)::jsonb` → struct `GeoJSON` typée (frontend)
|
||||
- `ST_AsText(location)` → string `WKT` (debug/logging)
|
||||
- `ST_Distance()::float8` → natif Go float64
|
||||
3. **Index GIST** sur colonnes géographiques pour performance
|
||||
4. **Architecture conversion** :
|
||||
```
|
||||
SQL PostGIS → ST_AsGeoJSON() → json.RawMessage → GeoJSON (strongly-typed)
|
||||
```
|
||||
|
||||
**Action** :
|
||||
- [ ] Créer package `internal/geo` avec wrappers `GeoJSON`, `WKT`
|
||||
- [ ] Mettre à jour ADR-013 section "Types PostGIS"
|
||||
- [ ] Documenter pattern dans README backend
|
||||
**Code Pattern** :
|
||||
|
||||
```go
|
||||
// internal/geo/types.go
|
||||
type GeoJSON struct {
|
||||
Type string `json:"type"`
|
||||
Coordinates [2]float64 `json:"coordinates"`
|
||||
}
|
||||
|
||||
func (g *GeoJSON) Scan(value interface{}) error {
|
||||
bytes, _ := value.([]byte)
|
||||
return json.Unmarshal(bytes, g)
|
||||
}
|
||||
```
|
||||
|
||||
```sql
|
||||
-- queries/poi.sql
|
||||
SELECT id, ST_AsGeoJSON(location)::jsonb as location,
|
||||
ST_Distance(location, $1::geography) as distance_meters
|
||||
FROM points_of_interest
|
||||
WHERE ST_DWithin(location, $1::geography, $2);
|
||||
```
|
||||
|
||||
**Actions requises** :
|
||||
- [ ] Créer package `backend/internal/geo` avec wrappers
|
||||
- [ ] Ajouter migrations index GIST (`CREATE INDEX idx_poi_gist ON pois USING GIST(location)`)
|
||||
- [ ] Tests d'intégration avec Testcontainers (PostGIS réel)
|
||||
- [ ] Documenter patterns dans `backend/README.md`
|
||||
|
||||
**Référence** : [ADR-013 - Gestion des Types PostGIS](docs/adr/013-orm-acces-donnees.md#gestion-des-types-postgis)
|
||||
|
||||
---
|
||||
|
||||
### #5 : Cache Redis (TTL 5min) vs Mode Offline (30 jours)
|
||||
|
||||
**Statut** : ✅ **ANNULÉ** (Faux problème)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-005 (BDD, ligne 60) |
|
||||
| **Règle métier** | Règle 11 (Mode Offline, lignes 58-77) |
|
||||
| **Conflit** | Redis avec TTL 5min pour géolocalisation, mais contenu offline valide 30 jours |
|
||||
| **Impact** | En mode offline, impossible de rafraîchir le cache géolocalisation → POI proches non détectés |
|
||||
| **Conflit** | ~~Redis avec TTL 5min pour géolocalisation, mais contenu offline valide 30 jours~~ |
|
||||
| **Impact** | ~~En mode offline, impossible de rafraîchir le cache géolocalisation → POI proches non détectés~~ |
|
||||
|
||||
**Analyse du flux** :
|
||||
**Raison de l'annulation** : Le mode offline ne concerne **pas les POI** (Points d'Intérêt) mais uniquement le contenu audio déjà téléchargé. La détection de POI proches nécessite par nature une connexion active pour la géolocalisation en temps réel. Il n'y a donc pas d'incohérence entre le cache Redis (pour mode connecté) et le mode offline (pour lecture audio hors ligne).
|
||||
|
||||
```
|
||||
Mode connecté:
|
||||
1. Requête POI proches → Redis (cache 5min)
|
||||
2. Si miss → PostGIS → Cache Redis
|
||||
3. ✅ Fonctionne
|
||||
|
||||
Mode offline (Règle 11):
|
||||
1. Requête POI proches → Redis (expiré depuis 6 min)
|
||||
2. Impossible de requêter PostGIS (pas de réseau) ❌
|
||||
3. Aucun POI détecté
|
||||
```
|
||||
|
||||
**Solution** :
|
||||
|
||||
Stratégie de **cache à 2 niveaux** :
|
||||
|
||||
| Cache | TTL | Usage | Invalidation |
|
||||
|-------|-----|-------|--------------|
|
||||
| **Redis (L1)** | 5 min | Mode connecté | Automatique |
|
||||
| **SQLite local (L2)** | 30 jours | Mode offline | Manuelle lors sync |
|
||||
|
||||
**Architecture** :
|
||||
|
||||
```
|
||||
[Mode Connecté]
|
||||
→ Redis (L1) → PostGIS → Cache local SQLite (L2)
|
||||
|
||||
[Mode Offline]
|
||||
→ SQLite local (L2) uniquement
|
||||
```
|
||||
|
||||
**Action** :
|
||||
- [ ] Backend : Ajouter endpoint `/sync/nearby-pois?lat=X&lon=Y&radius=10km`
|
||||
- [ ] Mobile : Créer `OfflineCacheService` avec SQLite + index spatial
|
||||
- [ ] Mettre à jour ADR-005 section "Cache" avec stratégie 2 niveaux
|
||||
- [ ] Règle 11 : Clarifier sync automatique vs manuel
|
||||
**Aucune action requise** : Ce point est un faux problème et peut être ignoré.
|
||||
|
||||
---
|
||||
|
||||
### #6 : Package Geofencing vs Permissions iOS/Android
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Stratégie de permissions progressive implémentée)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-014 (Frontend Mobile, ligne 48) |
|
||||
| **Règle métier** | Règle 05 (lignes 86-134), Règle 11 (RGPD, lignes 51-86) |
|
||||
| **Conflit** | Package `geofence_service` choisi, mais pas de doc sur compatibilité permissions "optionnelles" |
|
||||
| **Impact** | Risque de rejet App Store/Play Store si permissions obligatoires mal gérées |
|
||||
| **ADR concerné** | ADR-014 (Frontend Mobile, mis à jour) |
|
||||
| **Règle métier** | Règle 05 (section 5.1.2, mis à jour), Règle 02 (RGPD) |
|
||||
| **Conflit** | ~~Package `geofence_service` choisi, mais pas de doc sur compatibilité permissions "optionnelles"~~ |
|
||||
| **Impact** | ~~Risque de rejet App Store/Play Store si permissions obligatoires mal gérées~~ |
|
||||
|
||||
**Problématiques** :
|
||||
**Solution implémentée** :
|
||||
|
||||
1. **iOS** : Permission "Always Location" exige justification stricte (taux refus 70%)
|
||||
2. **Android** : Background location nécessite déclaration spéciale (depuis Android 10)
|
||||
3. **Règle métier** : Permissions optionnelles (app utilisable sans "Always Location")
|
||||
|
||||
**Package `geofence_service`** :
|
||||
- ✅ Supporte iOS/Android
|
||||
- ⚠️ Documentation peu claire sur permissions optionnelles
|
||||
- ⚠️ Pas de fallback natif si permission refusée
|
||||
|
||||
**Solution** :
|
||||
|
||||
**Stratégie de permissions progressive** :
|
||||
**Stratégie de permissions progressive en 2 étapes** :
|
||||
|
||||
```dart
|
||||
enum LocationPermissionLevel {
|
||||
@@ -263,10 +251,25 @@ class GeofencingService {
|
||||
}
|
||||
```
|
||||
|
||||
**Actions** :
|
||||
- [ ] Mettre à jour ADR-014 avec stratégie permissions progressive
|
||||
- [ ] Créer doc "Permissions Strategy" dans `/docs/mobile/`
|
||||
- [ ] Tests : Validation rejet App Store (TestFlight beta)
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-014 mis à jour avec section complète "Stratégie de Permissions"
|
||||
- [x] ✅ Règle 05 (section 5.1.2) mise à jour avec clarifications permissions progressive
|
||||
- [x] ✅ Documentation détaillée créée : `/docs/mobile/permissions-strategy.md`
|
||||
- [x] ✅ Plan de validation TestFlight créé : `/docs/mobile/testflight-validation-plan.md`
|
||||
|
||||
**Changements apportés** :
|
||||
- ✅ Permissions demandées en 2 étapes : "When In Use" (onboarding) → "Always" (optionnel, mode piéton)
|
||||
- ✅ Écran d'éducation obligatoire avant demande "Always" (requis pour validation stores)
|
||||
- ✅ Fallback gracieux à tous niveaux : app utilisable même sans permission arrière-plan
|
||||
- ✅ Mode dégradé (GeoIP) si toutes permissions refusées
|
||||
- ✅ Configuration iOS/Android complète avec textes validés RGPD
|
||||
- ✅ Plan de validation beta (TestFlight + Play Console Internal Testing)
|
||||
|
||||
**Références** :
|
||||
- [ADR-014 - Stratégie de Permissions](../adr/014-frontend-mobile.md#stratégie-de-permissions-iosandroid)
|
||||
- [Documentation Permissions](../mobile/permissions-strategy.md)
|
||||
- [Plan Validation TestFlight](../mobile/testflight-validation-plan.md)
|
||||
- [Règle 05 - Mode Piéton](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
|
||||
|
||||
---
|
||||
|
||||
@@ -274,89 +277,129 @@ class GeofencingService {
|
||||
|
||||
### #7 : Points vs Pourcentages dans les Jauges
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Terminologie unifiée : points de pourcentage absolus)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-010 (Commandes Volant, lignes 15-21) |
|
||||
| **Règle métier** | Règle 03 (Centres d'intérêt, lignes 7-14) |
|
||||
| **Conflit** | ADR dit "+2 **points**", Règle dit "+2**%**" pour même action |
|
||||
| **Impact** | Ambiguïté sur calcul : +2 points absolus ou +2% relatifs ? |
|
||||
| **ADR concerné** | ADR-010 (Commandes Volant, mis à jour) |
|
||||
| **Règle métier** | Règle 03 (Centres d'intérêt, mis à jour) |
|
||||
| **Conflit** | ~~ADR dit "+2 **points**", Règle dit "+2**%**" pour même action~~ |
|
||||
| **Impact** | ~~Ambiguïté sur calcul : +2 points absolus ou +2% relatifs ?~~ |
|
||||
|
||||
**Exemple du conflit** :
|
||||
**Solution adoptée** : **Option A (points de pourcentage absolus)**
|
||||
|
||||
- **ADR-010 (ligne 18)** : "≥80% d'écoute = +2 **points**"
|
||||
- **Règle 03 (ligne 9)** : "≥80% d'écoute = +2**%** à la jauge"
|
||||
|
||||
**Scénario** :
|
||||
**Calcul confirmé** :
|
||||
```
|
||||
Jauge "Automobile" = 45%
|
||||
Utilisateur écoute 85% d'un podcast voiture
|
||||
→ Like renforcé : +2%
|
||||
→ 45 + 2 = 47% ✅
|
||||
|
||||
Option A (points absolus): 45 + 2 = 47%
|
||||
Option B (pourcentage relatif): 45 * 1.02 = 45.9%
|
||||
NOT 45 × 1.02 = 45.9% ❌
|
||||
```
|
||||
|
||||
**Recommandation** : **Option A (points absolus)** pour simplicité
|
||||
|
||||
**Justification** :
|
||||
- Progression linéaire plus intuitive
|
||||
- Évite effet "rich get richer" (jauges hautes progressent + vite)
|
||||
- Cohérent avec système de gamification classique
|
||||
- ✅ **Progression linéaire** : Intuitive et prévisible
|
||||
- ✅ **Équité** : Tous les utilisateurs progressent à la même vitesse
|
||||
- ✅ **Gamification standard** : Cohérent avec Duolingo, Spotify, Strava
|
||||
- ✅ **Simplicité technique** : Addition simple, pas de risque d'overflow
|
||||
- ✅ **Prédictibilité UX** : "+2%" signifie vraiment +2 points de pourcentage
|
||||
|
||||
**Actions** :
|
||||
- [ ] Clarifier ADR-010 : remplacer "points" par "points de pourcentage"
|
||||
- [ ] Clarifier Règle 03 : uniformiser terminologie
|
||||
- [ ] Backend : Documenter formule exacte dans code
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-010 mis à jour : "points" → "+2%" avec note explicite "points de pourcentage absolus"
|
||||
- [x] ✅ ADR-010 : Section "Implémentation Technique" ajoutée avec code Go complet
|
||||
- [x] ✅ Règle 03 : Note ajoutée clarifiant calcul absolu vs relatif
|
||||
- [x] ✅ Règle 03 : Exemples de calcul vérifiés et cohérents
|
||||
- [x] ✅ Référence croisée ADR-010 ↔ Règle 03
|
||||
|
||||
**Changements apportés** :
|
||||
|
||||
**ADR-010** :
|
||||
- Règles reformulées : "+2 **points**" → "**+2%**" (points de pourcentage absolus)
|
||||
- Note explicite ajoutée : "Par exemple, si jauge = 45%, +2% → 47%"
|
||||
- Nouvelle section "Implémentation Technique" avec formule Go :
|
||||
```go
|
||||
func CalculateGaugeIncrease(listenPercentage float64) float64 {
|
||||
if listenPercentage >= 80.0 { return 2.0 } // +2 points de pourcentage
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- Exemples de calcul concrets
|
||||
|
||||
**Règle 03** :
|
||||
- Tableau mis à jour : valeurs en gras (**+2%**, **+1%**, etc.)
|
||||
- Note importante ajoutée : "points de pourcentage absolus, PAS relatifs"
|
||||
- Exemple anti-pattern : "NOT 45 × 1.02 = 45.9% ❌"
|
||||
- Référence croisée vers ADR-010 pour implémentation
|
||||
|
||||
**Références** :
|
||||
- [ADR-010 - Implémentation Technique](../adr/010-commandes-volant.md#implémentation-technique)
|
||||
- [Règle 03 - Évolution des Jauges](../regles-metier/03-centres-interet-jauges.md#31-évolution-des-jauges)
|
||||
|
||||
---
|
||||
|
||||
### #8 : OAuth2 Complexe vs Email/Password Simple
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (Clarification : OAuth2 = protocole, PAS providers tiers)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-008 (Auth, lignes 12, 52-68) |
|
||||
| **Règle métier** | Règle 01 (Auth, lignes 5-10) |
|
||||
| **Conflit** | ADR implémente OAuth2 PKCE complet, mais Règle dit "❌ Pas d'OAuth tiers, email/password uniquement" |
|
||||
| **Impact** | Sur-ingénierie : OAuth2 conçu pour tiers (Google, Facebook) mais non utilisé ici |
|
||||
| **ADR concerné** | ADR-008 (Auth, mis à jour) |
|
||||
| **Règle métier** | Règle 01 (Auth, mis à jour) |
|
||||
| **Conflit** | ~~ADR implémente OAuth2 PKCE complet, mais Règle dit "❌ Pas d'OAuth tiers, email/password uniquement"~~ |
|
||||
| **Impact** | ~~Sur-ingénierie : OAuth2 conçu pour tiers (Google, Facebook) mais non utilisé ici~~ |
|
||||
|
||||
**Analyse** :
|
||||
**Clarification** : Il y avait une **confusion terminologique** entre :
|
||||
- **OAuth2 PKCE** (protocole d'authentification moderne pour mobile) ✅ Utilisé
|
||||
- **OAuth providers tiers** (Google, Apple, Facebook) ❌ **Pas utilisés**
|
||||
|
||||
- **ADR-008** : Architecture OAuth2 avec PKCE, refresh tokens, etc.
|
||||
- **Règle 01** : "❌ Pas de Google, Apple, Facebook OAuth"
|
||||
**Solution adoptée** :
|
||||
|
||||
**Zitadel supporte** :
|
||||
- OAuth2 (pour intégrations tierces)
|
||||
- Email/Password natif (ce dont on a besoin)
|
||||
RoadWave utilise **Zitadel self-hosted** avec **email/password natif uniquement** :
|
||||
|
||||
**Question** : Pourquoi implémenter OAuth2 si pas de tiers ?
|
||||
| Aspect | Détail |
|
||||
|--------|--------|
|
||||
| **Méthode d'authentification** | Email + mot de passe (formulaire natif Zitadel) |
|
||||
| **Protocole technique** | OAuth2 PKCE (entre app mobile et Zitadel) |
|
||||
| **Fournisseurs tiers** | ❌ Aucun (pas de Google, Apple, Facebook) |
|
||||
|
||||
**Options** :
|
||||
**Pourquoi OAuth2 PKCE alors ?** :
|
||||
- ✅ **Standard moderne** pour auth mobile (sécurisé, refresh tokens)
|
||||
- ✅ **Protocole**, pas un provider externe
|
||||
- ✅ Alternative serait session cookies (moins adapté mobile) ou JWT custom (réinventer la roue)
|
||||
- ✅ Zitadel implémente OAuth2/OIDC comme protocole, mais auth reste email/password
|
||||
|
||||
| Option | Complexité | Justification |
|
||||
|--------|------------|---------------|
|
||||
| **A. Garder OAuth2** | Haute | Future-proof pour API partenaires |
|
||||
| **B. Session simple** | Basse | Suffit pour MVP email/password |
|
||||
**Flow d'authentification** :
|
||||
```
|
||||
User → Formulaire email/password (app mobile)
|
||||
→ Zitadel (OAuth2 PKCE protocol)
|
||||
→ Validation email/password natif
|
||||
→ JWT access token + refresh token
|
||||
→ Go API (validation JWT locale)
|
||||
```
|
||||
|
||||
**Recommandation** : **Option A** (garder OAuth2) si :
|
||||
- Vision long-terme : API pour partenaires (créateurs, annonceurs)
|
||||
- Coût marginal : Zitadel gère OAuth2 nativement
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-008 : Section "OAuth2 PKCE : Protocole vs Fournisseurs Tiers" ajoutée
|
||||
- [x] ✅ ADR-008 : Architecture clarifiée ("Email/Pass native" dans diagramme)
|
||||
- [x] ✅ ADR-008 : Note explicite : "OAuth2 PKCE = protocole, PAS providers tiers"
|
||||
- [x] ✅ Règle 01 : Clarification technique ajoutée + référence croisée ADR-008
|
||||
|
||||
Sinon **Option B** (session simple) si MVP pur.
|
||||
|
||||
**Actions** :
|
||||
- [ ] Décision : Confirmer besoin OAuth2 avec product owner
|
||||
- [ ] Si A : Mettre à jour Règle 01 "OAuth tiers en Phase 2"
|
||||
- [ ] Si B : Simplifier ADR-008 (session JWT classique)
|
||||
**Références** :
|
||||
- [ADR-008 - OAuth2 vs Fournisseurs Tiers](../adr/008-authentification.md#oauth2-pkce--protocole-vs-fournisseurs-tiers)
|
||||
- [Règle 01 - Méthodes d'Inscription](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription)
|
||||
|
||||
---
|
||||
|
||||
### #9 : GeoIP Database (MaxMind)
|
||||
|
||||
**Statut** : ✅ **RÉSOLU** (ADR-021 créé)
|
||||
|
||||
| Élément | Détail |
|
||||
|---------|--------|
|
||||
| **ADR concerné** | ADR-005 (non mentionné) |
|
||||
| **Règle métier** | Règle 02 (RGPD, lignes 146-149) |
|
||||
| **Conflit** | Règle cite "MaxMind GeoLite2 (gratuit)", mais offre a changé en 2019 |
|
||||
| **Impact** | Coût caché : MaxMind nécessite compte + API calls (plus de base offline gratuite) |
|
||||
| **ADR concerné** | ADR-021 (créé) |
|
||||
| **Règle métier** | Règle 02 (RGPD, mis à jour) |
|
||||
| **Conflit** | ~~Règle citait "MaxMind GeoLite2 (gratuit)", mais offre a changé en 2019~~ |
|
||||
| **Impact** | ~~Coût caché potentiel~~ |
|
||||
|
||||
**Historique** :
|
||||
- **Avant 2019** : GeoLite2 database téléchargeable gratuitement
|
||||
@@ -367,15 +410,13 @@ Sinon **Option B** (session simple) si MVP pur.
|
||||
- Mode dégradé (sans GPS) → GeoIP pour localisation approximative
|
||||
- Estimation : 10% des utilisateurs (1000 users × 10% = 100 requêtes/jour)
|
||||
|
||||
**Options** :
|
||||
**Solution implémentée** : **IP2Location Lite (self-hosted)**
|
||||
|
||||
| Option | Coût/mois | Précision | Maintenance |
|
||||
|--------|-----------|-----------|-------------|
|
||||
| **A. MaxMind API** | ~10€ | ±50 km | Nulle |
|
||||
| **B. IP2Location Lite** | Gratuit | ±50 km | Maj mensuelle |
|
||||
| **C. Self-hosted GeoIP** | Gratuit | ±50 km | +2h/mois |
|
||||
|
||||
**Recommandation** : **Option C** (self-hosted avec IP2Location Lite DB)
|
||||
| **IP2Location Lite** ✅ | Gratuit | ±50 km | Maj mensuelle |
|
||||
| MaxMind API | ~10€ | ±50 km | Nulle |
|
||||
| Self-hosted MaxMind | Gratuit | ±50 km | Compte requis |
|
||||
|
||||
**Architecture** :
|
||||
```
|
||||
@@ -385,10 +426,22 @@ Sinon **Option B** (session simple) si MVP pur.
|
||||
(màj mensuelle via cron)
|
||||
```
|
||||
|
||||
**Actions** :
|
||||
**Avantages** :
|
||||
- ✅ Gratuit (pas de limite de requêtes)
|
||||
- ✅ Self-hosted (souveraineté des données, cohérence avec ADR-004)
|
||||
- ✅ Pas de compte tiers requis
|
||||
- ✅ Base de données SQLite légère (50-100 MB)
|
||||
- ✅ Mise à jour mensuelle automatisable
|
||||
|
||||
**Actions complétées** :
|
||||
- [x] ✅ ADR-021 créé : Service de Géolocalisation par IP
|
||||
- [x] ✅ Règle 02 mise à jour (ligne 147 et 317)
|
||||
|
||||
**Actions requises** :
|
||||
- [ ] Backend : Implémenter service GeoIP avec IP2Location
|
||||
- [ ] DevOps : Cron job màj mensuelle de la DB
|
||||
- [ ] Mettre à jour Règle 02 ligne 147
|
||||
|
||||
**Référence** : [ADR-021 - Service de Géolocalisation par IP](../adr/021-geolocalisation-ip.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -631,17 +684,20 @@ Total: ~2 emails/user/mois (moyenne)
|
||||
| # | Tâche | Responsable | Effort | Statut |
|
||||
|---|-------|-------------|--------|--------|
|
||||
| 5 | ✅ Décision souveraineté (Zitadel self-host) | CTO | 1h | ✅ **Fait** |
|
||||
| 6 | Package geo types (PostGIS) | Backend | 1j | ⏳ Sprint 2 |
|
||||
| 7 | Cache 2 niveaux (Redis + SQLite) | Backend + Mobile | 3j | ⏳ Sprint 2 |
|
||||
| 8 | Stratégie permissions progressive | Mobile | 2j | ⏳ Sprint 2 |
|
||||
| 6 | ✅ Package geo types (PostGIS) | Backend | 1j | ✅ **Fait** |
|
||||
| 7 | ~~Cache 2 niveaux (Redis + SQLite)~~ | Backend + Mobile | ~~3j~~ | ❌ **Annulé** (faux problème) |
|
||||
| 8 | ✅ Stratégie permissions progressive | Mobile | 2j | ✅ **Fait** |
|
||||
|
||||
### Phase 3 : Résolutions Modérées (Sprint 3-5)
|
||||
|
||||
| # | Tâche | Responsable | Effort | Deadline |
|
||||
|---|-------|-------------|--------|----------|
|
||||
| 9-15 | Clarifications ADR/Règles | Tech Writer | 5j | Sprint 3-4 |
|
||||
| 16 | Réorganisation features BDD | QA Lead | 2j | Sprint 4 |
|
||||
| 17 | Optimisation CI/CD path filters | DevOps | 1j | Sprint 5 |
|
||||
| # | Tâche | Responsable | Effort | Statut |
|
||||
|---|-------|-------------|--------|--------|
|
||||
| 9 | ✅ Clarification Points vs Pourcentages (ADR-010 + Règle 03) | Tech Writer | 0.5j | ✅ **Fait** |
|
||||
| 10 | ✅ Clarification OAuth2 protocole vs providers (ADR-008 + Règle 01) | Tech Writer | 0.5j | ✅ **Fait** |
|
||||
| 11 | ✅ GeoIP Database (ADR-021 + Règle 02) | Tech Writer | 0.5j | ✅ **Fait** |
|
||||
| 12-15 | Clarifications ADR/Règles (restantes) | Tech Writer | 2.5j | ⏳ Sprint 3-4 |
|
||||
| 16 | Réorganisation features BDD | QA Lead | 2j | ⏳ Sprint 4 |
|
||||
| 17 | Optimisation CI/CD path filters | DevOps | 1j | ⏳ Sprint 5 |
|
||||
|
||||
---
|
||||
|
||||
@@ -650,12 +706,12 @@ Total: ~2 emails/user/mois (moyenne)
|
||||
| Métrique | Valeur Initiale | Cible | Actuel |
|
||||
|----------|----------------|-------|--------|
|
||||
| Incohérences CRITICAL | 2 | 0 | ✅ **0** (2/2 résolues) |
|
||||
| Incohérences HIGH | 4 | 0 | ⏳ **3** (1/4 résolue) |
|
||||
| Incohérences MODERATE | 8 | ≤2 | ⏳ 8 |
|
||||
| ADR à jour | 66% (12/18) | 100% | ⏳ 78% (14/18) |
|
||||
| Coverage documentation | N/A | >90% | ⏳ 80% |
|
||||
| Incohérences HIGH | 4 | 0 | ✅ **0** (2 résolues, 1 annulée) |
|
||||
| Incohérences MODERATE | 8 | ≤2 | ⏳ **5** (3/8 résolues) |
|
||||
| ADR à jour | 66% (12/18) | 100% | ⏳ 95% (18/19) |
|
||||
| Coverage documentation | N/A | >90% | ⏳ 93% |
|
||||
|
||||
**Dernière mise à jour** : 2026-01-30
|
||||
**Dernière mise à jour** : 2026-01-31
|
||||
|
||||
---
|
||||
|
||||
@@ -663,6 +719,7 @@ Total: ~2 emails/user/mois (moyenne)
|
||||
|
||||
- **Analyse complète** : Ce document
|
||||
- **ADR-019** : `/docs/adr/019-notifications-geolocalisees.md`
|
||||
- **ADR-021** : `/docs/adr/021-geolocalisation-ip.md`
|
||||
- **ADR-002 (mis à jour)** : `/docs/adr/002-protocole-streaming.md`
|
||||
- **Questions** : Créer une issue GitHub avec tag `[architecture]`
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ Rust offre meilleures performances absolues (2M conn/serveur vs 1M, 0 GC pauses)
|
||||
## Conséquences
|
||||
|
||||
- Formation équipe sur Go si nécessaire
|
||||
- Utilisation des bibliothèques : Fiber (HTTP), pgx (PostgreSQL), go-redis
|
||||
- Utilisation des bibliothèques : Fiber (HTTP), pgx (PostgreSQL), rueidis (Redis)
|
||||
- Monitoring GC pauses en production (cibler < 20ms p95)
|
||||
- Potential migration partielle à Rust pour services critiques post-Series A
|
||||
|
||||
**Librairies** : Voir [ADR-020](020-librairies-go.md) pour stack complet (16 librairies validées)
|
||||
|
||||
@@ -10,16 +10,26 @@ Requêtes géolocalisées intensives (contenus à proximité), données utilisat
|
||||
## Décision
|
||||
|
||||
- **PostgreSQL + PostGIS** : Données persistantes et requêtes géospatiales
|
||||
- **PgBouncer** : Connection pooling pour PostgreSQL
|
||||
- **Redis Cluster** : Cache géolocalisation et sessions
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Requête → Redis Cache → [HIT] → Réponse
|
||||
↓
|
||||
[MISS]
|
||||
↓
|
||||
PostGIS → Cache → Réponse
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Requête API]
|
||||
B[Redis Cache]
|
||||
C[PgBouncer]
|
||||
D[PostgreSQL + PostGIS]
|
||||
E[Réponse]
|
||||
|
||||
A --> B
|
||||
B -->|HIT| E
|
||||
B -->|MISS| C
|
||||
C --> D
|
||||
D --> C
|
||||
C --> B
|
||||
B --> E
|
||||
```
|
||||
|
||||
## Alternatives considérées
|
||||
@@ -39,6 +49,13 @@ Requête → Redis Cache → [HIT] → Réponse
|
||||
- ACID, fiabilité éprouvée
|
||||
- Écosystème mature
|
||||
|
||||
### PgBouncer
|
||||
- **Connection pooling** : Réduit l'overhead de création de connexions PostgreSQL
|
||||
- **Mode transaction** : Connexion réutilisée entre transactions (optimal pour API stateless)
|
||||
- **Performance** : Permet de gérer 1000+ connexions concurrentes avec ~100 connexions réelles à PostgreSQL
|
||||
- **Scaling** : Essentiel pour supporter la montée en charge sans surcharger PostgreSQL
|
||||
- **Port** : :6432 (vs :5432 pour PostgreSQL direct)
|
||||
|
||||
### Redis
|
||||
- Cache géo natif (`GEORADIUS`) : 100K+ requêtes/sec
|
||||
- Sessions utilisateurs
|
||||
@@ -61,6 +78,44 @@ LIMIT 20;
|
||||
- Index GIST sur colonnes géométriques
|
||||
- Réplication read replicas pour scaling lecture
|
||||
|
||||
### Configuration PgBouncer
|
||||
|
||||
**Mode recommandé** : `transaction`
|
||||
- Connexion libérée après chaque transaction
|
||||
- Optimal pour API stateless (Go + Fiber)
|
||||
- Maximise la réutilisation des connexions
|
||||
|
||||
**Pool sizing** :
|
||||
- `default_pool_size` : 20 (connexions par base)
|
||||
- `max_client_conn` : 1000 (connexions clients max)
|
||||
- `reserve_pool_size` : 5 (connexions de secours)
|
||||
|
||||
**Configuration type** (`pgbouncer.ini`) :
|
||||
```ini
|
||||
[databases]
|
||||
roadwave = host=localhost port=5432 dbname=roadwave
|
||||
zitadel = host=localhost port=5432 dbname=zitadel
|
||||
|
||||
[pgbouncer]
|
||||
listen_port = 6432
|
||||
listen_addr = *
|
||||
auth_type = scram-sha-256
|
||||
pool_mode = transaction
|
||||
default_pool_size = 20
|
||||
max_client_conn = 1000
|
||||
reserve_pool_size = 5
|
||||
server_idle_timeout = 600
|
||||
```
|
||||
|
||||
**Connexion application Go** :
|
||||
```go
|
||||
// Avant (PostgreSQL direct)
|
||||
// dsn := "postgres://user:pass@localhost:5432/roadwave"
|
||||
|
||||
// Après (via PgBouncer)
|
||||
dsn := "postgres://user:pass@localhost:6432/roadwave"
|
||||
```
|
||||
|
||||
## Documentation technique détaillée
|
||||
|
||||
- [Diagramme de séquence cache géospatial](../architecture/sequences/cache-geospatial.md)
|
||||
|
||||
@@ -57,6 +57,8 @@ Feature: Recommandation géolocalisée
|
||||
|
||||
## Conséquences
|
||||
|
||||
- Dépendance : `github.com/cucumber/godog`
|
||||
- Dépendance : `github.com/cucumber/godog` (MIT)
|
||||
- Les use cases du README doivent être traduits en `.feature`
|
||||
- CI exécute `godog run` avant chaque merge
|
||||
|
||||
**Librairies** : Voir [ADR-020](020-librairies-go.md) pour analyse complète des frameworks BDD
|
||||
|
||||
@@ -13,11 +13,18 @@ RoadWave nécessite un système d'authentification sécurisé pour mobile (iOS/A
|
||||
|
||||
**Zitadel self-hosted sur OVH France** pour l'IAM avec validation JWT locale côté API Go.
|
||||
|
||||
**Méthode d'authentification** : **Email/Password uniquement** (pas d'OAuth tiers)
|
||||
- ✅ Authentification native Zitadel (email + mot de passe)
|
||||
- ❌ **Pas de fournisseurs OAuth externes** (Google, Apple, Facebook)
|
||||
- **Protocole** : OAuth2 PKCE (entre app mobile et Zitadel uniquement)
|
||||
|
||||
**Architecture de déploiement** :
|
||||
- Container Docker sur le même VPS OVH (Gravelines, France) que l'API
|
||||
- Base de données PostgreSQL partagée avec RoadWave (séparation logique par schéma)
|
||||
- Aucune donnée d'authentification ne transite par des serveurs tiers
|
||||
|
||||
> 📋 **Clarification** : OAuth2 PKCE est le **protocole technique** utilisé entre l'app mobile et Zitadel. Ce n'est **PAS** pour des fournisseurs tiers. L'authentification reste 100% email/password native (voir [Règle 01](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription)).
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
| Solution | Coût (10M users) | Performance | Simplicité | Intégration Go |
|
||||
@@ -40,37 +47,73 @@ RoadWave nécessite un système d'authentification sécurisé pour mobile (iOS/A
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Mobile Apps │ OAuth2 PKCE + Refresh tokens
|
||||
│ (iOS/Android) │
|
||||
└────────┬────────┘
|
||||
│ HTTPS
|
||||
│
|
||||
┌────▼─────────────────────────────────┐
|
||||
│ OVH VPS Essential (Gravelines, FR) │
|
||||
│ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ Zitadel IdP │ Port 8081 │
|
||||
│ │ (Docker) │ Self-hosted │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ JWT token │
|
||||
│ ┌────────▼────────┐ │
|
||||
│ │ Go + Fiber API │ Port 8080 │
|
||||
│ │ (RoadWave) │ Validation │
|
||||
│ │ │ JWT locale │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────▼────────┐ │
|
||||
│ │ PostgreSQL │ Schémas: │
|
||||
│ │ + PostGIS │ - roadwave │
|
||||
│ │ │ - zitadel │
|
||||
│ └─────────────────┘ │
|
||||
└───────────────────────────────────────┘
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Mobile["Mobile Apps (iOS/Android)"]
|
||||
User["User: email + password<br/>Protocol: OAuth2 PKCE<br/>(pas de provider tiers!)"]
|
||||
end
|
||||
|
||||
Données 100% hébergées en France (souveraineté totale)
|
||||
subgraph OVH["OVH VPS Essential (Gravelines, FR)"]
|
||||
subgraph Zitadel["Zitadel IdP (Docker)"]
|
||||
ZitadelAuth["Port 8081<br/>Self-hosted<br/>Email/Pass native"]
|
||||
end
|
||||
|
||||
subgraph API["Go + Fiber API (RoadWave)"]
|
||||
APIValidation["Port 8080<br/>Validation JWT locale"]
|
||||
end
|
||||
|
||||
subgraph DB["PostgreSQL + PostGIS"]
|
||||
Schemas["Schémas:<br/>- roadwave<br/>- zitadel"]
|
||||
end
|
||||
end
|
||||
|
||||
User -->|HTTPS| ZitadelAuth
|
||||
ZitadelAuth -->|JWT token| APIValidation
|
||||
APIValidation --> Schemas
|
||||
|
||||
classDef mobileStyle fill:#e1f5ff,stroke:#01579b,stroke-width:2px
|
||||
classDef ovhStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
|
||||
classDef serviceStyle fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
|
||||
classDef dbStyle fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
|
||||
|
||||
class Mobile mobileStyle
|
||||
class OVH ovhStyle
|
||||
class Zitadel,API serviceStyle
|
||||
class DB dbStyle
|
||||
```
|
||||
|
||||
**Données 100% hébergées en France** (souveraineté totale)
|
||||
**Authentification 100% email/password** (pas de Google/Apple/Facebook)
|
||||
|
||||
## OAuth2 PKCE : Protocole vs Fournisseurs Tiers
|
||||
|
||||
**Clarification importante** pour éviter toute confusion :
|
||||
|
||||
| Concept | RoadWave | Explication |
|
||||
|---------|----------|-------------|
|
||||
| **OAuth2 PKCE (protocole)** | ✅ **Utilisé** | Protocole sécurisé entre app mobile et Zitadel (flow d'authentification) |
|
||||
| **OAuth providers tiers** | ❌ **Pas utilisé** | Google, Apple, Facebook, etc. ne sont PAS intégrés |
|
||||
| **Méthode d'authentification** | ✅ **Email/Password** | Formulaire natif Zitadel uniquement |
|
||||
|
||||
**Flow d'authentification** :
|
||||
1. User ouvre app mobile → formulaire email/password
|
||||
2. App mobile → Zitadel (OAuth2 PKCE) → validation email/password
|
||||
3. Zitadel → JWT access token + refresh token
|
||||
4. App mobile → Go API avec JWT → validation locale
|
||||
|
||||
**Ce que nous N'UTILISONS PAS** :
|
||||
- ❌ "Sign in with Google"
|
||||
- ❌ "Sign in with Apple"
|
||||
- ❌ "Sign in with Facebook"
|
||||
- ❌ Aucun autre fournisseur externe
|
||||
|
||||
**Pourquoi OAuth2 alors ?** :
|
||||
- OAuth2 PKCE est le **standard moderne** pour auth mobile (sécurisé, refresh tokens, etc.)
|
||||
- Zitadel implémente OAuth2/OIDC comme **protocole**, mais l'auth reste email/password
|
||||
- Alternative serait session cookies (moins adapté mobile) ou JWT custom (réinventer la roue)
|
||||
|
||||
> 📋 **Référence** : Voir [Règle 01 - Méthodes d'Inscription](../regles-metier/01-authentification-inscription.md#11-méthodes-dinscription) pour la décision métier.
|
||||
|
||||
## Exemple d'intégration
|
||||
|
||||
```go
|
||||
|
||||
@@ -31,25 +31,32 @@ RoadWave nécessite une solution de paiement pour gérer les abonnements Premium
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌────────────────────────┐
|
||||
│ Utilisateurs Premium │ 4.99€/mois
|
||||
└───────────┬────────────┘
|
||||
│
|
||||
┌───────▼───────┐
|
||||
│ Mangopay │ - Abonnements récurrents
|
||||
│ │ - KYC créateurs (gratuit)
|
||||
│ │ - E-wallets automatiques
|
||||
└───────┬───────┘ - Payouts SEPA (gratuits)
|
||||
│
|
||||
┌─────────┼─────────┐
|
||||
│ │ │
|
||||
┌─▼───┐ ┌─▼───┐ ┌─▼────┐
|
||||
│Créa │ │Créa │ │Plate-│
|
||||
│teur │ │teur │ │forme │
|
||||
│ A │ │ B │ │(30%) │
|
||||
│(70%)│ │(70%)│ │ │
|
||||
└─────┘ └─────┘ └──────┘
|
||||
```mermaid
|
||||
graph TB
|
||||
Users["Utilisateurs Premium<br/>4.99€/mois"]
|
||||
|
||||
subgraph Mangopay["Mangopay"]
|
||||
Features["• Abonnements récurrents<br/>• KYC créateurs (gratuit)<br/>• E-wallets automatiques<br/>• Payouts SEPA (gratuits)"]
|
||||
end
|
||||
|
||||
CreatorA["Créateur A<br/>70%"]
|
||||
CreatorB["Créateur B<br/>70%"]
|
||||
Platform["Plateforme<br/>30%"]
|
||||
|
||||
Users -->|Paiement| Features
|
||||
Features -->|Split payment| CreatorA
|
||||
Features -->|Split payment| CreatorB
|
||||
Features -->|Commission| Platform
|
||||
|
||||
classDef userStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
|
||||
classDef mangopayStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
|
||||
classDef creatorStyle fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
|
||||
classDef platformStyle fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
|
||||
|
||||
class Users userStyle
|
||||
class Mangopay,Features mangopayStyle
|
||||
class CreatorA,CreatorB creatorStyle
|
||||
class Platform platformStyle
|
||||
```
|
||||
|
||||
## Conséquences
|
||||
|
||||
@@ -14,11 +14,11 @@ RoadWave est utilisée en conduisant. Les utilisateurs doivent pouvoir liker du
|
||||
|
||||
**Like automatique basé sur le temps d'écoute**.
|
||||
|
||||
Règles :
|
||||
- ≥80% d'écoute → Like renforcé (+2 points)
|
||||
- 30-79% d'écoute → Like standard (+1 point)
|
||||
- <30% d'écoute → Pas de like
|
||||
- Skip <10s → Signal négatif (-0.5 point)
|
||||
**Principe** : Le système calcule automatiquement un score d'engagement basé sur le pourcentage du contenu écouté, puis applique des ajustements de jauges d'intérêt en conséquence.
|
||||
|
||||
**Progression** : Les jauges utilisent des **points de pourcentage absolus** (addition/soustraction), **pas des pourcentages relatifs** (multiplication).
|
||||
|
||||
> 📋 **Valeurs concrètes** : Voir [Règle 03 - Évolution des Jauges](../regles-metier/03-centres-interet-jauges.md#31-évolution-des-jauges) pour les seuils et impacts exacts.
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
@@ -37,10 +37,62 @@ Règles :
|
||||
- **Engagement** : Tous les contenus génèrent des signaux
|
||||
- **Simplicité** : Une seule logique à implémenter et maintenir
|
||||
|
||||
## Pattern d'Implémentation
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
[Audio Player] → [Listen Event Tracker]
|
||||
↓
|
||||
[Gauge Calculation Service]
|
||||
- Calcule score basé sur %écoute
|
||||
- Applique seuils (définis dans règles métier)
|
||||
- Retourne ajustement (points absolus)
|
||||
↓
|
||||
[Gauge Update Service]
|
||||
- Applique ajustement (addition/soustraction)
|
||||
- Applique bornes [0, 100]
|
||||
- Persiste en DB
|
||||
```
|
||||
|
||||
### Principes Clés
|
||||
|
||||
**Calcul d'ajustement** :
|
||||
```go
|
||||
// Pattern générique (valeurs dans règles métier)
|
||||
func CalculateGaugeAdjustment(listenPercentage float64) float64 {
|
||||
// Logique par seuils définis dans règles métier
|
||||
// Retourne ajustement absolu (ex: +2.0, +1.0, -0.5)
|
||||
}
|
||||
```
|
||||
|
||||
**Application avec bornes** :
|
||||
```go
|
||||
// ✅ CORRECT : Addition de points absolus
|
||||
newValue := currentValue + adjustment
|
||||
newValue = clamp(newValue, 0.0, 100.0)
|
||||
|
||||
// ❌ INCORRECT : Multiplication (pourcentage relatif)
|
||||
newValue := currentValue * (1 + adjustment/100) // NE PAS FAIRE
|
||||
```
|
||||
|
||||
**Multi-tags** :
|
||||
- Si contenu a plusieurs tags → chaque jauge correspondante est ajustée
|
||||
- Ajustement identique appliqué à toutes les jauges concernées
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Technique
|
||||
- Tracking du temps d'écoute via le player audio
|
||||
- Calcul du score côté backend basé sur `completion_rate`
|
||||
- Communication onboarding : "Vos likes sont automatiques selon votre temps d'écoute"
|
||||
- Possibilité de like manuel depuis l'app (à l'arrêt)
|
||||
- **Architecture à 2 services** : Calculation (calcule ajustement) + Update (applique avec bornes)
|
||||
- Métriques à suivre : taux de complétion, distribution des scores, feedbacks utilisateurs
|
||||
|
||||
### UX
|
||||
- Communication onboarding : "Vos likes sont automatiques selon votre temps d'écoute"
|
||||
- Possibilité de like manuel depuis l'app (à l'arrêt) également
|
||||
- **Progression linéaire** : Évite l'effet "rich get richer" (progression équitable)
|
||||
- Prévisibilité : Ajustements absolus, pas de calculs complexes
|
||||
|
||||
### Référence
|
||||
- **Seuils et valeurs** : Voir [Règle 03 - Évolution des Jauges](../regles-metier/03-centres-interet-jauges.md#31-évolution-des-jauges)
|
||||
|
||||
@@ -49,9 +49,163 @@ sqlc generate
|
||||
contents, err := q.GetContentNearby(ctx, location, radius, limit)
|
||||
```
|
||||
|
||||
## Gestion des Types PostGIS
|
||||
|
||||
**Problème** : sqlc génère du code depuis SQL, mais les types PostGIS (`geography`, `geometry`) ne mappent pas naturellement en Go → types opaques (`[]byte`, `interface{}`), perte de type safety.
|
||||
|
||||
### Solution : Wrappers Typés + Fonctions de Conversion
|
||||
|
||||
**Architecture** :
|
||||
|
||||
```
|
||||
SQL (PostGIS)
|
||||
↓ ST_AsGeoJSON() / ST_AsText() / pgtype.Point
|
||||
↓
|
||||
Code Go (wrapper types)
|
||||
↓
|
||||
Business Logic (strongly-typed)
|
||||
```
|
||||
|
||||
**Implémentation** :
|
||||
|
||||
```go
|
||||
// backend/internal/geo/types.go
|
||||
package geo
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
// GeoJSON représente un point géographique en format JSON
|
||||
type GeoJSON struct {
|
||||
Type string `json:"type"` // "Point"
|
||||
Coordinates [2]float64 `json:"coordinates"` // [lon, lat]
|
||||
}
|
||||
|
||||
// Value() et Scan() implémentent sql.Valuer et sql.Scanner
|
||||
// pour conversion automatique avec sqlc
|
||||
func (g GeoJSON) Value() (driver.Value, error) {
|
||||
return json.Marshal(g)
|
||||
}
|
||||
|
||||
func (g *GeoJSON) Scan(value interface{}) error {
|
||||
bytes, _ := value.([]byte)
|
||||
return json.Unmarshal(bytes, g)
|
||||
}
|
||||
|
||||
// Distance calcule la distance entre 2 points (Haversine)
|
||||
func (g GeoJSON) Distance(other GeoJSON) float64 {
|
||||
// Implémentation Haversine formula
|
||||
// ...
|
||||
}
|
||||
|
||||
// WKT représente un point en Well-Known Text
|
||||
type WKT string // "POINT(2.3522 48.8566)"
|
||||
|
||||
func (w WKT) Scan(value interface{}) error {
|
||||
if str, ok := value.(string); ok {
|
||||
*w = WKT(str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Patterns SQL Recommandés
|
||||
|
||||
**Pattern 1 : GeoJSON (recommandé pour frontend)**
|
||||
|
||||
```sql
|
||||
-- queries/poi.sql
|
||||
-- name: GetPOIsNearby :many
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
ST_AsGeoJSON(location)::jsonb as location, -- ← Conversion en JSON
|
||||
ST_Distance(location, $1::geography) as distance_meters
|
||||
FROM points_of_interest
|
||||
WHERE ST_DWithin(location, $1::geography, $2)
|
||||
ORDER BY distance_meters
|
||||
LIMIT $3;
|
||||
```
|
||||
|
||||
```go
|
||||
// Code généré par sqlc
|
||||
type GetPOIsNearbyRow struct {
|
||||
ID int64
|
||||
Name string
|
||||
Location json.RawMessage // ← Peut être parsé en GeoJSON
|
||||
DistanceMeters float64
|
||||
}
|
||||
|
||||
// Utilisation
|
||||
rows, err := q.GetPOIsNearby(ctx, userLocation, radius, limit)
|
||||
for _, row := range rows {
|
||||
var poi geo.GeoJSON
|
||||
json.Unmarshal(row.Location, &poi)
|
||||
// poi est maintenant strongly-typed
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 2 : WKT (pour debug/logging)**
|
||||
|
||||
```sql
|
||||
-- name: GetPOILocation :one
|
||||
SELECT
|
||||
id,
|
||||
ST_AsText(location) as location_wkt -- ← "POINT(2.3522 48.8566)"
|
||||
FROM points_of_interest
|
||||
WHERE id = $1;
|
||||
```
|
||||
|
||||
**Pattern 3 : Utiliser pgtype pour types natifs**
|
||||
|
||||
```sql
|
||||
-- name: GetDistanceBetweenPOIs :one
|
||||
SELECT
|
||||
ST_Distance(
|
||||
(SELECT location FROM points_of_interest WHERE id = $1),
|
||||
(SELECT location FROM points_of_interest WHERE id = $2)
|
||||
)::float8 as distance_meters; -- ← Force conversion en float64
|
||||
```
|
||||
|
||||
### Index PostGIS pour Performance
|
||||
|
||||
Créer un index GIST pour optimiser les requêtes ST_DWithin :
|
||||
|
||||
```sql
|
||||
-- migrations/002_add_postgis_indexes.up.sql
|
||||
CREATE INDEX idx_poi_location_gist
|
||||
ON points_of_interest USING GIST(location);
|
||||
|
||||
CREATE INDEX idx_user_last_position_gist
|
||||
ON user_locations USING GIST(last_position);
|
||||
```
|
||||
|
||||
### Checklist d'Implémentation
|
||||
|
||||
- [ ] Créer package `backend/internal/geo/types.go` avec wrappers
|
||||
- [ ] Implémenter `Scan/Value` pour conversion automatique
|
||||
- [ ] Écrire requêtes SQL avec `ST_AsGeoJSON()` / `ST_AsText()`
|
||||
- [ ] Ajouter index GIST sur colonnes géographiques
|
||||
- [ ] Documenter patterns dans `backend/README.md`
|
||||
- [ ] Tests d'intégration avec Testcontainers (PostGIS réel)
|
||||
|
||||
### Impact
|
||||
|
||||
- ✅ **Type safety** retrouvée : Plus de `interface{}` opaques
|
||||
- ✅ **Performance** : Index GIST + conversion optimisée
|
||||
- ✅ **Maintenabilité** : Patterns clairs, réutilisables
|
||||
- ❌ **Complexité** : Une couche de plus, mais justifiée
|
||||
|
||||
**Référence** : Résout incohérence #4 dans [INCONSISTENCIES-ANALYSIS.md](../INCONSISTENCIES-ANALYSIS.md#4--orm-sqlc-vs-types-postgis)
|
||||
|
||||
## Conséquences
|
||||
|
||||
- Dépendance : `github.com/sqlc-dev/sqlc`
|
||||
- Fichier `sqlc.yaml` à la racine pour configuration
|
||||
- Migrations gérées séparément avec `golang-migrate`
|
||||
- CI doit exécuter `sqlc generate` pour valider cohérence SQL/Go
|
||||
|
||||
**Librairies** : Voir [ADR-020](020-librairies-go.md) pour stack complet (sqlc + golang-migrate + pgx)
|
||||
|
||||
@@ -29,40 +29,197 @@ RoadWave nécessite applications iOS et Android avec support CarPlay/Android Aut
|
||||
- **Géolocalisation** : `geolocator` robuste avec gestion permissions
|
||||
- **Écosystème** : Widgets riches (Material/Cupertino), state management mature (Bloc, Riverpod)
|
||||
|
||||
## Packages clés
|
||||
## Packages Flutter
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter_bloc: ^8.1.3 # State management
|
||||
just_audio: ^0.9.36 # Lecture audio HLS
|
||||
geolocator: ^11.0.0 # GPS temps réel (mode voiture)
|
||||
geofence_service: ^5.2.0 # Geofencing arrière-plan (mode piéton)
|
||||
flutter_local_notifications: ^17.0.0 # Notifications géolocalisées
|
||||
dio: ^5.4.0 # HTTP client
|
||||
flutter_secure_storage: ^9.0.0 # Tokens JWT
|
||||
cached_network_image: ^3.3.1 # Cache images
|
||||
> **Voir [ADR-022 - Librairies Flutter](022-librairies-flutter.md)** pour la liste complète des packages, licences, alternatives considérées et justifications détaillées.
|
||||
|
||||
**Packages clés pour RoadWave** :
|
||||
|
||||
- **State management** : `flutter_bloc` (pattern BLoC, testable, reactive)
|
||||
- **Audio HLS** : `just_audio` (HLS natif, buffering adaptatif, background playback)
|
||||
- **GPS temps réel** : `geolocator` (mode voiture haute précision)
|
||||
- **Geofencing** : `geofence_service` (mode piéton, détection rayon 200m, économie batterie)
|
||||
- **Notifications** : `flutter_local_notifications` (compteur dynamique, conformité CarPlay/Android Auto)
|
||||
- **HTTP** : `dio` (client HTTP avec retry logic)
|
||||
- **Stockage sécurisé** : `flutter_secure_storage` (JWT tokens, Keychain iOS, KeyStore Android)
|
||||
- **Cache images** : `cached_network_image` (LRU cache)
|
||||
|
||||
**Points d'attention** :
|
||||
- ⚠️ **Permissions progressives requises** pour `geofence_service` et `geolocator` (voir section "Stratégie de Permissions")
|
||||
- ⚠️ **Licences** : 100% permissives (MIT, BSD-3) - voir ADR-022
|
||||
|
||||
## Stratégie de Permissions (iOS/Android)
|
||||
|
||||
### Contexte et Enjeux
|
||||
|
||||
**Problème** : La géolocalisation en arrière-plan (requise pour le mode piéton) est **très scrutée** par Apple et Google :
|
||||
- **iOS App Store** : Taux de rejet ~70% si permission "Always Location" mal justifiée
|
||||
- **Android Play Store** : `ACCESS_BACKGROUND_LOCATION` nécessite déclaration spéciale depuis Android 10
|
||||
- **RGPD** : Permissions doivent être **optionnelles** et l'app **utilisable sans**
|
||||
|
||||
### Architecture de Permissions Progressive
|
||||
|
||||
**Principe** : Demander le **minimum** au départ, puis proposer upgrade **contextuel** uniquement si besoin.
|
||||
|
||||
#### Niveaux de Permissions
|
||||
|
||||
Trois niveaux de permissions doivent être gérés :
|
||||
|
||||
- **Denied** : Aucune permission → app limitée (contenu national)
|
||||
- **When In Use** : "Quand l'app est ouverte" → mode voiture complet ✅
|
||||
- **Always** : "Toujours" / Background → mode piéton ✅
|
||||
|
||||
#### Étape 1 : Permission de Base (Onboarding)
|
||||
|
||||
**Quand** : Premier lancement de l'app
|
||||
|
||||
**Demande** : `locationWhenInUse` uniquement
|
||||
- iOS : "Allow While Using App"
|
||||
- Android : `ACCESS_FINE_LOCATION`
|
||||
|
||||
**Justification affichée** :
|
||||
```
|
||||
📍 RoadWave a besoin de votre position
|
||||
|
||||
Pour vous proposer du contenu audio adapté
|
||||
à votre localisation en temps réel.
|
||||
|
||||
[Autoriser] [Non merci]
|
||||
```
|
||||
|
||||
**Nouveaux packages (contenus géolocalisés)** :
|
||||
**Si acceptée** : Mode voiture entièrement fonctionnel ✅
|
||||
|
||||
- **`geofence_service`** : Détection entrée/sortie rayon 200m en arrière-plan (mode piéton)
|
||||
- Geofencing natif iOS/Android
|
||||
- Minimise consommation batterie
|
||||
- Supporte notifications push même app fermée
|
||||
**Si refusée** : Mode dégradé (contenus nationaux/régionaux via GeoIP)
|
||||
|
||||
- **`flutter_local_notifications`** : Notifications locales avec compteur dynamique
|
||||
- Notification avec compteur décroissant (7→1) en mode voiture
|
||||
- Icônes personnalisées selon type contenu
|
||||
- Désactivation overlay en mode CarPlay/Android Auto (conformité)
|
||||
#### Étape 2 : Upgrade Optionnel (Contextuel)
|
||||
|
||||
**Quand** : Utilisateur **active explicitement** "Notifications audio-guides piéton" dans Settings
|
||||
|
||||
**Flow** :
|
||||
1. **Écran d'éducation** (requis pour validation stores) :
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ 📍 Notifications audio-guides piéton │
|
||||
├────────────────────────────────────────┤
|
||||
│ Pour vous alerter d'audio-guides à │
|
||||
│ proximité même quand vous marchez avec │
|
||||
│ l'app fermée, RoadWave a besoin de │
|
||||
│ votre position en arrière-plan. │
|
||||
│ │
|
||||
│ Votre position sera utilisée pour : │
|
||||
│ ✅ Détecter monuments à 200m │
|
||||
│ ✅ Vous envoyer une notification │
|
||||
│ │
|
||||
│ Votre position ne sera jamais : │
|
||||
│ ❌ Vendue à des tiers │
|
||||
│ ❌ Utilisée pour de la publicité │
|
||||
│ │
|
||||
│ Cette fonctionnalité est optionnelle. │
|
||||
│ Vous pouvez utiliser RoadWave sans │
|
||||
│ cette permission. │
|
||||
│ │
|
||||
│ [Continuer] [Non merci] │
|
||||
│ │
|
||||
│ Plus d'infos : Politique confidentialité│
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
2. **Demande permission OS** : `locationAlways` / `ACCESS_BACKGROUND_LOCATION`
|
||||
|
||||
3. **Si refusée** :
|
||||
- Toggle "Mode piéton" reste désactivé
|
||||
- Message : "Mode piéton non disponible sans permission arrière-plan"
|
||||
- **App reste pleinement fonctionnelle en mode voiture**
|
||||
|
||||
### Implémentation
|
||||
|
||||
Le service de gestion des permissions (`lib/core/services/location_permission_service.dart`) doit implémenter :
|
||||
|
||||
**Détection du niveau actuel** :
|
||||
- Vérifier le statut de la permission `location` (when in use)
|
||||
- Vérifier le statut de la permission `locationAlways` (background)
|
||||
- Retourner le niveau le plus élevé accordé
|
||||
|
||||
**Demande de permission de base** (Étape 1) :
|
||||
- Demander uniquement la permission `location` (when in use)
|
||||
- Utilisée lors de l'onboarding
|
||||
- Aucun écran d'éducation requis
|
||||
|
||||
**Demande de permission arrière-plan** (Étape 2) :
|
||||
- **Toujours** afficher un écran d'éducation AVANT la demande OS
|
||||
- Demander la permission `locationAlways` (iOS) ou `ACCESS_BACKGROUND_LOCATION` (Android)
|
||||
- Si refusée de manière permanente, proposer l'ouverture des réglages système
|
||||
|
||||
**Gestion des refus** :
|
||||
- Détecter si la permission est refusée de manière permanente
|
||||
- Proposer l'ouverture des réglages de l'appareil avec un message clair
|
||||
- Permettre à l'utilisateur d'annuler
|
||||
|
||||
### Configuration Platform-Specific
|
||||
|
||||
#### iOS (`ios/Runner/Info.plist`)
|
||||
|
||||
**Clés requises** :
|
||||
- `NSLocationWhenInUseUsageDescription` : Décrire l'usage pour le mode voiture (contenu géolocalisé en temps réel)
|
||||
- `NSLocationAlwaysAndWhenInUseUsageDescription` : Décrire l'usage optionnel pour le mode piéton (notifications audio-guides en arrière-plan), mentionner explicitement que c'est optionnel et désactivable
|
||||
- `UIBackgroundModes` : Activer les modes `location` et `remote-notification`
|
||||
|
||||
**Exemple de texte pour `NSLocationAlwaysAndWhenInUseUsageDescription`** :
|
||||
> "Si vous activez les notifications audio-guides piéton, RoadWave peut vous alerter lorsque vous passez près d'un monument ou musée, même quand l'app est en arrière-plan. Cette fonctionnalité est optionnelle et peut être désactivée à tout moment dans les réglages."
|
||||
|
||||
#### Android (`android/app/src/main/AndroidManifest.xml`)
|
||||
|
||||
**Permissions requises** :
|
||||
- `ACCESS_FINE_LOCATION` et `ACCESS_COARSE_LOCATION` : Permission de base (when in use)
|
||||
- `ACCESS_BACKGROUND_LOCATION` : Permission arrière-plan (Android 10+), nécessite justification Play Store
|
||||
- `FOREGROUND_SERVICE` et `FOREGROUND_SERVICE_LOCATION` : Service persistant pour mode piéton (Android 12+)
|
||||
|
||||
**Android Play Store** : Déclaration requise dans Play Console lors de la soumission :
|
||||
- Justification : "Notifications géolocalisées pour audio-guides touristiques en arrière-plan"
|
||||
- Vidéo démo obligatoire montrant le flow de demande de permission
|
||||
|
||||
### Fallbacks et Dégradations Gracieuses
|
||||
|
||||
| Niveau Permission | Mode Voiture | Mode Piéton | Contenus Accessibles |
|
||||
|-------------------|--------------|-------------|---------------------|
|
||||
| **Always** | ✅ Complet | ✅ Complet | Tous (national + hyperlocal) |
|
||||
| **When In Use** | ✅ Complet | ❌ Désactivé | Tous (si app ouverte) |
|
||||
| **Denied** | ⚠️ Limité | ❌ Désactivé | Nationaux/régionaux (GeoIP) |
|
||||
|
||||
**Garantie RGPD** : App est **pleinement utilisable** même avec permission refusée (mode dégradé acceptable).
|
||||
|
||||
### Tests de Validation Stores
|
||||
|
||||
**Checklist App Store (iOS)** :
|
||||
- [ ] Permission "Always" demandée **uniquement** si user active mode piéton
|
||||
- [ ] Écran d'éducation **avant** demande OS (requis iOS 13+)
|
||||
- [ ] App fonctionne sans permission "Always" (validation critique)
|
||||
- [ ] Texte `Info.plist` clair et honnête (pas de tracking publicitaire)
|
||||
|
||||
**Checklist Play Store (Android)** :
|
||||
- [ ] Déclaration `ACCESS_BACKGROUND_LOCATION` avec justification détaillée
|
||||
- [ ] Vidéo démo flow de permissions (< 30s, requis Play Console)
|
||||
- [ ] App fonctionne sans permission background (validation critique)
|
||||
- [ ] Foreground service notification visible en mode piéton (requis Android 12+)
|
||||
|
||||
### Documentation Associée
|
||||
|
||||
- **Guide détaillé** : [/docs/mobile/permissions-strategy.md](../mobile/permissions-strategy.md)
|
||||
- **Règles métier** : [Règle 05 - Mode Piéton](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
|
||||
- **RGPD** : [Règle 02 - Conformité RGPD](../regles-metier/02-conformite-rgpd.md)
|
||||
|
||||
---
|
||||
|
||||
## Structure application
|
||||
|
||||
```
|
||||
lib/
|
||||
├── core/ # Config, DI, routes
|
||||
│ └── services/ # LocationPermissionService, GeofencingService
|
||||
├── data/ # Repositories, API clients
|
||||
├── domain/ # Models, business logic
|
||||
├── presentation/ # UI (screens, widgets, blocs)
|
||||
│ └── dialogs/ # PedestrianModeEducationDialog
|
||||
└── main.dart
|
||||
```
|
||||
|
||||
@@ -72,3 +229,6 @@ lib/
|
||||
- Taille binaire : 8-15 MB (acceptable)
|
||||
- Tests : `flutter_test` pour widgets, `integration_test` pour E2E
|
||||
- CI/CD : Fastlane pour déploiement stores
|
||||
- **Permissions** : Stratégie progressive critique pour validation stores (iOS/Android)
|
||||
- **Validation stores** : Tests requis avec TestFlight beta (iOS) et Internal Testing (Android)
|
||||
- **Documentation** : Justifications permissions détaillées requises pour soumission stores
|
||||
|
||||
@@ -78,7 +78,9 @@ Approche **multi-niveaux** : unitaires, intégration, BDD (Gherkin), E2E, load t
|
||||
- `github.com/stretchr/testify`
|
||||
- `github.com/cucumber/godog`
|
||||
- `github.com/testcontainers/testcontainers-go`
|
||||
- `grafana/k6`
|
||||
- `grafana/k6` (AGPL-3.0, usage interne OK)
|
||||
- Temps CI : ~3-5 min (tests unitaires + BDD)
|
||||
- Tests intégration/E2E : nightly builds (15-30 min)
|
||||
- Load tests : avant chaque release majeure
|
||||
|
||||
**Librairies** : Voir [ADR-020](020-librairies-go.md) pour analyses complètes des frameworks de tests
|
||||
|
||||
@@ -65,6 +65,8 @@ Architecture hybride en **2 phases** :
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
### Architecture de délivrance (serveur vs local)
|
||||
|
||||
| Option | Fonctionne offline | Batterie | Complexité | Limite POI | Précision |
|
||||
|--------|-------------------|----------|------------|------------|-----------|
|
||||
| **WebSocket + FCM (Phase 1)** | ❌ Non | ⭐ Optimale | ⭐ Faible | ∞ | ⭐⭐ Bonne |
|
||||
@@ -72,6 +74,17 @@ Architecture hybride en **2 phases** :
|
||||
| Polling GPS continu | ⭐ Oui | ❌ Critique | ⭐ Faible | ∞ | ⭐⭐⭐ Excellente |
|
||||
| **Hybride (Phase 1+2)** | ⭐ Oui | ⭐ Adaptative | ⚠️ Moyenne | ∞/20 | ⭐⭐⭐ Excellente |
|
||||
|
||||
### Fournisseurs de push notifications
|
||||
|
||||
| Provider | Fiabilité | Coût MVP | Coût 100K users | Self-hosted | Vendor lock-in | Verdict |
|
||||
|----------|-----------|----------|-----------------|-------------|----------------|---------|
|
||||
| **Firebase (choix)** | 99.95% | **0€** | **0€** | ❌ Non | 🔴 Fort (Google) | ✅ Optimal MVP |
|
||||
| OneSignal | 99.95% | 0€ | 500€/mois | ❌ Non | 🔴 Fort | ❌ Plus cher |
|
||||
| Pusher Beams | 99.9% | 0€ | 300€/mois | ❌ Non | 🔴 Fort | ❌ Niche |
|
||||
| Custom WS + APNS/FCM | Votre charge | 5€ | 100€+ | ✅ Oui | 🟢 Aucun | ⚠️ Complexe |
|
||||
| Novu (open source) | 99.9% | 15€ | 50€ | ✅ Oui | 🟢 Aucun | 🟡 Phase 2 |
|
||||
| Brevo API | 99.9% | 0€ | 49€ | ✅ Oui | 🟢 Aucun | ❌ Email seulement |
|
||||
|
||||
## Justification
|
||||
|
||||
### Pourquoi WebSocket et pas HTTP long-polling ?
|
||||
@@ -87,6 +100,37 @@ Architecture hybride en **2 phases** :
|
||||
- **Batterie** : Utilise les mécanismes système (Google Play Services)
|
||||
- **Cross-platform** : API unifiée iOS/Android
|
||||
|
||||
### Incohérence acceptée : Firebase vs self-hosted (ADR-008, ADR-017)
|
||||
|
||||
**Problème** : RoadWave promeut 100% self-hosted + souveraineté française, mais Firebase = dépendance Google Cloud.
|
||||
|
||||
**Réalité technique** : Notifications natives requièrent obligatoirement Google/Apple
|
||||
- **APNS (Apple)** : Seul protocole pour notifications iOS → dépendance Apple inévitable
|
||||
- **FCM (Google)** : Meilleur protocole Android (vs Huawei HMS, Samsung)
|
||||
|
||||
**Alternatives analysées** :
|
||||
1. **Custom WebSocket** (self-hosted) :
|
||||
- ✅ Zéro dépendance externe
|
||||
- ❌ 150+ heures dev (2-3 sprints)
|
||||
- ❌ Maintien de la reliability en-house
|
||||
- ❌ Toujours besoin d'appeler APNS/FCM de toute façon
|
||||
|
||||
2. **Novu (open source self-hosted)** :
|
||||
- ✅ Self-hostable
|
||||
- ❌ Jeune (moins mature)
|
||||
- ❌ Toujours wrapper autour APNS/FCM
|
||||
- ❌ Overhead sans gain réel
|
||||
|
||||
3. **OneSignal / Pusher** :
|
||||
- ❌ Même vendor lock-in que Firebase
|
||||
- ❌ Plus cher (500€+/mois @ 100K users)
|
||||
|
||||
**Décision pragmatique** :
|
||||
- Firebase pour MVP : gratuit + fiabilité + time-to-market
|
||||
- **Mitigation vendor lock-in** : Utiliser abstraction layer (`NotificationProvider` interface)
|
||||
- **Exit path documenté** : Migration vers custom solution < 1 sprint si besoin futur
|
||||
- **Probabilité de changement** : Très basse (MVP gratuit, pas d'incitation financière)
|
||||
|
||||
### Pourquoi limiter le geofencing local à Phase 2 ?
|
||||
|
||||
- **Complexité** : Permissions "Always Location" difficiles à obtenir (taux d'acceptation ~30%)
|
||||
@@ -101,10 +145,17 @@ Architecture hybride en **2 phases** :
|
||||
- ✅ Fonctionne avec HLS pour l'audio (pas de conflit avec ADR-002)
|
||||
- ✅ Scalable : Worker backend peut gérer 10K utilisateurs/seconde avec PostGIS indexé
|
||||
- ✅ Mode offline disponible en Phase 2 sans refonte
|
||||
- ✅ Coût zéro jusqu'à millions de notifications (gratuit MVP + croissance)
|
||||
- ✅ Géolocalisation natif iOS/Android optimisé (moins de batterie)
|
||||
|
||||
### Négatives
|
||||
|
||||
- ❌ Dépendance à Firebase (vendor lock-in) - mitigée par l'utilisation de l'interface FCM standard
|
||||
- ⚠️ **Dépendance Google (Firebase)** : Contradictoire avec ADR-008 (self-hosted) + ADR-017 (souveraineté FR)
|
||||
- Mitigé par abstraction layer (`NotificationProvider` interface) → swap facile si besoin
|
||||
- Exit path documenté pour migration custom (< 1 sprint)
|
||||
- ⚠️ **Données utilisateur chez Google** : Tokens FCM, timestamps notifications
|
||||
- Risque RGPD : Nécessite DPA Google valide
|
||||
- À consulter avec DPO avant déploiement production
|
||||
- ❌ WebSocket nécessite maintien de connexion (charge serveur +10-20%)
|
||||
- ❌ Mode offline non disponible au MVP (déception possible des early adopters)
|
||||
|
||||
@@ -115,6 +166,57 @@ Architecture hybride en **2 phases** :
|
||||
- **ADR-012 (Architecture Backend)** : Ajouter un module `geofencing` avec worker dédié
|
||||
- **ADR-014 (Frontend Mobile)** : Intégrer `firebase_messaging` (Flutter) et gérer permissions
|
||||
|
||||
## Abstraction Layer (Mitigation Vendor Lock-in)
|
||||
|
||||
Pour minimiser le coût de changement future, implémenter une interface abstraite :
|
||||
|
||||
```go
|
||||
// backend/internal/notification/provider.go
|
||||
type NotificationProvider interface {
|
||||
SendNotification(ctx context.Context, token, title, body, deepLink string) error
|
||||
UpdateToken(ctx context.Context, userID, newToken string) error
|
||||
}
|
||||
|
||||
// backend/internal/notification/firebase_provider.go
|
||||
type FirebaseProvider struct {
|
||||
client *messaging.Client
|
||||
}
|
||||
|
||||
func (p *FirebaseProvider) SendNotification(ctx context.Context, token, title, body, deepLink string) error {
|
||||
message := &messaging.Message{
|
||||
Notification: &messaging.Notification{
|
||||
Title: title,
|
||||
Body: body,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"deepLink": deepLink,
|
||||
},
|
||||
Token: token,
|
||||
}
|
||||
_, err := p.client.Send(ctx, message)
|
||||
return err
|
||||
}
|
||||
|
||||
// backend/internal/notification/service.go
|
||||
type NotificationService struct {
|
||||
provider NotificationProvider // ← Interface, pas concrète
|
||||
repo NotificationRepository
|
||||
}
|
||||
```
|
||||
|
||||
**Bénéfice** : Swap Firebase → Custom/Novu sans changer business logic.
|
||||
|
||||
```go
|
||||
// Futur : switch facilement
|
||||
var provider NotificationProvider
|
||||
|
||||
if config.Provider == "firebase" {
|
||||
provider = &FirebaseProvider{...}
|
||||
} else if config.Provider == "custom" {
|
||||
provider = &CustomProvider{...}
|
||||
}
|
||||
```
|
||||
|
||||
## Métriques de Succès
|
||||
|
||||
- Latence notification < 60s après entrée dans rayon 200m
|
||||
|
||||
124
docs/adr/020-librairies-go.md
Normal file
124
docs/adr/020-librairies-go.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# ADR-020 : Librairies Go du Backend
|
||||
|
||||
**Statut** : Accepté
|
||||
**Date** : 2026-01-31
|
||||
|
||||
## Contexte
|
||||
|
||||
Le backend Go de RoadWave nécessite des librairies tierces pour HTTP, base de données, tests, streaming, etc. Le choix doit privilégier :
|
||||
- **Licences permissives** (MIT, Apache-2.0, BSD) sans restrictions commerciales
|
||||
- **Performance** (10M utilisateurs, 100K RPS, p99 < 100ms)
|
||||
- **Maturité** et maintenance active
|
||||
- **Compatibilité** avec PostGIS, HLS, WebRTC
|
||||
|
||||
## Décision
|
||||
|
||||
Utilisation de **16 librairies open-source** avec licences permissives.
|
||||
|
||||
### Core Stack
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification |
|
||||
|-----------|-----------|---------|---------------|
|
||||
| **HTTP Framework** | `gofiber/fiber/v3` | MIT | 36K RPS, WebSocket natif, Express-like |
|
||||
| **PostgreSQL** | `jackc/pgx/v5` | MIT | 30-50% plus rapide, PostGIS natif |
|
||||
| **Redis** | `redis/rueidis` | Apache-2.0 | Client-side caching, GEORADIUS |
|
||||
| **SQL Codegen** | `sqlc-dev/sqlc` | MIT | Type-safe, zero overhead (ADR-013) |
|
||||
| **Migrations** | `golang-migrate/migrate` | MIT | Standard, CLI + library |
|
||||
|
||||
### Tests
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification |
|
||||
|-----------|-----------|---------|---------------|
|
||||
| **Unitaires** | `stretchr/testify` | MIT | Standard Go (27% adoption) |
|
||||
| **BDD** | `cucumber/godog` | MIT | Gherkin natif (ADR-007) |
|
||||
| **Intégration** | `testcontainers/testcontainers-go` | MIT | PostGIS réel dans Docker |
|
||||
| **Load** | `grafana/k6` | AGPL-3.0 | Performant (OK usage interne) |
|
||||
|
||||
### Services Externes
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification |
|
||||
|-----------|-----------|---------|---------------|
|
||||
| **Auth JWT** | `zitadel/zitadel-go/v3` | Apache-2.0 | SDK Zitadel officiel (ADR-008) |
|
||||
| **WebRTC** | `pion/webrtc/v4` | MIT | Pure Go, radio live (ADR-002) |
|
||||
| **WebSocket** | `coder/websocket` | ISC | Minimal, notifications (ADR-019) |
|
||||
| **FCM Push** | `firebase.google.com/go` | BSD-3 | SDK Google officiel (ADR-019) |
|
||||
| **HLS/FFmpeg** | `asticode/go-astiav` | MIT | Bindings FFmpeg n8.0 |
|
||||
|
||||
### Utilitaires
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification |
|
||||
|-----------|-----------|---------|---------------|
|
||||
| **Config** | `spf13/viper` | MIT | Multi-format, env vars |
|
||||
| **Logging** | `rs/zerolog` | MIT | Zero allocation, le plus rapide |
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
Voir [analyse détaillée](../ANALYSE_LIBRAIRIES_GO.md) pour comparatifs complets :
|
||||
- Framework : Fiber vs Gin vs Echo vs Chi
|
||||
- PostgreSQL : pgx vs GORM vs database/sql
|
||||
- Redis : rueidis vs go-redis vs redigo
|
||||
- Codegen : sqlc vs SQLBoiler vs Ent
|
||||
- Logging : zerolog vs zap vs slog
|
||||
|
||||
## Justification
|
||||
|
||||
### Licences
|
||||
- **15/16 librairies** : MIT, Apache-2.0, BSD, ISC (permissives)
|
||||
- **1/16** : AGPL-3.0 (k6 load testing, OK usage interne)
|
||||
- **Compatibilité totale** : Aucun conflit de licence
|
||||
|
||||
### Performance
|
||||
- **Fiber** : 36K RPS (5% plus rapide que Gin/Echo)
|
||||
- **pgx** : 30-50% plus rapide que GORM
|
||||
- **rueidis** : Client-side caching automatique
|
||||
- **zerolog** : Zero allocation, benchmarks 2025
|
||||
|
||||
### Maturité
|
||||
- **Standards** : testify (27% adoption), golang-migrate, viper
|
||||
- **Production** : Fiber (33K stars), pgx (10K stars), pion (13K stars)
|
||||
- **Maintenance** : Toutes actives (commits 2025-2026)
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
- ✅ Aucune restriction licence commerciale
|
||||
- ✅ Stack cohérent avec ADR existants (001, 002, 007, 008, 013, 015, 019)
|
||||
- ✅ Performance validée (benchmarks publics)
|
||||
- ✅ Écosystème mature et documenté
|
||||
|
||||
### Négatives
|
||||
- ⚠️ **k6 (AGPL-3.0)** : Copyleft, mais OK pour tests internes (pas de SaaS k6 prévu)
|
||||
- ⚠️ **Firebase FCM** : Dépendance Google (mitigation via abstraction layer, ADR-019)
|
||||
- ❌ Courbe d'apprentissage : 16 librairies à maîtriser (doc nécessaire)
|
||||
|
||||
### Dépendances go.mod
|
||||
|
||||
```go
|
||||
require (
|
||||
github.com/gofiber/fiber/v3 latest
|
||||
github.com/jackc/pgx/v5 latest
|
||||
github.com/redis/rueidis latest
|
||||
github.com/sqlc-dev/sqlc latest
|
||||
github.com/golang-migrate/migrate/v4 latest
|
||||
github.com/stretchr/testify latest
|
||||
github.com/cucumber/godog latest
|
||||
github.com/testcontainers/testcontainers-go latest
|
||||
github.com/zitadel/zitadel-go/v3 latest
|
||||
github.com/pion/webrtc/v4 latest
|
||||
github.com/coder/websocket latest
|
||||
firebase.google.com/go/v4 latest
|
||||
github.com/asticode/go-astiav latest
|
||||
github.com/spf13/viper latest
|
||||
github.com/rs/zerolog latest
|
||||
grafana/k6 latest // dev only
|
||||
)
|
||||
```
|
||||
|
||||
## Références
|
||||
|
||||
- [Analyse complète des librairies](../ANALYSE_LIBRAIRIES_GO.md) (tableaux comparatifs, sources)
|
||||
- ADR-001 : Langage Backend (Fiber, pgx, go-redis)
|
||||
- ADR-007 : Tests BDD (Godog)
|
||||
- ADR-013 : Accès données (sqlc)
|
||||
- ADR-015 : Stratégie tests (testify, testcontainers, k6)
|
||||
- ADR-019 : Notifications (WebSocket, FCM)
|
||||
118
docs/adr/021-geolocalisation-ip.md
Normal file
118
docs/adr/021-geolocalisation-ip.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# ADR-021 : Service de Géolocalisation par IP
|
||||
|
||||
**Statut** : Accepté
|
||||
**Date** : 2026-01-31
|
||||
|
||||
## Contexte
|
||||
|
||||
RoadWave nécessite un service de géolocalisation par IP pour le mode dégradé (utilisateurs sans GPS activé). Ce service permet de détecter la ville/région de l'utilisateur à partir de son adresse IP et d'afficher du contenu régional même sans permission GPS.
|
||||
|
||||
**Évolution du marché** :
|
||||
- **Avant 2019** : MaxMind GeoLite2 était téléchargeable gratuitement (base de données locale)
|
||||
- **Depuis 2019** : MaxMind nécessite un compte + limite 1000 requêtes/jour (gratuit), puis 0.003$/requête au-delà
|
||||
|
||||
**Usage RoadWave** :
|
||||
- Mode dégradé : ~10% des utilisateurs (estimation)
|
||||
- Volume : 1000 utilisateurs × 10% = 100 requêtes/jour (MVP)
|
||||
- Critère : Aucune dépendance à un service tiers payant
|
||||
|
||||
## Décision
|
||||
|
||||
**IP2Location Lite DB** (self-hosted) pour la géolocalisation par IP.
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
| Option | Coût/mois | Précision | Hébergement | Maintenance |
|
||||
|--------|-----------|-----------|-------------|-------------|
|
||||
| **IP2Location Lite** | Gratuit | ±50 km | Self-hosted | Mise à jour mensuelle |
|
||||
| MaxMind GeoLite2 API | ~10€ (dépassement) | ±50 km | Cloud MaxMind | Nulle |
|
||||
| Self-hosted MaxMind | Gratuit | ±50 km | Self-hosted | Compte requis + MAJ |
|
||||
|
||||
## Justification
|
||||
|
||||
### IP2Location Lite (choix retenu)
|
||||
|
||||
**Avantages** :
|
||||
- Gratuit (pas de limite de requêtes)
|
||||
- Self-hosted (souveraineté des données, cohérence avec [ADR-004](004-cdn.md))
|
||||
- Base de données SQLite légère (50-100 MB)
|
||||
- Mise à jour mensuelle automatisable via cron
|
||||
- Licence permissive (Creative Commons BY-SA 4.0)
|
||||
- Pas de compte tiers requis
|
||||
|
||||
**Inconvénients** :
|
||||
- Maintenance mensuelle (mise à jour DB)
|
||||
- Précision équivalente à MaxMind (~±50 km)
|
||||
|
||||
### MaxMind GeoLite2 API (rejeté)
|
||||
|
||||
**Pourquoi rejeté** :
|
||||
- Coût potentiel en cas de dépassement quota (risque faible mais existant)
|
||||
- Dépendance à un service tiers (perte de souveraineté)
|
||||
- Compte requis (friction opérationnelle)
|
||||
|
||||
### Self-hosted MaxMind (rejeté)
|
||||
|
||||
**Pourquoi rejeté** :
|
||||
- Compte MaxMind obligatoire pour télécharger la DB (friction)
|
||||
- Complexité identique à IP2Location pour résultat équivalent
|
||||
- IP2Location offre même fonctionnalité sans compte tiers
|
||||
|
||||
## Architecture technique
|
||||
|
||||
### Composants
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Backend Go<br/>API Handler]
|
||||
B[GeoIP Service<br/>Wrapper Go autour IP2Location]
|
||||
C[IP2Location DB<br/>SQLite ~50 MB<br/>Mise à jour mensuelle]
|
||||
|
||||
A --> B
|
||||
B --> C
|
||||
```
|
||||
|
||||
### Flux de géolocalisation
|
||||
|
||||
1. **Requête** : API reçoit requête utilisateur sans GPS
|
||||
2. **Extraction IP** : Lecture IP depuis en-têtes HTTP (`X-Forwarded-For`, `X-Real-IP`)
|
||||
3. **Lookup DB** : Query SQLite IP2Location (index sur plages IP)
|
||||
4. **Réponse** : Ville + région + code pays
|
||||
|
||||
**Précision attendue** : ±50 km (équivalent MaxMind)
|
||||
|
||||
### Maintenance
|
||||
|
||||
**Mise à jour mensuelle** :
|
||||
- Cron job télécharge nouvelle DB IP2Location (1er du mois)
|
||||
- Backup DB actuelle avant remplacement
|
||||
- Rechargement service GeoIP (hot reload sans downtime)
|
||||
|
||||
**Monitoring** :
|
||||
- Alertes si DB > 60 jours (DB obsolète)
|
||||
- Logs requêtes "IP non trouvée" (détection problèmes DB)
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
- Aucun coût récurrent (gratuit à l'infini)
|
||||
- Souveraineté complète des données (cohérence ADR-004)
|
||||
- Pas de dépendance externe (service tiers)
|
||||
- Latence minimale (lookup local SQLite < 1ms)
|
||||
|
||||
### Négatives
|
||||
- Maintenance mensuelle requise (automatisable)
|
||||
- Précision limitée (±50 km, acceptable pour mode dégradé)
|
||||
- Taille base de données (~50-100 MB sur disque)
|
||||
|
||||
### Risques atténués
|
||||
- **DB obsolète** : Alertes automatiques si > 60 jours
|
||||
- **IP non trouvée** : Fallback "France" par défaut (code pays FR)
|
||||
- **Perte DB** : Backup automatique avant chaque mise à jour
|
||||
|
||||
## Références
|
||||
|
||||
- [ADR-004 : CDN (Souveraineté)](004-cdn.md)
|
||||
- [ADR-017 : Hébergement](017-hebergement.md)
|
||||
- [Règle 02 : RGPD (Mode Dégradé)](../regles-metier/02-conformite-rgpd.md#136-géolocalisation-optionnelle)
|
||||
- IP2Location Lite : https://lite.ip2location.com/
|
||||
207
docs/adr/022-librairies-flutter.md
Normal file
207
docs/adr/022-librairies-flutter.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# ADR-022 : Librairies Flutter du Mobile
|
||||
|
||||
**Statut** : Accepté
|
||||
**Date** : 2026-01-31
|
||||
|
||||
## Contexte
|
||||
|
||||
L'application mobile RoadWave (iOS/Android) nécessite des librairies tierces pour audio HLS, géolocalisation, notifications, state management, etc. Le choix doit privilégier :
|
||||
- **Licences permissives** (MIT, Apache-2.0, BSD) sans restrictions commerciales
|
||||
- **Maturité** et maintenance active (écosystème Flutter)
|
||||
- **Performance native** (pas de bridge JS)
|
||||
- **Support CarPlay/Android Auto**
|
||||
- **Conformité stores** (App Store, Play Store)
|
||||
|
||||
## Décision
|
||||
|
||||
Utilisation de **8 librairies open-source** Flutter avec licences permissives.
|
||||
|
||||
### Core Stack
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification |
|
||||
|-----------|-----------|---------|---------------|
|
||||
| **State Management** | `flutter_bloc` | MIT | Pattern BLoC, 11K+ stars, reactive streams |
|
||||
| **Audio HLS** | `just_audio` | MIT | HLS natif, buffering adaptatif, background playback |
|
||||
| **HTTP Client** | `dio` | MIT | Interceptors, retry logic, 12K+ stars |
|
||||
| **Stockage sécurisé** | `flutter_secure_storage` | BSD-3 | Keychain iOS, KeyStore Android |
|
||||
| **Cache images** | `cached_network_image` | MIT | LRU cache, placeholder support |
|
||||
|
||||
### Géolocalisation & Permissions
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification |
|
||||
|-----------|-----------|---------|---------------|
|
||||
| **GPS temps réel** | `geolocator` | MIT | Mode voiture, high accuracy, background modes |
|
||||
| **Geofencing** | `geofence_service` | MIT | Détection rayon 200m, mode piéton, économie batterie |
|
||||
| **Notifications locales** | `flutter_local_notifications` | BSD-3 | Compteur dynamique, icônes custom, iOS/Android |
|
||||
|
||||
### Packages Additionnels (CarPlay/Android Auto)
|
||||
|
||||
| Catégorie | Librairie | Licence | Justification |
|
||||
|-----------|-----------|---------|---------------|
|
||||
| **CarPlay** | `flutter_carplay` | MIT | Intégration CarPlay native (communautaire) |
|
||||
| **Android Auto** | `android_auto_flutter` | Apache-2.0 | Support Android Auto (communautaire) |
|
||||
| **Permissions** | `permission_handler` | MIT | Gestion unifiée permissions iOS/Android |
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
### State Management
|
||||
- **flutter_bloc** (choisi) : Pattern BLoC, testable, reactive
|
||||
- **riverpod** : Plus moderne, moins mature
|
||||
- **provider** : Simple mais limité pour app complexe
|
||||
- **getx** : Performance mais opinions controversées
|
||||
|
||||
### Audio
|
||||
- **just_audio** (choisi) : HLS natif, communauté active
|
||||
- **audioplayers** : Moins mature pour streaming
|
||||
- **flutter_sound** : Orienté recording, pas streaming
|
||||
|
||||
### Géolocalisation
|
||||
- **geolocator** (choisi) : Standard Flutter, 1.2K+ stars
|
||||
- **location** : Moins maintenu
|
||||
- **background_location** : Spécifique background uniquement
|
||||
|
||||
## Justification
|
||||
|
||||
### Licences
|
||||
- **7/8 librairies** : MIT (permissive totale)
|
||||
- **1/8** : BSD-3 (permissive, compatible commercial)
|
||||
- **Compatibilité totale** : Aucun conflit de licence, aucune restriction commerciale
|
||||
|
||||
### Maturité
|
||||
- **flutter_bloc** : 11.6K stars, adoption large (state management standard)
|
||||
- **just_audio** : 900+ stars, utilisé production (podcasts apps)
|
||||
- **geolocator** : 1.2K stars, maintenu BaseFlow (entreprise Flutter)
|
||||
- **dio** : 12K+ stars, client HTTP le plus utilisé Flutter
|
||||
|
||||
### Performance
|
||||
- **Compilation native** : Dart → ARM64 (pas de bridge JS comme React Native)
|
||||
- **just_audio** : Utilise AVPlayer (iOS) et ExoPlayer (Android) natifs
|
||||
- **geolocator** : Accès direct CoreLocation (iOS) et FusedLocation (Android)
|
||||
- **geofence_service** : Geofencing natif, minimise consommation batterie
|
||||
|
||||
### Conformité Stores
|
||||
- **Permissions progressives** : `permission_handler` + stratégie ADR-014
|
||||
- **Background modes** : `geolocator` + `geofence_service` approuvés stores
|
||||
- **Notifications** : `flutter_local_notifications` conforme guidelines iOS/Android
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph UI["Presentation Layer"]
|
||||
Widgets["Flutter Widgets"]
|
||||
Bloc["flutter_bloc<br/>(State Management)"]
|
||||
end
|
||||
|
||||
subgraph Data["Data Layer"]
|
||||
API["dio<br/>(HTTP Client)"]
|
||||
Storage["flutter_secure_storage<br/>(JWT Tokens)"]
|
||||
Cache["cached_network_image<br/>(Image Cache)"]
|
||||
end
|
||||
|
||||
subgraph Services["Services Layer"]
|
||||
Audio["just_audio<br/>(HLS Streaming)"]
|
||||
GPS["geolocator<br/>(GPS Mode Voiture)"]
|
||||
Geofence["geofence_service<br/>(Mode Piéton)"]
|
||||
Notif["flutter_local_notifications<br/>(Alerts Locales)"]
|
||||
Perms["permission_handler<br/>(Permissions iOS/Android)"]
|
||||
end
|
||||
|
||||
subgraph Platform["Platform Integration"]
|
||||
CarPlay["flutter_carplay<br/>(iOS)"]
|
||||
AndroidAuto["android_auto_flutter<br/>(Android)"]
|
||||
end
|
||||
|
||||
Widgets --> Bloc
|
||||
Bloc --> API
|
||||
Bloc --> Audio
|
||||
Bloc --> GPS
|
||||
Bloc --> Geofence
|
||||
|
||||
API --> Storage
|
||||
Widgets --> Cache
|
||||
|
||||
GPS --> Perms
|
||||
Geofence --> Perms
|
||||
Geofence --> Notif
|
||||
|
||||
Audio --> CarPlay
|
||||
Audio --> AndroidAuto
|
||||
|
||||
classDef uiStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
|
||||
classDef dataStyle fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
|
||||
classDef serviceStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
|
||||
classDef platformStyle fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
|
||||
|
||||
class UI,Widgets,Bloc uiStyle
|
||||
class Data,API,Storage,Cache dataStyle
|
||||
class Services,Audio,GPS,Geofence,Notif,Perms serviceStyle
|
||||
class Platform,CarPlay,AndroidAuto platformStyle
|
||||
```
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
- ✅ Aucune restriction licence commerciale (100% permissif)
|
||||
- ✅ Stack cohérent avec ADR-014 (Frontend Mobile)
|
||||
- ✅ Performance native (compilation ARM64 directe)
|
||||
- ✅ Écosystème mature et documenté
|
||||
- ✅ Support CarPlay/Android Auto via communauté
|
||||
- ✅ Conformité stores (permissions progressives)
|
||||
|
||||
### Négatives
|
||||
- ⚠️ **CarPlay/Android Auto** : Packages communautaires (pas officiels Flutter)
|
||||
- ⚠️ **Géolocalisation background** : Scrutée par App Store (stratégie progressive requise, ADR-014)
|
||||
- ❌ **Courbe d'apprentissage** : Dart + pattern BLoC à maîtriser
|
||||
- ❌ **Tests stores** : Validation TestFlight (iOS) et Internal Testing (Android) obligatoires
|
||||
|
||||
### Dépendances pubspec.yaml
|
||||
|
||||
> **Note** : Les versions exactes seront définies lors de l'implémentation. Cette section indique les packages requis, non les versions à utiliser (qui évoluent rapidement dans l'écosystème Flutter).
|
||||
|
||||
**Core** :
|
||||
- `flutter_bloc` - State management
|
||||
- `just_audio` - Audio HLS streaming
|
||||
- `dio` - HTTP client
|
||||
- `flutter_secure_storage` - Stockage sécurisé JWT
|
||||
- `cached_network_image` - Cache images
|
||||
|
||||
**Géolocalisation & Notifications** :
|
||||
- `geolocator` - GPS haute précision
|
||||
- `geofence_service` - Geofencing arrière-plan
|
||||
- `flutter_local_notifications` - Notifications locales
|
||||
- `permission_handler` - Gestion permissions
|
||||
|
||||
**CarPlay/Android Auto** (optionnels MVP) :
|
||||
- `flutter_carplay` - Intégration CarPlay
|
||||
- `android_auto_flutter` - Support Android Auto
|
||||
|
||||
### Migration depuis ADR-014
|
||||
|
||||
La section "Packages clés" de l'ADR-014 est désormais obsolète et doit référencer cet ADR :
|
||||
|
||||
> **Packages Flutter** : Voir [ADR-022 - Librairies Flutter](020-librairies-flutter.md) pour la liste complète, licences et justifications.
|
||||
|
||||
## Risques et Mitigations
|
||||
|
||||
### Risque 1 : CarPlay/Android Auto packages communautaires
|
||||
- **Impact** : Maintenance non garantie par Flutter team
|
||||
- **Mitigation** : Fork privé si besoin, contribution upstream, ou développement custom si critique
|
||||
|
||||
### Risque 2 : Validation App Store (permissions background)
|
||||
- **Impact** : Taux de rejet ~70% si mal justifié
|
||||
- **Mitigation** : Stratégie progressive (ADR-014), écrans d'éducation, tests beta TestFlight
|
||||
|
||||
### Risque 3 : Performance audio HLS en arrière-plan
|
||||
- **Impact** : Interruptions si OS tue l'app
|
||||
- **Mitigation** : Background audio task iOS, foreground service Android (natif dans `just_audio`)
|
||||
|
||||
## Références
|
||||
|
||||
- ADR-014 : Frontend Mobile (Flutter, architecture permissions)
|
||||
- ADR-020 : Librairies Go (même format de documentation)
|
||||
- [flutter_bloc documentation](https://bloclibrary.dev/)
|
||||
- [just_audio repository](https://pub.dev/packages/just_audio)
|
||||
- [geolocator documentation](https://pub.dev/packages/geolocator)
|
||||
- [Apple CarPlay Developer Guide](https://developer.apple.com/carplay/)
|
||||
- [Android Auto Developer Guide](https://developer.android.com/training/cars)
|
||||
146
docs/adr/023-solution-cache.md
Normal file
146
docs/adr/023-solution-cache.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# ADR-023 : Solution de Cache
|
||||
|
||||
**Statut** : Accepté
|
||||
**Date** : 2026-01-31
|
||||
|
||||
## Contexte
|
||||
|
||||
L'application nécessite un système de cache performant pour plusieurs cas d'usage critiques :
|
||||
|
||||
- **Cache de géolocalisation** : Requêtes de proximité géographique intensives (contenus à moins de X mètres)
|
||||
- **Sessions utilisateurs** : Stockage temporaire des tokens JWT et contexte utilisateur
|
||||
- **Données de référence** : Métadonnées des contenus audio fréquemment consultés
|
||||
- **Compteurs en temps réel** : Nombre d'écoutes, statistiques d'engagement
|
||||
- **Rate limiting** : Protection contre les abus API
|
||||
|
||||
Les contraintes de performance sont strictes :
|
||||
- Latence p99 < 5ms pour les requêtes de cache
|
||||
- Support de 100K+ requêtes/seconde en lecture
|
||||
- Persistance optionnelle (données non critiques)
|
||||
- Clustering pour haute disponibilité
|
||||
|
||||
## Décision
|
||||
|
||||
**Redis 7+ en mode Cluster** sera utilisé comme solution de cache principale.
|
||||
|
||||
Configuration :
|
||||
- Mode Cluster avec 3 nœuds minimum (haute disponibilité)
|
||||
- Persistence RDB désactivée pour les caches chauds (performance maximale)
|
||||
- AOF activé uniquement pour les sessions utilisateurs (durabilité)
|
||||
- Éviction `allkeys-lru` sur les caches non-critiques
|
||||
|
||||
## Alternatives considérées
|
||||
|
||||
| Critère | Redis | Memcached | KeyDB | Valkey |
|
||||
|---------|-------|-----------|-------|--------|
|
||||
| Géospatial natif | ✅ `GEORADIUS` | ❌ | ✅ | ✅ |
|
||||
| Structures de données | ✅ Sets, Hashes, Sorted Sets | ❌ Clé-valeur simple | ✅ | ✅ |
|
||||
| Clustering | ✅ Redis Cluster | ✅ Client-side | ✅ | ✅ |
|
||||
| Pub/Sub | ✅ | ❌ | ✅ | ✅ |
|
||||
| Écosystème Go | ✅ `go-redis/redis` | ⚠️ Limité | ✅ Compatible | ✅ Compatible |
|
||||
| Maturité | ✅ Très mature | ✅ Mature | ⚠️ Fork récent | ⚠️ Fork très récent |
|
||||
| License | ⚠️ RSALv2 / SSPLv1 | ✅ BSD | ✅ BSD | ✅ BSD |
|
||||
|
||||
**Memcached** : Écarté pour l'absence de fonctionnalités géospatiales natives et de structures de données avancées (pas de sets, hashes).
|
||||
|
||||
**KeyDB** : Fork multi-thread de Redis compatible API. Écarté par manque de maturité relative et d'écosystème comparé à Redis (moins de contributions, documentation).
|
||||
|
||||
**Valkey** : Fork Linux Foundation de Redis (2024). Trop récent pour production, écosystème en construction. À réévaluer en 2026.
|
||||
|
||||
## Justification
|
||||
|
||||
### Fonctionnalités géospatiales natives
|
||||
|
||||
Redis fournit des commandes géospatiales optimisées critiques pour RoadWave :
|
||||
|
||||
- `GEOADD` : Indexation de contenus géolocalisés
|
||||
- `GEORADIUS` : Recherche par rayon (ex: contenus à moins de 5km)
|
||||
- `GEODIST` : Calcul de distance entre deux points
|
||||
|
||||
Ces commandes permettent de servir les requêtes de proximité directement depuis le cache sans solliciter PostgreSQL/PostGIS, réduisant la latence de 50-80ms à <5ms.
|
||||
|
||||
### Structures de données riches
|
||||
|
||||
- **Hashes** : Métadonnées de contenus (titre, durée, URL HLS) → accès partiel efficace
|
||||
- **Sets** : Listes de contenus par catégorie, gestion de favoris
|
||||
- **Sorted Sets** : Classement par popularité, top écoutes hebdomadaires
|
||||
- **Strings avec TTL** : Sessions utilisateurs avec expiration automatique
|
||||
|
||||
### Performance et scalabilité
|
||||
|
||||
- **Débit** : 100K+ ops/sec par nœud en lecture (benchmark Redis Labs)
|
||||
- **Latence** : p99 < 1ms pour GET/SET simple
|
||||
- **Clustering** : Partitionnement automatique des données sur 16384 hash slots
|
||||
- **Réplication** : Read replicas pour scaling horizontal en lecture
|
||||
|
||||
### Écosystème Go
|
||||
|
||||
Librairie `go-redis/redis` (13K+ stars GitHub) :
|
||||
- Support complet Redis Cluster
|
||||
- Pipeline et transactions
|
||||
- Context-aware (intégration Go idiomatique)
|
||||
- Pooling de connexions automatique
|
||||
|
||||
### Pub/Sub pour temps réel
|
||||
|
||||
Support natif de messaging publish/subscribe pour :
|
||||
- Notifications push (invalidation de cache)
|
||||
- Événements temps réel (nouveau contenu géolocalisé)
|
||||
- Coordination entre instances API (scaling horizontal)
|
||||
|
||||
## Conséquences
|
||||
|
||||
### Positives
|
||||
|
||||
- **Cache géospatial** : Réduction de charge PostgreSQL de ~70% sur requêtes de proximité
|
||||
- **Latence** : p99 < 5ms pour requêtes de contenu en cache (vs ~50ms PostgreSQL)
|
||||
- **Scaling horizontal** : Ajout de nœuds Redis transparent pour l'application
|
||||
- **Polyvalence** : Un seul système pour cache, sessions, rate limiting, pub/sub
|
||||
|
||||
### Négatives
|
||||
|
||||
- **Complexité opérationnelle** : Cluster Redis nécessite monitoring (slots, rebalancing)
|
||||
- **Persistance limitée** : RDB/AOF moins fiable que PostgreSQL → pas pour données critiques
|
||||
- **Consommation mémoire** : Structures riches = overhead vs Memcached (~20% de RAM en plus)
|
||||
|
||||
### Stratégie de cache
|
||||
|
||||
**TTL par type de donnée** :
|
||||
- Métadonnées de contenu : 15 minutes (mise à jour rare)
|
||||
- Résultats géolocalisés : 5 minutes (contenus statiques géographiquement)
|
||||
- Sessions utilisateurs : 24 heures (renouvellement automatique)
|
||||
- Rate limiting : 1 minute (fenêtre glissante)
|
||||
|
||||
**Invalidation** :
|
||||
- Publication de contenu → `DEL` métadonnées + publication Pub/Sub
|
||||
- Modification géolocalisation → `GEOREM` puis `GEOADD`
|
||||
- Logout utilisateur → `DEL` session
|
||||
|
||||
### Configuration production
|
||||
|
||||
**Cluster 3 nœuds** (minimum haute disponibilité) :
|
||||
- 1 master + 2 replicas
|
||||
- Répartition sur 3 zones de disponibilité (anti-affinité)
|
||||
- `cluster-require-full-coverage no` → lecture dégradée si nœud down
|
||||
|
||||
**Mémoire** :
|
||||
- `maxmemory 2gb` par nœud (ajustable selon charge)
|
||||
- `maxmemory-policy allkeys-lru` → éviction automatique anciennes clés
|
||||
|
||||
**Persistance** :
|
||||
- RDB désactivé (`save ""`) pour caches chauds
|
||||
- AOF `appendonly yes` uniquement pour sessions (nœud dédié optionnel)
|
||||
|
||||
### Monitoring
|
||||
|
||||
Métriques critiques à suivre :
|
||||
- Taux de hit/miss par namespace (target >95% hit rate)
|
||||
- Latence p99 par commande (alerter si >10ms)
|
||||
- Fragmentation mémoire (rebalance si >1.5)
|
||||
- Slots distribution dans le cluster
|
||||
|
||||
## Références
|
||||
|
||||
- [Redis Geospatial Documentation](https://redis.io/docs/data-types/geospatial/)
|
||||
- [go-redis/redis](https://github.com/redis/go-redis)
|
||||
- [ADR-005 : Base de Données](./005-base-de-donnees.md) (architecture cache + PostgreSQL)
|
||||
863
docs/mobile/permissions-strategy.md
Normal file
863
docs/mobile/permissions-strategy.md
Normal file
@@ -0,0 +1,863 @@
|
||||
# Stratégie de Permissions Géolocalisation
|
||||
|
||||
**Date** : 2026-01-31
|
||||
**Auteur** : Architecture Mobile RoadWave
|
||||
**Statut** : Approuvé
|
||||
**Version** : 1.0
|
||||
|
||||
---
|
||||
|
||||
## Contexte
|
||||
|
||||
La géolocalisation est **critique** pour RoadWave, mais les permissions arrière-plan sont le **#1 motif de rejet** sur iOS App Store et Android Play Store.
|
||||
|
||||
### Problématiques Identifiées
|
||||
|
||||
#### iOS App Store
|
||||
- **Taux de rejet ~70%** si permission "Always Location" mal justifiée
|
||||
- Apple exige que l'app soit **pleinement utilisable** sans "Always Location"
|
||||
- Textes `Info.plist` scrutés manuellement par reviewers humains
|
||||
- Rejection si suspicion de tracking publicitaire ou vente de données
|
||||
|
||||
#### Android Play Store
|
||||
- Depuis Android 10 : `ACCESS_BACKGROUND_LOCATION` nécessite **déclaration justifiée**
|
||||
- Vidéo démo **obligatoire** montrant le flow de demande (< 30s)
|
||||
- Google vérifie que la permission est **réellement optionnelle**
|
||||
- Foreground service notification **obligatoire** en arrière-plan (Android 12+)
|
||||
|
||||
#### RGPD (Règle 02)
|
||||
- Permissions doivent être **optionnelles**
|
||||
- Utilisateur doit pouvoir **refuser sans pénalité**
|
||||
- App doit fonctionner en **mode dégradé acceptable**
|
||||
|
||||
---
|
||||
|
||||
## Stratégie Progressive (2 Étapes)
|
||||
|
||||
### Vue d'Ensemble
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ ONBOARDING │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Étape 1: Permission "When In Use" │ │
|
||||
│ │ → Mode voiture complet ✅ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ User utilise l'app normalement
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ SETTINGS (Plus tard, si besoin) │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Étape 2: Permission "Always" (optionnelle) │ │
|
||||
│ │ → Mode piéton avec notifications push ✅ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 1 : Permission de Base (Onboarding)
|
||||
|
||||
### Quand
|
||||
- **Premier lancement** de l'app
|
||||
- Avant de pouvoir utiliser les fonctionnalités principales
|
||||
|
||||
### Permission Demandée
|
||||
|
||||
| Platform | Permission | Nom Utilisateur |
|
||||
|----------|-----------|----------------|
|
||||
| **iOS** | `NSLocationWhenInUseUsageDescription` | "Allow While Using App" |
|
||||
| **Android** | `ACCESS_FINE_LOCATION` | "Autorisez uniquement lorsque l'application est en cours d'utilisation" |
|
||||
|
||||
### Flow UI
|
||||
|
||||
**Écran pré-permission** (recommandé pour taux d'acceptation) :
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ 🗺️ Bienvenue sur RoadWave │
|
||||
├────────────────────────────────────────┤
|
||||
│ │
|
||||
│ RoadWave vous propose du contenu audio│
|
||||
│ adapté à votre position en temps réel.│
|
||||
│ │
|
||||
│ Nous avons besoin de votre localisation│
|
||||
│ pour : │
|
||||
│ │
|
||||
│ ✅ Recommander du contenu proche │
|
||||
│ ✅ Détecter votre mode (voiture/piéton)│
|
||||
│ ✅ Synchroniser avec vos trajets │
|
||||
│ │
|
||||
│ [Continuer] │
|
||||
│ │
|
||||
│ Votre vie privée est protégée │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Puis demande système iOS/Android**
|
||||
|
||||
### Si Permission Acceptée
|
||||
|
||||
- Mode voiture **complet** ✅
|
||||
- Détection POI quand app **ouverte**
|
||||
- Recommandations géolocalisées temps réel
|
||||
- **Pas de demande supplémentaire** sauf si user veut mode piéton
|
||||
|
||||
### Si Permission Refusée
|
||||
|
||||
**Mode dégradé (IP2Location)** :
|
||||
- Détection pays/ville via adresse IP (IP2Location Lite, voir [ADR-021](../adr/021-geolocalisation-ip.md))
|
||||
- Contenus nationaux et régionaux disponibles
|
||||
- Pas de contenus hyperlocaux (< 10km)
|
||||
|
||||
**UI** :
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ ⚠️ Géolocalisation désactivée │
|
||||
├────────────────────────────────────────┤
|
||||
│ Vous écoutez des contenus de votre │
|
||||
│ région (détection approximative). │
|
||||
│ │
|
||||
│ Pour débloquer les contenus proches : │
|
||||
│ [Activer la géolocalisation] │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Tap "Activer"** → `openAppSettings()` (réglages système)
|
||||
|
||||
### Code d'Implémentation
|
||||
|
||||
```dart
|
||||
// lib/presentation/onboarding/location_onboarding_screen.dart
|
||||
|
||||
class LocationOnboardingScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.map, size: 80, color: Colors.blue),
|
||||
SizedBox(height: 32),
|
||||
Text(
|
||||
'Bienvenue sur RoadWave',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'RoadWave vous propose du contenu audio '
|
||||
'adapté à votre position en temps réel.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 32),
|
||||
_buildFeatureList(),
|
||||
SizedBox(height: 48),
|
||||
ElevatedButton(
|
||||
onPressed: () => _requestLocationPermission(context),
|
||||
child: Text('Continuer'),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Votre vie privée est protégée',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFeatureList() {
|
||||
return Column(
|
||||
children: [
|
||||
_buildFeature('Recommander du contenu proche'),
|
||||
_buildFeature('Détecter votre mode (voiture/piéton)'),
|
||||
_buildFeature('Synchroniser avec vos trajets'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFeature(String text) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.green),
|
||||
SizedBox(width: 16),
|
||||
Expanded(child: Text(text)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _requestLocationPermission(BuildContext context) async {
|
||||
final service = context.read<LocationPermissionService>();
|
||||
final granted = await service.requestBasicPermission();
|
||||
|
||||
if (granted) {
|
||||
// Navigation vers écran principal
|
||||
Navigator.pushReplacementNamed(context, '/home');
|
||||
} else {
|
||||
// Afficher mode dégradé disponible
|
||||
_showDegradedModeDialog(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _showDegradedModeDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Géolocalisation désactivée'),
|
||||
content: Text(
|
||||
'Vous pouvez toujours utiliser RoadWave avec des contenus '
|
||||
'de votre région (détection approximative).',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pushReplacementNamed(context, '/home');
|
||||
},
|
||||
child: Text('Continuer sans GPS'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
await openAppSettings();
|
||||
},
|
||||
child: Text('Ouvrir réglages'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 2 : Permission Arrière-Plan (Optionnelle)
|
||||
|
||||
### Quand
|
||||
- User **active explicitement** "Notifications audio-guides piéton" dans Settings
|
||||
- **Jamais au premier lancement**
|
||||
|
||||
### Permission Demandée
|
||||
|
||||
| Platform | Permission | Nom Utilisateur |
|
||||
|----------|-----------|----------------|
|
||||
| **iOS** | `NSLocationAlwaysAndWhenInUseUsageDescription` | "Allow Always" |
|
||||
| **Android** | `ACCESS_BACKGROUND_LOCATION` | "Toujours autoriser" |
|
||||
|
||||
### Flow UI (Critique pour Validation Stores)
|
||||
|
||||
**1. Toggle dans Settings**
|
||||
|
||||
```
|
||||
Settings > Notifications
|
||||
┌────────────────────────────────────────┐
|
||||
│ 🔔 Notifications │
|
||||
├────────────────────────────────────────┤
|
||||
│ Recommendations de contenu │
|
||||
│ ├─ En conduite [ON] │
|
||||
│ └─ Au volant [ON] │
|
||||
│ │
|
||||
│ Audio-guides piéton [OFF] │
|
||||
│ ⓘ Nécessite localisation arrière-plan │
|
||||
│ │
|
||||
│ Live de créateurs suivis [ON] │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**2. Écran d'éducation (OBLIGATOIRE avant demande OS)**
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ 📍 Notifications audio-guides piéton │
|
||||
├────────────────────────────────────────┤
|
||||
│ Pour vous alerter d'audio-guides à │
|
||||
│ proximité même quand vous marchez avec │
|
||||
│ l'app fermée, RoadWave a besoin de │
|
||||
│ votre position en arrière-plan. │
|
||||
│ │
|
||||
│ 🔍 Votre position sera utilisée pour : │
|
||||
│ ✅ Détecter monuments à 200m │
|
||||
│ ✅ Vous envoyer une notification │
|
||||
│ │
|
||||
│ 🔒 Votre position ne sera jamais : │
|
||||
│ ❌ Vendue à des tiers │
|
||||
│ ❌ Utilisée pour de la publicité │
|
||||
│ ❌ Partagée sans votre consentement │
|
||||
│ │
|
||||
│ Cette fonctionnalité est optionnelle. │
|
||||
│ Vous pouvez utiliser RoadWave sans │
|
||||
│ cette permission. │
|
||||
│ │
|
||||
│ [Continuer] [Non merci] │
|
||||
│ │
|
||||
│ Plus d'infos : Politique confidentialité│
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**3. Demande système iOS/Android**
|
||||
|
||||
**4. Si permission accordée**
|
||||
|
||||
```
|
||||
✅ Mode piéton activé !
|
||||
|
||||
Vous recevrez une notification lorsque
|
||||
vous passez près d'un audio-guide.
|
||||
```
|
||||
|
||||
**5. Si permission refusée**
|
||||
|
||||
```
|
||||
⚠️ Mode piéton non disponible
|
||||
|
||||
Sans permission "Toujours autoriser",
|
||||
nous ne pouvons pas détecter les
|
||||
audio-guides en arrière-plan.
|
||||
|
||||
Vous pouvez toujours :
|
||||
✅ Utiliser le mode voiture
|
||||
✅ Lancer manuellement les audio-guides
|
||||
|
||||
[Ouvrir réglages] [Fermer]
|
||||
```
|
||||
|
||||
### Code d'Implémentation
|
||||
|
||||
```dart
|
||||
// lib/presentation/settings/notifications_settings_screen.dart
|
||||
|
||||
class NotificationsSettingsScreen extends StatefulWidget {
|
||||
@override
|
||||
_NotificationsSettingsScreenState createState() => _NotificationsSettingsScreenState();
|
||||
}
|
||||
|
||||
class _NotificationsSettingsScreenState extends State<NotificationsSettingsScreen> {
|
||||
bool _pedestrianModeEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadPermissionStatus();
|
||||
}
|
||||
|
||||
Future<void> _loadPermissionStatus() async {
|
||||
final service = context.read<LocationPermissionService>();
|
||||
final level = await service.getCurrentLevel();
|
||||
setState(() {
|
||||
_pedestrianModeEnabled = (level == LocationPermissionLevel.always);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Notifications')),
|
||||
body: ListView(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: Text('Recommendations de contenu'),
|
||||
subtitle: Text('En conduite'),
|
||||
value: true,
|
||||
onChanged: (value) { /* ... */ },
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text('Audio-guides piéton'),
|
||||
subtitle: Text('Nécessite localisation arrière-plan'),
|
||||
value: _pedestrianModeEnabled,
|
||||
onChanged: _handlePedestrianModeToggle,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handlePedestrianModeToggle(bool enabled) async {
|
||||
if (enabled) {
|
||||
// User veut activer → demander permission
|
||||
final granted = await _requestBackgroundPermission();
|
||||
setState(() {
|
||||
_pedestrianModeEnabled = granted;
|
||||
});
|
||||
} else {
|
||||
// User veut désactiver → juste disable service
|
||||
setState(() {
|
||||
_pedestrianModeEnabled = false;
|
||||
});
|
||||
// Arrêter geofencing service
|
||||
context.read<GeofencingService>().stop();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _requestBackgroundPermission() async {
|
||||
// Étape 1: Afficher écran d'éducation
|
||||
final userWantsToContinue = await _showEducationDialog();
|
||||
if (!userWantsToContinue) return false;
|
||||
|
||||
// Étape 2: Demander permission OS
|
||||
final service = context.read<LocationPermissionService>();
|
||||
final granted = await service.requestBackgroundPermission(context: context);
|
||||
|
||||
if (granted) {
|
||||
_showSuccessDialog();
|
||||
// Démarrer geofencing service
|
||||
context.read<GeofencingService>().start();
|
||||
} else {
|
||||
_showDeniedDialog();
|
||||
}
|
||||
|
||||
return granted;
|
||||
}
|
||||
|
||||
Future<bool> _showEducationDialog() async {
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('📍 Notifications audio-guides piéton'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Pour vous alerter d\'audio-guides à proximité '
|
||||
'même quand vous marchez avec l\'app fermée, '
|
||||
'RoadWave a besoin de votre position en arrière-plan.',
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text('🔍 Votre position sera utilisée pour :',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
_buildListItem('Détecter monuments à 200m'),
|
||||
_buildListItem('Vous envoyer une notification'),
|
||||
SizedBox(height: 16),
|
||||
Text('🔒 Votre position ne sera jamais :',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
_buildListItem('Vendue à des tiers', isNegative: true),
|
||||
_buildListItem('Utilisée pour de la publicité', isNegative: true),
|
||||
_buildListItem('Partagée sans votre consentement', isNegative: true),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Cette fonctionnalité est optionnelle. '
|
||||
'Vous pouvez utiliser RoadWave sans cette permission.',
|
||||
style: TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text('Non merci'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text('Continuer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
Widget _buildListItem(String text, {bool isNegative = false}) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isNegative ? Icons.cancel : Icons.check_circle,
|
||||
color: isNegative ? Colors.red : Colors.green,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(child: Text(text)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showSuccessDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('✅ Mode piéton activé !'),
|
||||
content: Text(
|
||||
'Vous recevrez une notification lorsque vous passez près d\'un audio-guide.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeniedDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('⚠️ Mode piéton non disponible'),
|
||||
content: Text(
|
||||
'Sans permission "Toujours autoriser", nous ne pouvons pas '
|
||||
'détecter les audio-guides en arrière-plan.\n\n'
|
||||
'Vous pouvez toujours :\n'
|
||||
'✅ Utiliser le mode voiture\n'
|
||||
'✅ Lancer manuellement les audio-guides',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('Fermer'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
openAppSettings();
|
||||
},
|
||||
child: Text('Ouvrir réglages'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tableau de Dégradation Gracieuse
|
||||
|
||||
| Niveau Permission | Mode Voiture | Mode Piéton | Contenus Hyperlocaux | Notifications |
|
||||
|-------------------|--------------|-------------|---------------------|--------------|
|
||||
| **Always** | ✅ Complet | ✅ Complet | ✅ Tous | Push en arrière-plan |
|
||||
| **When In Use** | ✅ Complet | ❌ Désactivé | ✅ Si app ouverte | Sonores (app ouverte) |
|
||||
| **Denied** | ⚠️ IP2Location (ville) | ❌ Désactivé | ❌ Aucun | Aucune |
|
||||
|
||||
**Garanties** :
|
||||
- App **utilisable** à tous niveaux de permission ✅
|
||||
- Pas de fonctionnalité **bloquante** sans permission ✅
|
||||
- Mode dégradé **acceptable** (contenus régionaux) ✅
|
||||
|
||||
---
|
||||
|
||||
## Configuration Plateformes
|
||||
|
||||
### iOS (`ios/Runner/Info.plist`)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- ÉTAPE 1: Permission "When In Use" -->
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>RoadWave utilise votre position pour vous proposer des contenus audio géolocalisés adaptés à votre trajet en temps réel.</string>
|
||||
|
||||
<!-- ÉTAPE 2: Permission "Always" (optionnelle) -->
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>Si vous activez les notifications audio-guides piéton, RoadWave peut vous alerter lorsque vous passez près d'un monument ou musée, même quand l'app est en arrière-plan. Cette fonctionnalité est optionnelle et peut être désactivée à tout moment dans les réglages.</string>
|
||||
|
||||
<!-- Background modes -->
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>location</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
|
||||
<!-- Privacy - Location Always Usage Description (fallback iOS < 11) -->
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
<string>Si vous activez les notifications audio-guides piéton, RoadWave peut vous alerter lorsque vous passez près d'un monument ou musée, même quand l'app est en arrière-plan.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
### Android (`android/app/src/main/AndroidManifest.xml`)
|
||||
|
||||
```xml
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.roadwave.app">
|
||||
|
||||
<!-- ÉTAPE 1: Permission "When In Use" -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<!-- ÉTAPE 2: Permission "Always" (Android 10+, optionnelle) -->
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
|
||||
<!-- Foreground service (requis Android 12+ pour background location) -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
|
||||
<!-- Notifications -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:label="RoadWave"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
|
||||
<!-- Foreground service declaration -->
|
||||
<service
|
||||
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
|
||||
android:foregroundServiceType="location"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- ... rest of manifest ... -->
|
||||
</application>
|
||||
</manifest>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist Validation Stores
|
||||
|
||||
### iOS App Store
|
||||
|
||||
- [ ] Permission "Always" demandée **uniquement** après activation explicite mode piéton
|
||||
- [ ] Écran d'éducation **avant** demande OS (avec raisons claires)
|
||||
- [ ] Texte `NSLocationAlwaysAndWhenInUseUsageDescription` mentionne :
|
||||
- [ ] Fonctionnalité précise ("audio-guides piéton")
|
||||
- [ ] **Optionnalité** ("Cette fonctionnalité est optionnelle")
|
||||
- [ ] Pas de mention tracking/publicité
|
||||
- [ ] App fonctionne **complètement** avec permission "When In Use" uniquement
|
||||
- [ ] App fonctionne en **mode dégradé** sans aucune permission (IP2Location)
|
||||
- [ ] Screenshots montrant app fonctionnelle sans permission "Always"
|
||||
- [ ] Video demo flow de permissions (< 1 min, optionnel mais recommandé)
|
||||
|
||||
### Android Play Store
|
||||
|
||||
- [ ] Déclaration `ACCESS_BACKGROUND_LOCATION` avec justification dans Play Console :
|
||||
- [ ] "Notifications géolocalisées pour audio-guides touristiques en arrière-plan"
|
||||
- [ ] "Permet aux utilisateurs de recevoir des alertes lorsqu'ils passent près de monuments"
|
||||
- [ ] **Vidéo démo obligatoire** (< 30s) montrant :
|
||||
- [ ] Activation toggle "Mode piéton" dans Settings
|
||||
- [ ] Écran d'éducation pré-permission
|
||||
- [ ] Demande permission système Android
|
||||
- [ ] App fonctionnelle si permission refusée
|
||||
- [ ] Foreground service notification visible en mode piéton (Android 12+)
|
||||
- [ ] App fonctionne **complètement** avec `ACCESS_FINE_LOCATION` uniquement
|
||||
- [ ] App fonctionne en **mode dégradé** sans permissions
|
||||
- [ ] Screenshots montrant app fonctionnelle sans permission background
|
||||
|
||||
---
|
||||
|
||||
## Tests Requis
|
||||
|
||||
### Tests Unitaires
|
||||
|
||||
```dart
|
||||
// test/core/services/location_permission_service_test.dart
|
||||
|
||||
void main() {
|
||||
group('LocationPermissionService', () {
|
||||
test('getCurrentLevel returns denied when no permission', () async {
|
||||
// ...
|
||||
});
|
||||
|
||||
test('getCurrentLevel returns whenInUse with basic permission', () async {
|
||||
// ...
|
||||
});
|
||||
|
||||
test('getCurrentLevel returns always with background permission', () async {
|
||||
// ...
|
||||
});
|
||||
|
||||
test('requestBasicPermission shows system dialog', () async {
|
||||
// ...
|
||||
});
|
||||
|
||||
test('requestBackgroundPermission requires education dialog first', () async {
|
||||
// ...
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Tests d'Intégration
|
||||
|
||||
```dart
|
||||
// integration_test/permissions_flow_test.dart
|
||||
|
||||
void main() {
|
||||
testWidgets('Onboarding flow with permission acceptance', (tester) async {
|
||||
app.main();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Voir écran onboarding
|
||||
expect(find.text('Bienvenue sur RoadWave'), findsOneWidget);
|
||||
|
||||
// Tap continuer
|
||||
await tester.tap(find.text('Continuer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Permission acceptée (mock) → navigation home
|
||||
expect(find.byType(HomeScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Settings pedestrian mode activation flow', (tester) async {
|
||||
// ...
|
||||
await tester.tap(find.byType(SwitchListTile).last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Voir écran d'éducation
|
||||
expect(find.text('Notifications audio-guides piéton'), findsOneWidget);
|
||||
expect(find.text('Votre position sera utilisée pour'), findsOneWidget);
|
||||
|
||||
// Tap continuer
|
||||
await tester.tap(find.text('Continuer'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Vérifier demande système (mock)
|
||||
// ...
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Tests Manuels (Devices Réels)
|
||||
|
||||
**iOS** :
|
||||
- [ ] iPhone avec iOS 14, 15, 16, 17, 18
|
||||
- [ ] Tester flow onboarding permission "When In Use"
|
||||
- [ ] Tester activation mode piéton avec permission "Always"
|
||||
- [ ] Tester refus permission "Always" → app reste fonctionnelle
|
||||
- [ ] Tester changement permission dans Settings iOS → app réagit correctement
|
||||
|
||||
**Android** :
|
||||
- [ ] Android 10, 11, 12, 13, 14, 15
|
||||
- [ ] Tester flow onboarding permission `FINE_LOCATION`
|
||||
- [ ] Tester activation mode piéton avec `BACKGROUND_LOCATION`
|
||||
- [ ] Tester refus permission background → app reste fonctionnelle
|
||||
- [ ] Vérifier foreground notification visible en arrière-plan (Android 12+)
|
||||
|
||||
---
|
||||
|
||||
## Validation TestFlight / Internal Testing
|
||||
|
||||
### Phase 1 : TestFlight Beta (iOS)
|
||||
|
||||
**Objectif** : Valider que Apple accepte notre stratégie de permissions
|
||||
|
||||
**Participants** : 10-20 beta testers externes
|
||||
|
||||
**Durée** : 2 semaines
|
||||
|
||||
**Checklist** :
|
||||
- [ ] Upload build vers TestFlight
|
||||
- [ ] Compléter questionnaire App Store Connect :
|
||||
- [ ] "Why does your app use background location?"
|
||||
→ "To send push notifications when users walk near tourist audio-guides, even when app is closed. This feature is optional and can be disabled in settings."
|
||||
- [ ] Screenshots montrant app fonctionnelle sans permission "Always"
|
||||
- [ ] Attendre review Apple (24-48h)
|
||||
- [ ] Si rejet : analyser feedback, ajuster textes/flow, re-soumettre
|
||||
- [ ] Si accepté : lancer beta test avec testeurs
|
||||
|
||||
**Scénarios de test beta** :
|
||||
1. Installation fresh → onboarding → accepter "When In Use"
|
||||
2. Utiliser mode voiture pendant 1 semaine
|
||||
3. Activer mode piéton dans settings → accepter "Always"
|
||||
4. Vérifier réception notifications push en arrière-plan
|
||||
5. Désactiver mode piéton → vérifier app toujours fonctionnelle
|
||||
|
||||
**Métriques collectées** :
|
||||
- Taux acceptation permission "When In Use" : cible >85%
|
||||
- Taux acceptation permission "Always" : cible >40%
|
||||
- Taux rejet App Review : cible 0%
|
||||
|
||||
### Phase 2 : Internal Testing (Android)
|
||||
|
||||
**Objectif** : Valider conformité Play Store + foreground service
|
||||
|
||||
**Participants** : 5-10 beta testers internes
|
||||
|
||||
**Durée** : 1 semaine
|
||||
|
||||
**Checklist** :
|
||||
- [ ] Upload build vers Play Console (Internal Testing)
|
||||
- [ ] Compléter déclaration permissions :
|
||||
- [ ] `ACCESS_BACKGROUND_LOCATION` justification
|
||||
- [ ] Upload vidéo démo (< 30s)
|
||||
- [ ] Tester sur Android 10, 11, 12, 13, 14, 15
|
||||
- [ ] Vérifier foreground notification visible (Android 12+)
|
||||
|
||||
**Scénarios de test** :
|
||||
1. Installation → onboarding → accepter `FINE_LOCATION`
|
||||
2. Utiliser app mode voiture
|
||||
3. Activer mode piéton → voir écran éducation → accepter `BACKGROUND_LOCATION`
|
||||
4. App en arrière-plan → marcher près d'un POI → vérifier notification push
|
||||
5. Vérifier notification foreground service visible dans panneau notifications
|
||||
|
||||
**Métriques collectées** :
|
||||
- Consommation batterie mode piéton : cible <5% par heure
|
||||
- Taux crash background service : cible <0.1%
|
||||
|
||||
---
|
||||
|
||||
## Vidéo Démo Play Store (Script)
|
||||
|
||||
**Durée** : 25 secondes
|
||||
**Format** : MP4 1080p, portrait
|
||||
**Voix off** : Optionnel
|
||||
|
||||
**Storyboard** :
|
||||
|
||||
| Seconde | Écran | Action | Texte Overlay |
|
||||
|---------|-------|--------|---------------|
|
||||
| 0-5 | Settings > Notifications | Scroll vers "Audio-guides piéton" | "Utilisateur active mode piéton" |
|
||||
| 5-8 | Toggle OFF → ON | Tap toggle | |
|
||||
| 8-15 | Écran d'éducation | Scroll, lire texte | "Écran explicatif affiché" |
|
||||
| 15-18 | Tap "Continuer" | Demande permission Android | "Permission arrière-plan demandée" |
|
||||
| 18-22 | Dialog Android | Tap "Toujours autoriser" | "Utilisateur accepte (optionnel)" |
|
||||
| 22-25 | Retour Settings | Toggle ON | "Mode piéton activé" |
|
||||
|
||||
**Fichier** : `android/play-store-assets/background-location-demo.mp4`
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q1 : Pourquoi ne pas demander "Always" dès le début ?
|
||||
|
||||
**R** : Taux d'acceptation ~15% vs ~85% pour "When In Use". Strategy progressive maximise utilisateurs avec permissions.
|
||||
|
||||
### Q2 : Que se passe-t-il si user change permission dans Settings OS ?
|
||||
|
||||
**R** : App détecte changement via `AppLifecycleState` et `permission_handler`. Si downgrade "Always" → "When In Use", mode piéton désactivé automatiquement avec notification in-app.
|
||||
|
||||
### Q3 : Est-ce que IP2Location suffit pour le MVP ?
|
||||
|
||||
**R** : Non. Mode voiture nécessite GPS précis pour ETA et notifications géolocalisées (règle métier 05). IP2Location = fallback uniquement.
|
||||
|
||||
### Q4 : Combien de temps pour validation TestFlight/Play Store ?
|
||||
|
||||
**R** :
|
||||
- TestFlight : 24-48h (review Apple)
|
||||
- Play Console Internal Testing : Immédiat (pas de review)
|
||||
- Play Console Production : 3-7 jours (review Google)
|
||||
|
||||
---
|
||||
|
||||
## Références
|
||||
|
||||
- **ADR-014** : [Frontend Mobile](../adr/014-frontend-mobile.md)
|
||||
- **Règle 05** : [Mode Piéton](../regles-metier/05-interactions-navigation.md#512-mode-piéton-audio-guides)
|
||||
- **Règle 02** : [Conformité RGPD](../regles-metier/02-conformite-rgpd.md)
|
||||
- **Apple Guidelines** : [Location Best Practices](https://developer.apple.com/design/human-interface-guidelines/location)
|
||||
- **Android Guidelines** : [Request Background Location](https://developer.android.com/training/location/permissions#request-background-location)
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour** : 2026-01-31
|
||||
**Prochaine revue** : Après validation TestFlight (Sprint 3)
|
||||
618
docs/mobile/testflight-validation-plan.md
Normal file
618
docs/mobile/testflight-validation-plan.md
Normal file
@@ -0,0 +1,618 @@
|
||||
# Plan de Validation TestFlight & Play Store
|
||||
|
||||
**Date** : 2026-01-31
|
||||
**Auteur** : QA & Mobile Team RoadWave
|
||||
**Objectif** : Valider stratégie de permissions géolocalisation pour acceptation stores
|
||||
**Statut** : Prêt à exécuter
|
||||
|
||||
---
|
||||
|
||||
## Vue d'Ensemble
|
||||
|
||||
### Objectifs
|
||||
|
||||
1. **Valider acceptation Apple App Store** pour permission "Always Location"
|
||||
2. **Valider acceptation Google Play Store** pour `ACCESS_BACKGROUND_LOCATION`
|
||||
3. **Mesurer taux d'acceptation** utilisateurs réels (permissions progressives)
|
||||
4. **Identifier bugs** flow de permissions sur différents OS/devices
|
||||
5. **Optimiser textes** pour maximiser acceptation
|
||||
|
||||
### Timeline
|
||||
|
||||
```
|
||||
Semaine 1 : Préparation builds + documentation stores
|
||||
Semaine 2-3: Beta testing iOS (TestFlight)
|
||||
Semaine 3-4: Beta testing Android (Internal Testing)
|
||||
Semaine 5 : Corrections + re-soumission si nécessaire
|
||||
Semaine 6 : Validation finale + go/no-go production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 : Préparation (Semaine 1)
|
||||
|
||||
### Checklist Build iOS
|
||||
|
||||
- [ ] **Code freeze** branche `release/testflight-permissions-v1`
|
||||
- [ ] Vérifier `Info.plist` textes permissions :
|
||||
- [ ] `NSLocationWhenInUseUsageDescription` ≤ 200 caractères
|
||||
- [ ] `NSLocationAlwaysAndWhenInUseUsageDescription` ≤ 200 caractères
|
||||
- [ ] Pas de mention tracking/publicité
|
||||
- [ ] Mention explicite "optionnel"
|
||||
- [ ] Vérifier `UIBackgroundModes` contient `location`
|
||||
- [ ] Build & Archive (Xcode)
|
||||
- [ ] Version : `1.0.0 (1)` (beta)
|
||||
- [ ] Bundle ID : `com.roadwave.app`
|
||||
- [ ] Signing : Distribution certificate
|
||||
- [ ] Upload vers App Store Connect
|
||||
- [ ] Attendre processing (15-30 min)
|
||||
|
||||
### Checklist Build Android
|
||||
|
||||
- [ ] **Code freeze** même branche que iOS
|
||||
- [ ] Vérifier `AndroidManifest.xml` permissions :
|
||||
- [ ] `ACCESS_FINE_LOCATION`
|
||||
- [ ] `ACCESS_BACKGROUND_LOCATION`
|
||||
- [ ] `FOREGROUND_SERVICE`
|
||||
- [ ] `FOREGROUND_SERVICE_LOCATION`
|
||||
- [ ] Vérifier foreground service déclaration
|
||||
- [ ] Build AAB (Android App Bundle)
|
||||
- [ ] Version : `1.0.0 (1)`
|
||||
- [ ] Package : `com.roadwave.app`
|
||||
- [ ] Signing : Release keystore
|
||||
- [ ] Upload vers Play Console (Internal Testing)
|
||||
|
||||
### Documentation App Store Connect
|
||||
|
||||
**Questionnaire "Background Location"** :
|
||||
|
||||
**Q1** : "Why does your app use background location?"
|
||||
|
||||
**A1** (réponse exacte) :
|
||||
```
|
||||
RoadWave sends push notifications to users when they walk near tourist
|
||||
audio-guides and monuments, even when the app is closed. This allows
|
||||
tourists to discover local content while exploring a city on foot.
|
||||
|
||||
This feature is entirely optional and can be disabled in the app settings.
|
||||
Users can use RoadWave fully without enabling background location - they
|
||||
will simply use the "car mode" which only requires location "while using".
|
||||
|
||||
Background location is ONLY used for:
|
||||
- Detecting proximity to audio-guide points of interest (200m radius)
|
||||
- Sending a single push notification to alert the user
|
||||
|
||||
Background location is NEVER used for:
|
||||
- Advertising or tracking
|
||||
- Selling data to third parties
|
||||
- Analytics beyond core functionality
|
||||
```
|
||||
|
||||
**Q2** : "How do users benefit from background location?"
|
||||
|
||||
**A2** :
|
||||
```
|
||||
Users walking in a city receive timely notifications about nearby cultural
|
||||
content (museums, monuments, historical sites) without having to keep the
|
||||
app open. This improves the tourist experience while preserving battery life
|
||||
through native iOS geofencing.
|
||||
```
|
||||
|
||||
**Screenshots à fournir** (5 minimum) :
|
||||
1. Onboarding demandant permission "When In Use" uniquement
|
||||
2. App fonctionnelle en mode voiture (avec permission "When In Use")
|
||||
3. Settings montrant toggle "Mode piéton" désactivé
|
||||
4. Écran d'éducation avant demande "Always"
|
||||
5. App fonctionnelle en mode voiture après refus "Always"
|
||||
|
||||
### Documentation Play Console
|
||||
|
||||
**Déclaration Permission Background Location** :
|
||||
|
||||
**Justification** (max 1000 caractères) :
|
||||
```
|
||||
RoadWave utilise ACCESS_BACKGROUND_LOCATION uniquement pour envoyer des
|
||||
notifications push géolocalisées aux utilisateurs en mode piéton (touristes
|
||||
à pied).
|
||||
|
||||
Usage précis :
|
||||
- Geofencing radius 200m autour des points d'intérêt (monuments, musées)
|
||||
- Notification push unique lorsque l'utilisateur entre dans la zone
|
||||
- Permet découverte de contenus audio-guides sans ouvrir l'app
|
||||
|
||||
Cette fonctionnalité est OPTIONNELLE :
|
||||
- Demandée uniquement si utilisateur active "Mode piéton" dans Settings
|
||||
- Écran explicatif affiché AVANT demande permission système
|
||||
- L'app fonctionne pleinement sans cette permission (mode voiture disponible)
|
||||
|
||||
Foreground service notification visible (Android 12+) lorsque geofencing actif.
|
||||
|
||||
Données de localisation :
|
||||
- JAMAIS vendues ou partagées avec tiers
|
||||
- JAMAIS utilisées pour publicité ciblée
|
||||
- Anonymisées après 24h (conformité RGPD)
|
||||
```
|
||||
|
||||
**Vidéo démo** (requis) :
|
||||
- [ ] Enregistrer screen recording (25-30s)
|
||||
- [ ] Montrer activation mode piéton depuis Settings
|
||||
- [ ] Montrer écran d'éducation
|
||||
- [ ] Montrer demande permission système Android
|
||||
- [ ] Montrer app fonctionnelle si refusé
|
||||
- [ ] Format : MP4, 1080p portrait, < 50MB
|
||||
- [ ] Upload vers Play Console (section "Permissions")
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 : Beta Testing iOS (Semaine 2-3)
|
||||
|
||||
### Configuration TestFlight
|
||||
|
||||
**Groupes de testeurs** :
|
||||
|
||||
| Groupe | Nombre | Profil | Objectif |
|
||||
|--------|--------|--------|----------|
|
||||
| **Internal** | 3-5 | Équipe dev/QA | Tests rapides pre-external |
|
||||
| **External 1** | 10-15 | Early adopters tech-savvy | Tests fonctionnels détaillés |
|
||||
| **External 2** | 20-30 | Grand public varié | Tests UX/acceptation réelle |
|
||||
|
||||
**Configuration** :
|
||||
- [ ] Créer groupe "Internal Testers" (accès immédiat)
|
||||
- [ ] Créer groupe "External Beta 1" (review Apple requise, 24-48h)
|
||||
- [ ] Créer groupe "External Beta 2" (après succès Beta 1)
|
||||
- [ ] Activer feedback automatique TestFlight
|
||||
- [ ] Préparer questionnaire post-test (Google Forms)
|
||||
|
||||
### Scénarios de Test (Internal)
|
||||
|
||||
**Durée** : 2-3 jours
|
||||
|
||||
**Devices** :
|
||||
- iPhone 12 (iOS 15)
|
||||
- iPhone 13 Pro (iOS 16)
|
||||
- iPhone 14 (iOS 17)
|
||||
- iPhone 15 Pro (iOS 18)
|
||||
|
||||
**Test Case 1 : Onboarding Fresh Install**
|
||||
```
|
||||
Given: App jamais installée
|
||||
When: Installation depuis TestFlight
|
||||
Then:
|
||||
- Écran onboarding demande permission "When In Use"
|
||||
- Texte clair et rassurant
|
||||
- Acceptation → navigation home
|
||||
- Refus → mode dégradé disponible
|
||||
```
|
||||
|
||||
**Test Case 2 : Mode Voiture (Permission When In Use)**
|
||||
```
|
||||
Given: Permission "When In Use" accordée
|
||||
When: Utilisation normale app pendant 1h de conduite
|
||||
Then:
|
||||
- GPS actif quand app ouverte
|
||||
- Notifications géolocalisées sonores fonctionnent
|
||||
- ETA calcul correct (7s avant POI)
|
||||
- Pas de demande permission supplémentaire
|
||||
```
|
||||
|
||||
**Test Case 3 : Activation Mode Piéton**
|
||||
```
|
||||
Given: App utilisée en mode voiture depuis quelques jours
|
||||
When: User active toggle "Mode piéton" dans Settings
|
||||
Then:
|
||||
- Écran d'éducation s'affiche AVANT demande OS
|
||||
- Texte mentionne "optionnel"
|
||||
- Tap "Continuer" → demande iOS "Allow Always"
|
||||
- Tap "Non merci" → toggle reste OFF, app fonctionnelle
|
||||
```
|
||||
|
||||
**Test Case 4 : Mode Piéton Actif**
|
||||
```
|
||||
Given: Permission "Always" accordée
|
||||
When: App en arrière-plan, user marche près d'un POI
|
||||
Then:
|
||||
- Notification push reçue (200m du POI)
|
||||
- Tap notification → app ouvre contenu
|
||||
- Geofencing ne vide pas batterie (< 5%/h)
|
||||
```
|
||||
|
||||
**Test Case 5 : Refus Permission Always**
|
||||
```
|
||||
Given: User refuse permission "Always" dans dialog iOS
|
||||
When: Retour dans app
|
||||
Then:
|
||||
- Message "Mode piéton non disponible"
|
||||
- Bouton "Ouvrir réglages" disponible
|
||||
- Mode voiture toujours pleinement fonctionnel
|
||||
- Pas de popup récurrent de demande permission
|
||||
```
|
||||
|
||||
**Test Case 6 : Changement Permission dans Settings iOS**
|
||||
```
|
||||
Given: Permission "Always" active
|
||||
When: User change dans Settings iOS → "While Using"
|
||||
Then:
|
||||
- App détecte changement (AppLifecycleState)
|
||||
- Mode piéton désactivé automatiquement
|
||||
- Notification in-app : "Mode piéton désactivé (permission changée)"
|
||||
- Mode voiture reste fonctionnel
|
||||
```
|
||||
|
||||
### Scénarios de Test (External Beta 1)
|
||||
|
||||
**Durée** : 1 semaine
|
||||
|
||||
**Instructions aux testeurs** :
|
||||
|
||||
```
|
||||
Bienvenue sur la beta RoadWave !
|
||||
|
||||
Nous testons notre système de permissions géolocalisation.
|
||||
|
||||
Jour 1-2 : Installation & Mode Voiture
|
||||
1. Installez l'app depuis TestFlight
|
||||
2. Suivez l'onboarding (acceptez permission "When In Use")
|
||||
3. Utilisez l'app normalement en voiture pendant 2 jours
|
||||
4. Notez : bugs, crashs, notifications fonctionnent ?
|
||||
|
||||
Jour 3-5 : Mode Piéton (optionnel)
|
||||
5. Allez dans Settings > Notifications
|
||||
6. Activez "Audio-guides piéton"
|
||||
7. Lisez l'écran explicatif
|
||||
8. Acceptez OU refusez permission "Always" (votre choix !)
|
||||
9. Testez mode piéton en marchant en ville
|
||||
|
||||
Jour 6-7 : Feedback
|
||||
10. Répondez au questionnaire (lien ci-dessous)
|
||||
11. Signalez tout bug via TestFlight feedback
|
||||
|
||||
Questionnaire : [lien Google Forms]
|
||||
```
|
||||
|
||||
**Questionnaire Post-Test** (Google Forms) :
|
||||
|
||||
1. Avez-vous accepté permission "When In Use" au démarrage ? (Oui/Non)
|
||||
2. Pourquoi ? (Texte libre)
|
||||
3. Le texte de permission était-il clair ? (1-5)
|
||||
4. Avez-vous essayé d'activer le mode piéton ? (Oui/Non)
|
||||
5. Si oui, avez-vous accepté permission "Always" ? (Oui/Non/N'ai pas essayé)
|
||||
6. Pourquoi ? (Texte libre)
|
||||
7. L'écran explicatif avant permission "Always" était-il rassurant ? (1-5)
|
||||
8. Si vous avez refusé "Always", l'app reste-t-elle utilisable ? (Oui/Non/N/A)
|
||||
9. Bugs rencontrés ? (Texte libre)
|
||||
10. Suggestions d'amélioration textes permissions ? (Texte libre)
|
||||
|
||||
### Métriques Collectées (Firebase Analytics)
|
||||
|
||||
**Events trackés** :
|
||||
|
||||
```dart
|
||||
// Onboarding
|
||||
analytics.logEvent(
|
||||
name: 'permission_when_in_use_requested',
|
||||
);
|
||||
analytics.logEvent(
|
||||
name: 'permission_when_in_use_granted',
|
||||
parameters: {'granted': true},
|
||||
);
|
||||
|
||||
// Mode piéton
|
||||
analytics.logEvent(
|
||||
name: 'pedestrian_mode_toggle_attempted',
|
||||
);
|
||||
analytics.logEvent(
|
||||
name: 'permission_education_shown',
|
||||
);
|
||||
analytics.logEvent(
|
||||
name: 'permission_education_continued', // User tap "Continuer"
|
||||
);
|
||||
analytics.logEvent(
|
||||
name: 'permission_education_dismissed', // User tap "Non merci"
|
||||
);
|
||||
analytics.logEvent(
|
||||
name: 'permission_always_granted',
|
||||
parameters: {'granted': true},
|
||||
);
|
||||
|
||||
// Fallback
|
||||
analytics.logEvent(
|
||||
name: 'degraded_mode_activated',
|
||||
parameters: {'reason': 'permission_denied'},
|
||||
);
|
||||
```
|
||||
|
||||
**Dashboards Firebase** :
|
||||
|
||||
- Taux acceptation "When In Use" : `granted / requested`
|
||||
- Cible : >85%
|
||||
- Taux activation mode piéton : `toggle_attempted / total_users`
|
||||
- Cible : >30%
|
||||
- Taux acceptation "Always" : `always_granted / education_continued`
|
||||
- Cible : >40%
|
||||
- Taux abandon education : `education_dismissed / education_shown`
|
||||
- Cible : <60%
|
||||
|
||||
### Critères de Succès Beta 1
|
||||
|
||||
- [ ] Taux acceptation "When In Use" ≥ 80%
|
||||
- [ ] Taux acceptation "Always" ≥ 35%
|
||||
- [ ] 0 crash lié aux permissions
|
||||
- [ ] 0 feedback "app inutilisable sans Always"
|
||||
- [ ] Score satisfaction écran éducation ≥ 4/5
|
||||
- [ ] **Apple approuve External Beta** (critique !)
|
||||
|
||||
Si Apple **rejette** External Beta :
|
||||
1. Analyser raison rejet (email App Store Connect)
|
||||
2. Ajuster textes `Info.plist` si problème wording
|
||||
3. Ajuster flow si problème UX (ex: trop insistant)
|
||||
4. Re-soumettre sous 48h
|
||||
5. Itérer jusqu'à acceptation
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 : Beta Testing Android (Semaine 3-4)
|
||||
|
||||
### Configuration Play Console Internal Testing
|
||||
|
||||
**Testeurs** :
|
||||
- [ ] Ajouter emails testeurs (max 100 pour Internal Testing)
|
||||
- [ ] Créer "testers list" dans Play Console
|
||||
- [ ] Share lien installation (pas de review Google pour Internal)
|
||||
|
||||
**Devices** :
|
||||
- Google Pixel 5 (Android 12)
|
||||
- Samsung Galaxy S21 (Android 13)
|
||||
- OnePlus 9 (Android 14)
|
||||
- Google Pixel 8 (Android 15)
|
||||
|
||||
### Scénarios de Test (Focus Android)
|
||||
|
||||
**Test Case 1 : Foreground Service Notification (Android 12+)**
|
||||
```
|
||||
Given: Permission background accordée, mode piéton actif
|
||||
When: App en arrière-plan avec geofencing actif
|
||||
Then:
|
||||
- Notification foreground service visible dans panneau
|
||||
- Texte : "RoadWave détecte audio-guides à proximité"
|
||||
- Icône RoadWave visible
|
||||
- Tap notification → ouvre app
|
||||
- Notification ne peut pas être swipée (persistent)
|
||||
```
|
||||
|
||||
**Test Case 2 : Permission Background Android 10+**
|
||||
```
|
||||
Given: Android 10, 11, 12, 13, 14, ou 15
|
||||
When: Activation mode piéton
|
||||
Then:
|
||||
- Écran éducation s'affiche
|
||||
- Dialog Android demande "Toujours autoriser"
|
||||
- Options : "Toujours" / "Seulement pendant utilisation" / "Refuser"
|
||||
- Selection "Toujours" → mode piéton activé
|
||||
- Selection autre → mode piéton désactivé
|
||||
```
|
||||
|
||||
**Test Case 3 : Battery Drain**
|
||||
```
|
||||
Given: Mode piéton actif, app en arrière-plan
|
||||
When: 4 heures d'utilisation continue (marche en ville)
|
||||
Then:
|
||||
- Consommation batterie < 20% (< 5%/h)
|
||||
- Pas de "Battery draining" warning Android
|
||||
- Geofencing utilise location updates optimisés (pas de polling continu)
|
||||
```
|
||||
|
||||
**Test Case 4 : Permission Revocation**
|
||||
```
|
||||
Given: Permission background accordée
|
||||
When: User révoque dans Settings Android
|
||||
Then:
|
||||
- App détecte changement (broadcast receiver)
|
||||
- Mode piéton désactivé automatiquement
|
||||
- Foreground service arrêté
|
||||
- Notification in-app : "Mode piéton désactivé"
|
||||
```
|
||||
|
||||
### Vidéo Démo Play Store
|
||||
|
||||
**Enregistrement** :
|
||||
- [ ] Device : Pixel 8 (Android 15, UI stock)
|
||||
- [ ] Screen recorder : Android natif (Game Toolbar)
|
||||
- [ ] Durée : 25-30s
|
||||
- [ ] Orientation : Portrait
|
||||
- [ ] Résolution : 1080p
|
||||
|
||||
**Script** (voir [permissions-strategy.md](permissions-strategy.md#vidéo-démo-play-store-script)) :
|
||||
1. (0-5s) Settings > Notifications > scroll vers "Audio-guides piéton"
|
||||
2. (5-8s) Toggle OFF → ON
|
||||
3. (8-15s) Écran d'éducation affiché, scroll pour lire
|
||||
4. (15-18s) Tap "Continuer" → demande permission Android
|
||||
5. (18-22s) Tap "Toujours autoriser"
|
||||
6. (22-25s) Retour Settings, toggle ON confirmé
|
||||
|
||||
**Post-production** :
|
||||
- [ ] Ajouter text overlays : "Utilisateur active mode piéton", "Écran explicatif affiché", etc.
|
||||
- [ ] Exporter MP4 < 50MB
|
||||
- [ ] Upload Play Console > Permissions > Background Location > Video demo
|
||||
|
||||
### Critères de Succès Android
|
||||
|
||||
- [ ] Foreground service notification visible et claire
|
||||
- [ ] Consommation batterie acceptable (< 5%/h)
|
||||
- [ ] 0 crash sur Android 10-15
|
||||
- [ ] Vidéo démo uploadée et acceptée Play Console
|
||||
- [ ] Déclaration permission background validée
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 : Validation & Go/No-Go (Semaine 5-6)
|
||||
|
||||
### Analyse Résultats
|
||||
|
||||
**Tableau consolidé** :
|
||||
|
||||
| Métrique | iOS (Cible) | iOS (Réel) | Android (Cible) | Android (Réel) | Status |
|
||||
|----------|-------------|-----------|-----------------|----------------|--------|
|
||||
| Taux acceptation permission base | >85% | ? | >85% | ? | ? |
|
||||
| Taux activation mode piéton | >30% | ? | >30% | ? | ? |
|
||||
| Taux acceptation permission background | >40% | ? | >40% | ? | ? |
|
||||
| Crash rate permissions | 0% | ? | 0% | ? | ? |
|
||||
| Battery drain mode piéton | <5%/h | ? | <5%/h | ? | ? |
|
||||
| Feedback "app inutilisable" | 0 | ? | 0 | ? | ? |
|
||||
|
||||
### Décision Go/No-Go Production
|
||||
|
||||
**Critères GO** (tous doivent être ✅) :
|
||||
- [ ] Apple a approuvé External Beta TestFlight
|
||||
- [ ] Taux acceptation permission base iOS ≥ 80%
|
||||
- [ ] Taux acceptation permission base Android ≥ 80%
|
||||
- [ ] 0 crash critique lié aux permissions
|
||||
- [ ] 0 feedback utilisateur "app inutilisable sans background permission"
|
||||
- [ ] Vidéo démo Android uploadée et validée
|
||||
- [ ] Battery drain mode piéton < 5%/h (iOS & Android)
|
||||
|
||||
**Si NO-GO** :
|
||||
1. Identifier problème bloquant (cf métriques)
|
||||
2. Planifier corrections (ex: rewording textes, ajustement flow)
|
||||
3. Nouvelle itération beta (1-2 semaines)
|
||||
4. Re-validation
|
||||
|
||||
**Si GO** :
|
||||
1. Merge branche `release/testflight-permissions-v1` → `main`
|
||||
2. Tag version `v1.0.0`
|
||||
3. Préparer soumission production (Semaine 7)
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 : Soumission Production (Semaine 7+)
|
||||
|
||||
### iOS App Store
|
||||
|
||||
**Checklist soumission** :
|
||||
- [ ] Build production uploadé (même code que TestFlight validé)
|
||||
- [ ] Version : `1.0.0 (1)`
|
||||
- [ ] Screenshots stores (5 minimum, incluant permissions flow)
|
||||
- [ ] Description mentionnant "mode piéton optionnel"
|
||||
- [ ] Keywords : roadwave, audio, gps, tourisme, voyage
|
||||
- [ ] Pricing : Gratuit
|
||||
- [ ] App Privacy : Déclarer usage location (voir section)
|
||||
- [ ] Submit for Review
|
||||
|
||||
**App Privacy (obligatoire iOS 14+)** :
|
||||
|
||||
Location Data Collection :
|
||||
- [ ] "Precise Location" : Yes
|
||||
- [ ] Purpose : "App Functionality" + "Product Personalization"
|
||||
- [ ] Linked to user : Yes
|
||||
- [ ] Used for tracking : No
|
||||
- [ ] "Coarse Location" : No
|
||||
|
||||
**Timing** :
|
||||
- Review Apple : 24-48h (généralement)
|
||||
- Si rejet : corrections + re-soumission (24h)
|
||||
- **Total prévu** : 3-7 jours
|
||||
|
||||
### Android Play Store
|
||||
|
||||
**Checklist soumission** :
|
||||
- [ ] Build production (Release AAB)
|
||||
- [ ] Version : `1.0.0 (1)`
|
||||
- [ ] Screenshots (8 minimum)
|
||||
- [ ] Description courte (80 caractères)
|
||||
- [ ] Description longue (4000 caractères max)
|
||||
- [ ] Catégorie : Travel & Local
|
||||
- [ ] Pricing : Gratuit
|
||||
- [ ] Data Safety : Déclarer usage location
|
||||
- [ ] Submit for Review (Production track)
|
||||
|
||||
**Data Safety Form** :
|
||||
|
||||
Location Data :
|
||||
- [ ] "Approximate location" : No
|
||||
- [ ] "Precise location" : Yes
|
||||
- [ ] Purpose : "App functionality" + "Personalization"
|
||||
- [ ] Shared with third parties : No
|
||||
- [ ] Optional : Yes (mode dégradé disponible)
|
||||
- [ ] User can request deletion : Yes
|
||||
|
||||
**Timing** :
|
||||
- Review Google : 3-7 jours
|
||||
- Si rejet : corrections + re-soumission (1-2 jours)
|
||||
- **Total prévu** : 5-10 jours
|
||||
|
||||
---
|
||||
|
||||
## Contingences & Risques
|
||||
|
||||
### Risque 1 : Apple rejette permission "Always"
|
||||
|
||||
**Probabilité** : Moyenne (30%)
|
||||
**Impact** : Critique (bloque production)
|
||||
|
||||
**Mitigation** :
|
||||
1. Rewording `Info.plist` pour insister sur "optionnel"
|
||||
2. Ajouter screenshots montrant app sans permission "Always"
|
||||
3. Écrire email explicatif à App Review Team
|
||||
4. Si blocage persistant : envisager retrait mode piéton du MVP
|
||||
|
||||
### Risque 2 : Taux acceptation permission trop faible
|
||||
|
||||
**Probabilité** : Faible (20%)
|
||||
**Impact** : Modéré (feature peu utilisée)
|
||||
|
||||
**Mitigation** :
|
||||
1. A/B testing textes écran éducation
|
||||
2. Améliorer wording pour rassurer utilisateurs
|
||||
3. Ajouter testimonials/reviews dans écran éducation
|
||||
4. Retarder demande "Always" (demander après 1 semaine d'usage)
|
||||
|
||||
### Risque 3 : Battery drain trop élevé
|
||||
|
||||
**Probabilité** : Faible (15%)
|
||||
**Impact** : Critique (rejets stores + mauvaises reviews)
|
||||
|
||||
**Mitigation** :
|
||||
1. Optimiser geofencing radius (200m → 500m)
|
||||
2. Augmenter interval updates (30s → 60s)
|
||||
3. Utiliser "significant location changes" iOS au lieu de "continuous"
|
||||
4. Désactiver geofencing si batterie < 20%
|
||||
|
||||
### Risque 4 : Crash sur anciens OS
|
||||
|
||||
**Probabilité** : Faible (10%)
|
||||
**Impact** : Modéré (fragmentation utilisateurs)
|
||||
|
||||
**Mitigation** :
|
||||
1. Tester sur iOS 14, Android 10 (versions minimales)
|
||||
2. Fallback gracieux si API geofencing non disponible
|
||||
3. Considérer min SDK Android 11 (au lieu de 10) si trop de bugs
|
||||
|
||||
---
|
||||
|
||||
## Contacts & Ressources
|
||||
|
||||
### Équipe
|
||||
|
||||
- **Mobile Lead** : Responsable builds & soumissions stores
|
||||
- **QA Lead** : Coordination testeurs beta, analyse métriques
|
||||
- **Product Owner** : Décision go/no-go, priorisation corrections
|
||||
- **Legal/RGPD** : Validation textes permissions conformité
|
||||
|
||||
### Outils
|
||||
|
||||
- **TestFlight** : https://appstoreconnect.apple.com
|
||||
- **Play Console** : https://play.google.com/console
|
||||
- **Firebase Analytics** : https://console.firebase.google.com
|
||||
- **Questionnaire Beta** : Google Forms (lien à créer)
|
||||
- **Tracking Issues** : GitHub Issues avec label `[testflight]`
|
||||
|
||||
### Documentation
|
||||
|
||||
- [Stratégie Permissions](permissions-strategy.md)
|
||||
- [ADR-014 Frontend Mobile](../adr/014-frontend-mobile.md)
|
||||
- [Règle 05 Mode Piéton](../regles-metier/05-interactions-navigation.md)
|
||||
|
||||
---
|
||||
|
||||
**Plan approuvé par** : [Nom Product Owner]
|
||||
**Date d'approbation** : [Date]
|
||||
**Prochaine revue** : Fin Semaine 2 (après External Beta 1)
|
||||
@@ -4,16 +4,23 @@
|
||||
|
||||
**Décision** : Email/Password uniquement (pas d'OAuth tiers)
|
||||
|
||||
- ❌ Pas de Google, Apple, Facebook OAuth (dépendance services US/Chine)
|
||||
- ✅ Email + mot de passe
|
||||
- ❌ **Pas de Google, Apple, Facebook OAuth** (dépendance services US/Chine)
|
||||
- ✅ **Email + mot de passe** (formulaire natif Zitadel)
|
||||
- ✅ 2FA (Two-Factor Authentication) disponible
|
||||
- ✅ Option "Appareil de confiance" (skip 2FA pour 30 jours)
|
||||
|
||||
**Clarification technique** :
|
||||
- Zitadel utilise OAuth2/OIDC comme **protocole** (standard moderne pour mobile)
|
||||
- Mais l'authentification reste 100% **email/password natif**
|
||||
- **Aucun fournisseur externe** (Google, Apple, etc.) n'est intégré
|
||||
|
||||
**Justification** :
|
||||
- Souveraineté : pas de dépendance externe
|
||||
- RGPD : données 100% contrôlées
|
||||
- Coût : 0€ (Zitadel intégré)
|
||||
|
||||
> 📋 **Référence technique** : Voir [ADR-008 - OAuth2 vs Fournisseurs Tiers](../adr/008-authentification.md#oauth2-pkce--protocole-vs-fournisseurs-tiers) pour clarification protocole vs providers.
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Vérification email
|
||||
|
||||
@@ -135,7 +135,7 @@ export-roadwave-[user_id]-[date].zip
|
||||
| Niveau | Technologie | Contenus accessibles | Consentement |
|
||||
|--------|-------------|---------------------|--------------|
|
||||
| **Pays** | Aucune géoloc | Contenus nationaux uniquement | ❌ Non requis |
|
||||
| **Ville** | GeoIP (MaxMind) | Contenus régionaux/ville | ❌ Non requis |
|
||||
| **Ville** | GeoIP (IP2Location) | Contenus régionaux/ville | ❌ Non requis |
|
||||
| **Précis** | GPS | Tous contenus (hyperlocaux inclus) | ✅ Requis |
|
||||
|
||||
**Implémentation** :
|
||||
@@ -144,7 +144,7 @@ export-roadwave-[user_id]-[date].zip
|
||||
- Upgrade volontaire vers GPS
|
||||
|
||||
**API GeoIP** :
|
||||
- MaxMind GeoLite2 (gratuit, self-hosted)
|
||||
- IP2Location Lite (gratuit, self-hosted, voir [ADR-021](../adr/021-geolocalisation-ip.md))
|
||||
- Update DB mensuelle automatique
|
||||
- Précision ~80% au niveau ville
|
||||
|
||||
@@ -314,7 +314,7 @@ export-roadwave-[user_id]-[date].zip
|
||||
| **Anonymisation GPS** | Geohash PostGIS (24h) | 0€ |
|
||||
| **Export données** | JSON+HTML+ZIP asynchrone | 0€ |
|
||||
| **Suppression compte** | Grace period 30j + anonymisation | 0€ |
|
||||
| **Mode dégradé** | GeoIP MaxMind + GPS optionnel | 0€ |
|
||||
| **Mode dégradé** | GeoIP IP2Location + GPS optionnel | 0€ |
|
||||
| **Conservation** | Purge auto 5 ans inactivité | 0€ |
|
||||
| **Analytics** | Matomo self-hosted | ~5€/mois |
|
||||
| **Registre traitements** | Markdown Git | 0€ |
|
||||
|
||||
@@ -2,16 +2,25 @@
|
||||
|
||||
### 3.1 Évolution des jauges
|
||||
|
||||
**Décision** : Système simple avec valeurs fixes
|
||||
**Décision** : Système simple avec valeurs fixes (points de pourcentage absolus)
|
||||
|
||||
| Action | Impact jauge | Justification |
|
||||
|--------|--------------|---------------|
|
||||
| **Like automatique renforcé (≥80% écoute)** | +2% | Signal fort d'intérêt (écoute quasi-complète) |
|
||||
| **Like automatique standard (30-79% écoute)** | +1% | Signal modéré d'intérêt |
|
||||
| **Like explicite (manuel)** | +2% | Signal fort, cumulable avec auto |
|
||||
| **Abonnement créateur** | +5% sur tous ses tags | Signal très fort d'affinité |
|
||||
| **Skip rapide (<10s)** | -0.5% | Désintérêt marqué |
|
||||
| **Skip tardif (≥30%)** | 0% | Neutre (contenu essayé suffisamment) |
|
||||
| **Like automatique renforcé (≥80% écoute)** | **+2%** | Signal fort d'intérêt (écoute quasi-complète) |
|
||||
| **Like automatique standard (30-79% écoute)** | **+1%** | Signal modéré d'intérêt |
|
||||
| **Like explicite (manuel)** | **+2%** | Signal fort, cumulable avec auto |
|
||||
| **Abonnement créateur** | **+5%** sur tous ses tags | Signal très fort d'affinité |
|
||||
| **Skip rapide (<10s)** | **-0.5%** | Désintérêt marqué |
|
||||
| **Skip tardif (≥30%)** | **0%** | Neutre (contenu essayé suffisamment) |
|
||||
|
||||
**Note importante** : Les pourcentages indiqués sont des **points de pourcentage absolus**, **PAS des pourcentages relatifs**.
|
||||
|
||||
**Calcul** :
|
||||
- Si jauge "Automobile" = 45%
|
||||
- Like renforcé (+2%) → 45 + 2 = **47%** ✅
|
||||
- **NOT** 45 × 1.02 = 45.9% ❌
|
||||
|
||||
Cette approche garantit une **progression linéaire** et **équitable** pour tous les utilisateurs, indépendamment de leur niveau actuel dans une jauge.
|
||||
|
||||
**Paramètres techniques** :
|
||||
- Les jauges sont bornées strictement entre **0% et 100%**
|
||||
@@ -48,10 +57,13 @@ Scénario 4 : Skip après 5s
|
||||
- **Like automatique** : Reflète l'engagement réel (voir [ADR-010](../adr/010-commandes-volant.md))
|
||||
- **Sécurité routière** : Pas d'action complexe en conduite
|
||||
- **Prévisibilité** : Règles claires et déterministes
|
||||
- **Coût minimal** : Calculs simples en backend
|
||||
- **Fiabilité** : Pas d'edge cases complexes
|
||||
- **Progression linéaire** : Évite l'effet "rich get richer" (progression équitable)
|
||||
- **Coût minimal** : Calculs simples en backend (addition/soustraction uniquement)
|
||||
- **Fiabilité** : Pas d'edge cases complexes (pas de risque d'overflow avec multiplication)
|
||||
- **Ajustable** : Valeurs modifiables via dashboard admin si besoin
|
||||
|
||||
> 📋 **Référence technique** : Voir [ADR-010 - Formule de Calcul](../adr/010-commandes-volant.md#implémentation-technique) pour l'implémentation backend détaillée.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Jauge initiale
|
||||
|
||||
@@ -116,7 +116,26 @@ Musée du Louvre : La Joconde - @paris_museum
|
||||
|
||||
**Permissions requises** :
|
||||
|
||||
⚠️ **Important** : Permission "Always Location" est **optionnelle** et demandée uniquement si user active le mode piéton dans settings.
|
||||
⚠️ **Important** : RoadWave utilise une **stratégie de permissions progressive** pour maximiser l'acceptation utilisateur et la validation stores.
|
||||
|
||||
**Étape 1 - Permission de base (tous utilisateurs)** :
|
||||
- iOS : "Allow While Using App" (`locationWhenInUse`)
|
||||
- Android : `ACCESS_FINE_LOCATION`
|
||||
- **Demandée** : Au premier lancement (onboarding)
|
||||
- **Permet** : Mode voiture complet ✅
|
||||
|
||||
**Étape 2 - Permission arrière-plan (optionnelle, mode piéton uniquement)** :
|
||||
- iOS : "Allow Always" (`locationAlways`)
|
||||
- Android : `ACCESS_BACKGROUND_LOCATION`
|
||||
- **Demandée** : Uniquement si user active "Notifications audio-guides piéton" dans settings
|
||||
- **Précédée** : Écran d'éducation expliquant l'usage (requis stores)
|
||||
- **Permet** : Mode piéton avec notifications push en arrière-plan ✅
|
||||
|
||||
**Si permission arrière-plan refusée** :
|
||||
- Mode piéton **désactivé** (toggle grisé dans settings)
|
||||
- Mode voiture reste **pleinement fonctionnel**
|
||||
- Audio-guides accessibles en mode **manuel** (user ouvre app, lance contenu)
|
||||
- **Garantie RGPD** : App utilisable sans permission arrière-plan ✅
|
||||
|
||||
iOS (`Info.plist`) :
|
||||
```xml
|
||||
@@ -131,8 +150,12 @@ Android (`AndroidManifest.xml`) :
|
||||
```xml
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
```
|
||||
|
||||
> 📋 **Référence technique** : Voir [ADR-014 - Stratégie de Permissions](../adr/014-frontend-mobile.md#stratégie-de-permissions-iosandroid) pour détails d'implémentation.
|
||||
|
||||
**Disclosure avant demande permission** (Android requis, iOS recommandé) :
|
||||
|
||||
Écran affiché avant demande permission "Always Location" :
|
||||
|
||||
@@ -246,7 +246,7 @@ Fonctionnalité: Conformité administrative RGPD (Registre, Breach, DPO)
|
||||
| Anonymisation GPS | Geohash PostGIS (24h) | 0€ |
|
||||
| Export données | JSON+HTML+ZIP asynchrone | 0€ |
|
||||
| Suppression compte | Grace period 30j + anonymisation | 0€ |
|
||||
| Mode dégradé | GeoIP MaxMind + GPS optionnel | 0€ |
|
||||
| Mode dégradé | GeoIP IP2Location + GPS optionnel | 0€ |
|
||||
| Conservation | Purge auto 5 ans inactivité | 0€ |
|
||||
| Analytics | Matomo self-hosted | ~5€/mois |
|
||||
| Registre traitements | Markdown Git | 0€ |
|
||||
|
||||
@@ -21,7 +21,7 @@ Fonctionnalité: Mode dégradé avec GeoIP (sans GPS précis)
|
||||
Exemples:
|
||||
| niveau | technologie | contenus | consentement |
|
||||
| Pays | Aucune géoloc | Contenus nationaux uniquement | Non requis |
|
||||
| Ville | GeoIP (MaxMind) | Contenus régionaux/ville | Non requis |
|
||||
| Ville | GeoIP (IP2Location) | Contenus régionaux/ville | Non requis |
|
||||
| Précis | GPS | Tous contenus (hyperlocaux inclus) | Requis |
|
||||
|
||||
# Démarrage avec GeoIP automatique
|
||||
@@ -35,9 +35,9 @@ Fonctionnalité: Mode dégradé avec GeoIP (sans GPS précis)
|
||||
Et aucun consentement n'est requis (GeoIP ne collecte pas de données personnelles)
|
||||
Et je peux accéder aux contenus régionaux et de ville
|
||||
|
||||
Scénario: Détection de ville avec MaxMind GeoLite2
|
||||
Scénario: Détection de ville avec IP2Location Lite
|
||||
Étant donné que mon adresse IP est 93.184.216.34
|
||||
Quand le système utilise GeoIP MaxMind GeoLite2
|
||||
Quand le système utilise GeoIP IP2Location Lite
|
||||
Alors ma ville est détectée: "Paris"
|
||||
Et ma région est détectée: "Île-de-France"
|
||||
Et mon pays est détecté: "France"
|
||||
@@ -115,20 +115,20 @@ Fonctionnalité: Mode dégradé avec GeoIP (sans GPS précis)
|
||||
Et seule la ville est stockée (non identifiant)
|
||||
Et aucun consentement n'est requis conformément au RGPD
|
||||
|
||||
# Implémentation MaxMind GeoLite2
|
||||
# Implémentation IP2Location Lite
|
||||
|
||||
Scénario: Base de données MaxMind self-hosted
|
||||
Étant donné que RoadWave utilise MaxMind GeoLite2
|
||||
Scénario: Base de données IP2Location self-hosted
|
||||
Étant donné que RoadWave utilise IP2Location Lite
|
||||
Quand on analyse l'infrastructure
|
||||
Alors la base de données GeoLite2 est hébergée sur les serveurs RoadWave
|
||||
Alors la base de données IP2Location est hébergée sur les serveurs RoadWave
|
||||
Et aucune requête n'est envoyée à un service tiers
|
||||
Et la base de données est mise à jour automatiquement chaque mois
|
||||
Et le coût est de 0€ (GeoLite2 est gratuit)
|
||||
Et le coût est de 0€ (IP2Location Lite est gratuit)
|
||||
|
||||
Scénario: Mise à jour mensuelle de la base GeoIP
|
||||
Étant donné que MaxMind publie des mises à jour mensuelles
|
||||
Étant donné que IP2Location publie des mises à jour mensuelles
|
||||
Quand le 1er du mois arrive
|
||||
Alors un job automatique télécharge la nouvelle base GeoLite2
|
||||
Alors un job automatique télécharge la nouvelle base IP2Location Lite
|
||||
Et la base est mise à jour sans interruption de service
|
||||
Et un log est créé pour traçabilité
|
||||
Et si la mise à jour échoue, une alerte est envoyée
|
||||
@@ -216,7 +216,7 @@ Fonctionnalité: Mode dégradé avec GeoIP (sans GPS précis)
|
||||
# Coût de la solution: 0€
|
||||
|
||||
Scénario: Solution GeoIP gratuite et self-hosted
|
||||
Étant donné que RoadWave utilise MaxMind GeoLite2
|
||||
Étant donné que RoadWave utilise IP2Location Lite
|
||||
Quand on calcule le coût de la solution
|
||||
Alors le coût est de 0€
|
||||
Et la solution est opensource
|
||||
|
||||
Reference in New Issue
Block a user