first commit

This commit is contained in:
DahoudG
2025-08-20 21:00:35 +00:00
commit b2a23bdf89
583 changed files with 243074 additions and 0 deletions

43
unionflow-mobile-apps/.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "2663184aa79047d0a33a14a3b607954f8fdd8730"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: android
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: ios
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@@ -0,0 +1,52 @@
# 📱 UnionFlow Mobile Apps
> Application mobile moderne pour la gestion d'associations en Côte d'Ivoire avec intégration Wave Money
[![React Native](https://img.shields.io/badge/React%20Native-0.73.2-blue.svg)](https://reactnative.dev/)
[![TypeScript](https://img.shields.io/badge/TypeScript-4.8.4-blue.svg)](https://www.typescriptlang.org/)
[![Wave Money](https://img.shields.io/badge/Wave%20Money-Intégré-orange.svg)](https://wave.com/)
[![Côte d'Ivoire](https://img.shields.io/badge/Côte%20d'Ivoire-🇨🇮-green.svg)](https://www.gouv.ci/)
## 🌟 Fonctionnalités
### 💳 **Paiements Wave Money**
- **Cotisations** : Paiement des cotisations mensuelles/annuelles
- **Adhésions** : Frais d'inscription nouveaux membres
- **Aide mutuelle** : Versements d'aide entre membres
- **Événements** : Paiement de participation aux événements
- **Calcul automatique des frais** selon le barème Wave CI
- **Mode hors ligne** avec synchronisation automatique
### 🔐 **Sécurité Avancée**
- **Authentification biométrique** (empreinte, Face ID)
- **Chiffrement des données** sensibles
- **Validation OWASP** des entrées utilisateur
- **Sessions sécurisées** avec timeout automatique
- **Audit complet** des transactions
### 🎨 **Interface Ultra Moderne**
- **Design System** cohérent inspiré des couleurs ivoiriennes
- **Animations fluides** avec Reanimated 3
- **Mode sombre** automatique
- **Responsive design** pour tous les écrans
- **Accessibilité** complète (WCAG 2.1)
### 🌐 **Fonctionnalités Avancées**
- **Workflows configurables** pour les processus métier
- **Notifications push** intelligentes
- **Support multilingue** (Français, Baoulé, Dioula)
- **Synchronisation temps réel** avec le backend
- **Cache intelligent** pour performance optimale
## 🚀 Installation
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -0,0 +1,182 @@
# 🚀 UnionFlow Mobile - Guide de Démarrage Rapide
## ✨ Système d'authentification sophistiqué prêt à tester !
### 🎯 Démarrage Express (2 minutes)
#### **Windows PowerShell :**
```powershell
.\quick_start.ps1
flutter run
```
#### **Linux/macOS :**
```bash
flutter pub get
cp lib/main_temp.dart lib/main.dart
flutter run
```
---
## 🔑 Identifiants de Test
| Champ | Valeur |
|-------|--------|
| **📧 Email** | `admin@unionflow.dev` |
| **🔑 Mot de passe** | `admin123` |
---
## ✨ Fonctionnalités Implémentées
### 🎨 **Interface Utilisateur Premium**
-**Splash screen animé** avec progression fluide
-**Écran de connexion sophistiqué** avec animations Material Design 3
-**Validation en temps réel** des formulaires
-**Feedback haptique** sur chaque interaction
-**Transitions animées** entre écrans
-**Design responsive** adaptatif
### 🔐 **Système d'Authentification Avancé**
-**Architecture Clean** avec BLoC pattern
-**Gestion d'état robuste** avec flutter_bloc
-**Stockage sécurisé** (simulation enterprise)
-**Auto-refresh des tokens** (préparé)
-**Gestion d'erreurs intelligente**
-**Session persistante**
### 🏗️ **Architecture Enterprise**
-**Clean Architecture** respectée
-**Injection de dépendances** configurée
-**Modularité** par features
-**Testabilité** intégrée
-**Scalabilité** pour production
---
## 🎪 Parcours Utilisateur
### 1. **Écran de Démarrage**
- Logo animé avec effet de scale élastique
- Barre de progression fluide
- Transition vers l'authentification
### 2. **Interface de Connexion**
- Animation d'entrée sophistiquée avec fade + slide
- Champs de saisie avec validation temps réel
- Checkbox "Se souvenir de moi" interactif
- Bouton de connexion avec états de chargement
- Gestion d'erreurs avec shake animation
### 3. **Navigation Principale**
- Dashboard avec widgets sophistiqués
- Module Membres fonctionnel
- Navigation bottom avec animations
- FAB contextuel par section
---
## 🛠️ Architecture Technique
```
lib/
├── core/ # Logique métier centrale
│ ├── auth/ # Système d'authentification
│ │ ├── bloc/ # Gestion d'état BLoC
│ │ ├── models/ # Modèles de données
│ │ ├── services/ # Services d'auth
│ │ └── storage/ # Stockage sécurisé
│ ├── network/ # Configuration HTTP
│ └── di/ # Injection de dépendances
├── features/ # Modules par fonctionnalité
│ ├── auth/ # UI d'authentification
│ ├── dashboard/ # Tableau de bord
│ ├── members/ # Gestion des membres
│ └── navigation/ # Navigation principale
└── shared/ # Composants partagés
├── theme/ # Thème et couleurs
└── widgets/ # Widgets réutilisables
```
---
## 🎨 Widgets Sophistiqués Disponibles
### **Badges Avancés**
- `StatusBadge` - 7 types, 4 tailles, 4 variants
- `CountBadge` - Compteurs animés avec effets
### **Cartes Premium**
- `SophisticatedCard` - 5 variants (elevated, outlined, filled, glass, gradient)
- `SophisticatedMemberCard` - Cartes membres expandables
### **Avatars Professionnels**
- `SophisticatedAvatar` - Status en ligne, badges, formes multiples
### **Boutons Enterprise**
- `SophisticatedButton` - 8 variants, 4 tailles, 3 formes
- `SophisticatedFAB` - FAB avec morphing, pulse, gradient
- `ButtonGroup` - Contrôles segmentés, toggles, tabs
---
## 🚀 Étapes Suivantes
### **Phase 1 - Test Actuel**
- [x] Authentification fonctionnelle
- [x] Interface premium
- [x] Navigation sophistiquée
### **Phase 2 - API Complète** (Prochaine)
- [ ] Connexion API JWT réelle
- [ ] Stockage sécurisé complet
- [ ] Auto-refresh des tokens
### **Phase 3 - Modules Avancés**
- [ ] CRUD Membres complet
- [ ] Module Cotisations
- [ ] Module Événements
- [ ] Dashboard financier
---
## 📱 Compatibilité
- **Flutter** 3.5.3+
- **Android** 5.0+ (API 21+)
- **iOS** 12.0+
- **Web** Navigateurs modernes
---
## 🆘 Résolution de Problèmes
### **Erreur de dépendances**
```bash
flutter clean
flutter pub get
```
### **Problème de build**
```bash
flutter pub deps
flutter doctor
```
### **Revenir à la version complète**
```bash
cp lib/main_original_backup.dart lib/main.dart
```
---
## 🎉 Prêt à Épater !
Votre système d'authentification est maintenant **prêt à impressionner** avec :
- Interface de niveau **production**
- Animations **fluides et naturelles**
- Architecture **scalable et maintenable**
- Code **propre et documenté**
**Lancez l'app et découvrez la magie ! ✨**

View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,44 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
android {
namespace = "dev.lions.unionflow_mobile_apps"
compileSdk = 34
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "dev.lions.unionflow_mobile_apps"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="unionflow_mobile_apps"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package dev.lions.unionflow_mobile_apps
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,39 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
// Configuration globale Java 17 pour tous les sous-projets (compatible avec Gradle 8.7)
subprojects {
afterEvaluate { project ->
if (project.hasProperty('android')) {
project.android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
}
// Configuration Kotlin pour tous les projets
project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = '17'
}
}
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,15 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError -Djava.net.preferIPv4Stack=true
android.useAndroidX=true
android.enableJetifier=true
# Configuration réseau - Décommentez et adaptez si nécessaire
# systemProp.http.proxyHost=your.proxy.host
# systemProp.http.proxyPort=8080
# systemProp.https.proxyHost=your.proxy.host
# systemProp.https.proxyPort=8080
# systemProp.http.nonProxyHosts=localhost|127.0.0.1
# systemProp.https.nonProxyHosts=localhost|127.0.0.1
# Timeout augmenté pour connexions lentes
systemProp.org.gradle.internal.http.connectionTimeout=120000
systemProp.org.gradle.internal.http.socketTimeout=120000

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip

View File

@@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
include ":app"

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env pwsh
# Script pour résoudre les problèmes de build Gradle Flutter
Write-Host "🔧 Résolution des problèmes de build Flutter/Gradle..." -ForegroundColor Cyan
# 1. Nettoyer le cache Flutter
Write-Host "`n📦 Nettoyage du cache Flutter..." -ForegroundColor Yellow
flutter clean
if ($LASTEXITCODE -ne 0) {
Write-Host "❌ Erreur lors du nettoyage Flutter" -ForegroundColor Red
}
# 2. Nettoyer le cache Gradle
Write-Host "`n📦 Nettoyage du cache Gradle..." -ForegroundColor Yellow
Set-Location android
if (Test-Path "gradlew.bat") {
./gradlew.bat clean
} else {
./gradlew clean
}
Set-Location ..
# 3. Supprimer les dossiers de build
Write-Host "`n🗑️ Suppression des dossiers de build..." -ForegroundColor Yellow
$foldersToDelete = @(
"build",
"android/build",
"android/app/build",
"android/.gradle",
".dart_tool"
)
foreach ($folder in $foldersToDelete) {
if (Test-Path $folder) {
Write-Host " Suppression de $folder"
Remove-Item -Path $folder -Recurse -Force -ErrorAction SilentlyContinue
}
}
# 4. Récupérer les packages Flutter
Write-Host "`n📥 Récupération des packages Flutter..." -ForegroundColor Yellow
flutter pub get
# 5. Vérifier la connexion réseau
Write-Host "`n🌐 Test de connexion réseau..." -ForegroundColor Yellow
$testConnection = Test-NetConnection -ComputerName "services.gradle.org" -Port 443 -InformationLevel Quiet
if ($testConnection) {
Write-Host "✅ Connexion à services.gradle.org OK" -ForegroundColor Green
} else {
Write-Host "❌ Impossible de se connecter à services.gradle.org" -ForegroundColor Red
Write-Host " Vérifiez votre connexion internet ou les paramètres proxy" -ForegroundColor Yellow
# Afficher les variables d'environnement proxy si elles existent
if ($env:HTTP_PROXY -or $env:HTTPS_PROXY) {
Write-Host "`n📋 Variables proxy détectées:" -ForegroundColor Yellow
if ($env:HTTP_PROXY) { Write-Host " HTTP_PROXY: $env:HTTP_PROXY" }
if ($env:HTTPS_PROXY) { Write-Host " HTTPS_PROXY: $env:HTTPS_PROXY" }
}
}
# 6. Télécharger Gradle manuellement si nécessaire
Write-Host "`n📥 Vérification de Gradle..." -ForegroundColor Yellow
$gradleVersion = "8.3"
$gradleHome = "$env:USERPROFILE\.gradle\wrapper\dists\gradle-$gradleVersion-all"
if (-not (Test-Path $gradleHome)) {
Write-Host " Gradle $gradleVersion n'est pas installé localement" -ForegroundColor Yellow
Write-Host " Tentative de téléchargement manuel..." -ForegroundColor Cyan
# Créer le dossier si nécessaire
$wrapperDir = "$env:USERPROFILE\.gradle\wrapper\dists"
if (-not (Test-Path $wrapperDir)) {
New-Item -Path $wrapperDir -ItemType Directory -Force | Out-Null
}
# URL de téléchargement
$gradleUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip"
$gradleZip = "$env:TEMP\gradle-$gradleVersion-all.zip"
try {
Write-Host " Téléchargement depuis $gradleUrl..."
Invoke-WebRequest -Uri $gradleUrl -OutFile $gradleZip -UseBasicParsing
Write-Host "✅ Téléchargement réussi" -ForegroundColor Green
} catch {
Write-Host "❌ Échec du téléchargement: $_" -ForegroundColor Red
Write-Host "`n💡 Solutions alternatives:" -ForegroundColor Yellow
Write-Host " 1. Téléchargez manuellement: $gradleUrl"
Write-Host " 2. Placez le fichier dans: $env:TEMP"
Write-Host " 3. Relancez ce script"
}
} else {
Write-Host "✅ Gradle $gradleVersion trouvé localement" -ForegroundColor Green
}
# 7. Tester le build
Write-Host "`n🚀 Test du build Android..." -ForegroundColor Cyan
$response = Read-Host "Voulez-vous lancer le build maintenant? (O/N)"
if ($response -eq 'O' -or $response -eq 'o') {
flutter build apk --debug
if ($LASTEXITCODE -eq 0) {
Write-Host "`n✅ Build réussi!" -ForegroundColor Green
} else {
Write-Host "`n❌ Le build a échoué" -ForegroundColor Red
Write-Host "`n💡 Essayez ces commandes manuellement:" -ForegroundColor Yellow
Write-Host " 1. cd android"
Write-Host " 2. ./gradlew.bat assembleDebug --offline"
Write-Host " (mode offline si problème réseau)"
}
}
Write-Host "`n📝 Rapport final:" -ForegroundColor Cyan
Write-Host " - Cache Flutter nettoyé"
Write-Host " - Cache Gradle nettoyé"
Write-Host " - Packages récupérés"
Write-Host " - Timeout réseau augmenté dans gradle.properties"
Write-Host "`n✨ Script terminé!" -ForegroundColor Green

View File

@@ -0,0 +1,62 @@
#!/bin/bash
# Script pour résoudre les problèmes de build Gradle Flutter
echo -e "\033[36m🔧 Résolution des problèmes de build Flutter/Gradle...\033[0m"
# 1. Nettoyer le cache Flutter
echo -e "\n\033[33m📦 Nettoyage du cache Flutter...\033[0m"
flutter clean
# 2. Nettoyer le cache Gradle
echo -e "\n\033[33m📦 Nettoyage du cache Gradle...\033[0m"
cd android
if [ -f "gradlew" ]; then
./gradlew clean
fi
cd ..
# 3. Supprimer les dossiers de build
echo -e "\n\033[33m🗑 Suppression des dossiers de build...\033[0m"
rm -rf build
rm -rf android/build
rm -rf android/app/build
rm -rf android/.gradle
rm -rf .dart_tool
# 4. Récupérer les packages Flutter
echo -e "\n\033[33m📥 Récupération des packages Flutter...\033[0m"
flutter pub get
# 5. Vérifier la connexion réseau
echo -e "\n\033[33m🌐 Test de connexion réseau...\033[0m"
if ping -c 1 services.gradle.org &> /dev/null; then
echo -e "\033[32m✅ Connexion à services.gradle.org OK\033[0m"
else
echo -e "\033[31m❌ Impossible de se connecter à services.gradle.org\033[0m"
echo -e "\033[33m Vérifiez votre connexion internet ou les paramètres proxy\033[0m"
# Afficher les variables proxy si elles existent
if [ ! -z "$HTTP_PROXY" ] || [ ! -z "$HTTPS_PROXY" ]; then
echo -e "\n\033[33m📋 Variables proxy détectées:\033[0m"
[ ! -z "$HTTP_PROXY" ] && echo " HTTP_PROXY: $HTTP_PROXY"
[ ! -z "$HTTPS_PROXY" ] && echo " HTTPS_PROXY: $HTTPS_PROXY"
fi
fi
# 6. Test du build
echo -e "\n\033[36m🚀 Test du build Android...\033[0m"
read -p "Voulez-vous lancer le build maintenant? (o/n): " response
if [ "$response" = "o" ] || [ "$response" = "O" ]; then
flutter build apk --debug
if [ $? -eq 0 ]; then
echo -e "\n\033[32m✅ Build réussi!\033[0m"
else
echo -e "\n\033[31m❌ Le build a échoué\033[0m"
echo -e "\n\033[33m💡 Essayez ces commandes manuellement:\033[0m"
echo " 1. cd android"
echo " 2. ./gradlew assembleDebug --offline"
echo " (mode offline si problème réseau)"
fi
fi
echo -e "\n\033[36m✨ Script terminé!\033[0m"

View File

@@ -0,0 +1,41 @@
# Script de réparation et test
Write-Host "🔧 Réparation des problèmes Android/Gradle..." -ForegroundColor Cyan
Write-Host "1. Nettoyage Flutter..." -ForegroundColor Yellow
flutter clean
Write-Host "2. Suppression du cache Gradle..." -ForegroundColor Yellow
if (Test-Path "$env:USERPROFILE\.gradle\caches") {
Remove-Item "$env:USERPROFILE\.gradle\caches" -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Host "3. Suppression des fichiers de build Android..." -ForegroundColor Yellow
if (Test-Path "android\.gradle") {
Remove-Item "android\.gradle" -Recurse -Force -ErrorAction SilentlyContinue
}
if (Test-Path "android\app\build") {
Remove-Item "android\app\build" -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Host "4. Récupération des dépendances..." -ForegroundColor Yellow
flutter pub get
Write-Host "5. S'assurer qu'on utilise la version temporaire..." -ForegroundColor Yellow
Copy-Item "lib\main_temp.dart" "lib\main.dart" -Force
Write-Host "✅ Réparation terminée!" -ForegroundColor Green
Write-Host ""
Write-Host "🚀 Essayez maintenant:" -ForegroundColor Cyan
Write-Host " flutter run --verbose" -ForegroundColor White
Write-Host ""
Write-Host "💡 Si ça ne marche toujours pas, essayez:" -ForegroundColor Blue
Write-Host " flutter run -d chrome (pour tester sur web)" -ForegroundColor Gray
Write-Host " flutter doctor (pour diagnostiquer)" -ForegroundColor Gray
Write-Host ""
Write-Host "🔑 Identifiants de test:" -ForegroundColor Magenta
Write-Host " 📧 Email: admin@unionflow.dev" -ForegroundColor White
Write-Host " 🔑 Mot de passe: admin123" -ForegroundColor White

View File

@@ -0,0 +1,34 @@
# Script PowerShell pour installer les dépendances et tester l'app
Write-Host "🚀 Installation des dépendances Flutter..." -ForegroundColor Cyan
# Installer les dépendances
flutter pub get
Write-Host "✅ Dépendances installées!" -ForegroundColor Green
Write-Host "🔧 Génération du code d'injection de dépendances..." -ForegroundColor Cyan
# Générer le code pour l'injection de dépendances
flutter packages pub run build_runner build --delete-conflicting-outputs
Write-Host "✅ Code généré!" -ForegroundColor Green
Write-Host "🧪 Test avec la version temporaire..." -ForegroundColor Cyan
# Copier le main temporaire pour tester
Copy-Item "lib\main_temp.dart" "lib\main_backup.dart" -Force
Copy-Item "lib\main.dart" "lib\main_original.dart" -Force
Copy-Item "lib\main_temp.dart" "lib\main.dart" -Force
Write-Host "✅ Version temporaire activée!" -ForegroundColor Green
Write-Host "📱 Vous pouvez maintenant lancer 'flutter run' pour tester l'app" -ForegroundColor Yellow
Write-Host ""
Write-Host "🔑 Identifiants de test:" -ForegroundColor Magenta
Write-Host " Email: admin@unionflow.dev" -ForegroundColor White
Write-Host " Mot de passe: admin123" -ForegroundColor White
Write-Host ""
Write-Host "📝 Pour revenir à la version complète:" -ForegroundColor Blue
Write-Host " Copy-Item lib\main_original.dart lib\main.dart -Force" -ForegroundColor Gray

View File

@@ -0,0 +1,36 @@
#!/bin/bash
# Script pour installer les dépendances et tester l'app
echo "🚀 Installation des dépendances Flutter..."
# Installer les dépendances
flutter pub get
echo "✅ Dépendances installées!"
echo "🔧 Génération du code d'injection de dépendances..."
# Générer le code pour l'injection de dépendances
flutter packages pub run build_runner build --delete-conflicting-outputs
echo "✅ Code généré!"
echo "🧪 Test avec la version temporaire..."
# Copier le main temporaire pour tester
cp lib/main_temp.dart lib/main_backup.dart
cp lib/main.dart lib/main_original.dart
cp lib/main_temp.dart lib/main.dart
echo "✅ Version temporaire activée!"
echo "📱 Vous pouvez maintenant lancer 'flutter run' pour tester l'app"
echo ""
echo "🔑 Identifiants de test:"
echo " Email: admin@unionflow.dev"
echo " Mot de passe: admin123"
echo ""
echo "📝 Pour revenir à la version complète:"
echo " cp lib/main_original.dart lib/main.dart"

34
unionflow-mobile-apps/ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?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>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = dev.lions.unionflowMobileApps;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.lions.unionflowMobileApps.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.lions.unionflowMobileApps.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.lions.unionflowMobileApps.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = dev.lions.unionflowMobileApps;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = dev.lions.unionflowMobileApps;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?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>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?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>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?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>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?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>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,49 @@
<?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>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Unionflow Mobile Apps</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>unionflow_mobile_apps</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'core/auth/bloc/auth_bloc.dart';
import 'core/auth/models/auth_state.dart';
import 'features/splash/presentation/pages/splash_screen.dart';
import 'features/auth/presentation/pages/login_page.dart';
import 'features/navigation/presentation/pages/main_navigation.dart';
/// Wrapper principal de l'application qui gère la navigation basée sur l'état d'authentification
class AppWrapper extends StatelessWidget {
const AppWrapper({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
switch (state.status) {
case AuthStatus.unknown:
case AuthStatus.checking:
// Afficher l'écran de chargement pendant l'initialisation
return const SplashScreen();
case AuthStatus.authenticated:
// Utilisateur connecté -> Navigation principale
return const MainNavigation();
case AuthStatus.unauthenticated:
case AuthStatus.error:
case AuthStatus.expired:
// Utilisateur non connecté -> Écran de connexion
return const LoginPage();
}
},
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'core/auth/bloc/temp_auth_bloc.dart';
import 'core/auth/models/auth_state.dart';
import 'features/splash/presentation/pages/splash_screen.dart';
import 'features/auth/presentation/pages/login_page_temp.dart';
import 'features/navigation/presentation/pages/main_navigation.dart';
/// Wrapper temporaire de l'application
class AppTempWrapper extends StatelessWidget {
const AppTempWrapper({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<TempAuthBloc, AuthState>(
builder: (context, state) {
switch (state.status) {
case AuthStatus.unknown:
case AuthStatus.checking:
return const SplashScreen();
case AuthStatus.authenticated:
return const MainNavigation();
case AuthStatus.unauthenticated:
case AuthStatus.error:
case AuthStatus.expired:
return const TempLoginPage();
}
},
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'main_ultra_simple.dart';
import 'core/auth/models/auth_state.dart';
import 'features/splash/presentation/pages/splash_screen.dart';
import 'features/auth/presentation/pages/login_page_temp.dart';
import 'features/navigation/presentation/pages/main_navigation.dart';
/// Wrapper ultra-simple de l'application
class UltraSimpleAppWrapper extends StatelessWidget {
const UltraSimpleAppWrapper({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<UltraSimpleAuthBloc, AuthState>(
builder: (context, state) {
switch (state.status) {
case AuthStatus.unknown:
case AuthStatus.checking:
return const SplashScreen();
case AuthStatus.authenticated:
return const MainNavigation();
case AuthStatus.unauthenticated:
case AuthStatus.error:
case AuthStatus.expired:
return const TempLoginPage();
}
},
);
}
}

View File

@@ -0,0 +1,203 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import '../models/auth_state.dart';
import '../services/auth_service.dart';
import '../services/auth_api_service.dart';
import 'auth_event.dart';
/// BLoC pour gérer l'authentification
@singleton
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final AuthService _authService;
late StreamSubscription<AuthState> _authStateSubscription;
AuthBloc(this._authService) : super(const AuthState.unknown()) {
// Écouter les changements d'état du service
_authStateSubscription = _authService.authStateStream.listen(
(authState) => add(AuthStateChanged(authState)),
);
// Gestionnaires d'événements
on<AuthInitializeRequested>(_onInitializeRequested);
on<AuthLoginRequested>(_onLoginRequested);
on<AuthLogoutRequested>(_onLogoutRequested);
on<AuthTokenRefreshRequested>(_onTokenRefreshRequested);
on<AuthSessionExpired>(_onSessionExpired);
on<AuthStatusCheckRequested>(_onStatusCheckRequested);
on<AuthErrorCleared>(_onErrorCleared);
on<AuthStateChanged>(_onStateChanged);
}
/// Initialisation de l'authentification
Future<void> _onInitializeRequested(
AuthInitializeRequested event,
Emitter<AuthState> emit,
) async {
emit(const AuthState.checking());
try {
await _authService.initialize();
} catch (e) {
emit(AuthState.error('Erreur d\'initialisation: $e'));
}
}
/// Gestion de la connexion
Future<void> _onLoginRequested(
AuthLoginRequested event,
Emitter<AuthState> emit,
) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
try {
await _authService.login(event.loginRequest);
// L'état sera mis à jour par le stream du service
} on AuthApiException catch (e) {
emit(state.copyWith(
isLoading: false,
errorMessage: e.message,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
errorMessage: 'Erreur de connexion: $e',
));
}
}
/// Gestion de la déconnexion
Future<void> _onLogoutRequested(
AuthLogoutRequested event,
Emitter<AuthState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
await _authService.logout();
// L'état sera mis à jour par le stream du service
} catch (e) {
// Même en cas d'erreur, on considère que la déconnexion locale a réussi
emit(const AuthState.unauthenticated());
}
}
/// Gestion du rafraîchissement de token
Future<void> _onTokenRefreshRequested(
AuthTokenRefreshRequested event,
Emitter<AuthState> emit,
) async {
// Le rafraîchissement est géré automatiquement par le service
// Cet événement peut être utilisé pour forcer un rafraîchissement manuel
try {
// Le service gère déjà le rafraîchissement automatique
// On peut ajouter ici une logique spécifique si nécessaire
} catch (e) {
emit(AuthState.error('Erreur lors du rafraîchissement: $e'));
}
}
/// Gestion de l'expiration de session
Future<void> _onSessionExpired(
AuthSessionExpired event,
Emitter<AuthState> emit,
) async {
emit(const AuthState.expired());
// Optionnel: essayer un rafraîchissement automatique
try {
await _authService.logout();
} catch (e) {
// Ignorer les erreurs de déconnexion lors de l'expiration
}
}
/// Vérification du statut d'authentification
Future<void> _onStatusCheckRequested(
AuthStatusCheckRequested event,
Emitter<AuthState> emit,
) async {
// Utiliser l'état actuel du service
final currentServiceState = _authService.currentState;
if (currentServiceState != state) {
emit(currentServiceState);
}
}
/// Nettoyage des erreurs
void _onErrorCleared(
AuthErrorCleared event,
Emitter<AuthState> emit,
) {
if (state.errorMessage != null) {
emit(state.copyWith(errorMessage: null));
}
}
/// Mise à jour depuis le service d'authentification
void _onStateChanged(
AuthStateChanged event,
Emitter<AuthState> emit,
) {
final newState = event.authState as AuthState;
// Émettre le nouvel état seulement s'il a changé
if (newState != state) {
emit(newState);
}
}
/// Vérifie si l'utilisateur est connecté
bool get isAuthenticated => state.isAuthenticated;
/// Récupère l'utilisateur actuel
get currentUser => state.user;
/// Vérifie si l'utilisateur a un rôle spécifique
bool hasRole(String role) {
return _authService.hasRole(role);
}
/// Vérifie si l'utilisateur a un des rôles spécifiés
bool hasAnyRole(List<String> roles) {
return _authService.hasAnyRole(roles);
}
/// Vérifie si la session expire bientôt
bool get isSessionExpiringSoon => state.isExpiringSoon;
/// Récupère le message d'erreur formaté
String? get errorMessage {
final error = state.errorMessage;
if (error == null) return null;
// Formatage des messages d'erreur pour l'utilisateur
if (error.contains('network') || error.contains('connexion')) {
return 'Problème de connexion. Vérifiez votre réseau.';
}
if (error.contains('401') || error.contains('Identifiants')) {
return 'Email ou mot de passe incorrect.';
}
if (error.contains('403')) {
return 'Accès non autorisé.';
}
if (error.contains('timeout')) {
return 'Délai d\'attente dépassé. Réessayez.';
}
if (error.contains('server') || error.contains('500')) {
return 'Erreur serveur temporaire. Réessayez plus tard.';
}
return error;
}
@override
Future<void> close() {
_authStateSubscription.cancel();
return super.close();
}
}

View File

@@ -0,0 +1,60 @@
import 'package:equatable/equatable.dart';
import '../models/login_request.dart';
/// Événements d'authentification
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
List<Object?> get props => [];
}
/// Initialiser l'authentification
class AuthInitializeRequested extends AuthEvent {
const AuthInitializeRequested();
}
/// Demande de connexion
class AuthLoginRequested extends AuthEvent {
final LoginRequest loginRequest;
const AuthLoginRequested(this.loginRequest);
@override
List<Object> get props => [loginRequest];
}
/// Demande de déconnexion
class AuthLogoutRequested extends AuthEvent {
const AuthLogoutRequested();
}
/// Demande de rafraîchissement de token
class AuthTokenRefreshRequested extends AuthEvent {
const AuthTokenRefreshRequested();
}
/// Session expirée
class AuthSessionExpired extends AuthEvent {
const AuthSessionExpired();
}
/// Vérification de l'état d'authentification
class AuthStatusCheckRequested extends AuthEvent {
const AuthStatusCheckRequested();
}
/// Réinitialisation de l'erreur
class AuthErrorCleared extends AuthEvent {
const AuthErrorCleared();
}
/// Changement d'état depuis le service
class AuthStateChanged extends AuthEvent {
final dynamic authState;
const AuthStateChanged(this.authState);
@override
List<Object?> get props => [authState];
}

View File

@@ -0,0 +1,74 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../models/auth_state.dart';
import '../services/temp_auth_service.dart';
import 'auth_event.dart';
/// BLoC temporaire pour test sans injection de dépendances
class TempAuthBloc extends Bloc<AuthEvent, AuthState> {
final TempAuthService _authService;
late StreamSubscription<AuthState> _authStateSubscription;
TempAuthBloc(this._authService) : super(const AuthState.unknown()) {
_authStateSubscription = _authService.authStateStream.listen(
(authState) => add(AuthStateChanged(authState)),
);
on<AuthInitializeRequested>(_onInitializeRequested);
on<AuthLoginRequested>(_onLoginRequested);
on<AuthLogoutRequested>(_onLogoutRequested);
on<AuthErrorCleared>(_onErrorCleared);
on<AuthStateChanged>(_onStateChanged);
}
Future<void> _onInitializeRequested(
AuthInitializeRequested event,
Emitter<AuthState> emit,
) async {
await _authService.initialize();
}
Future<void> _onLoginRequested(
AuthLoginRequested event,
Emitter<AuthState> emit,
) async {
try {
await _authService.login(event.loginRequest);
} catch (e) {
emit(AuthState.error(e.toString()));
}
}
Future<void> _onLogoutRequested(
AuthLogoutRequested event,
Emitter<AuthState> emit,
) async {
await _authService.logout();
}
void _onErrorCleared(
AuthErrorCleared event,
Emitter<AuthState> emit,
) {
if (state.errorMessage != null) {
emit(state.copyWith(errorMessage: null));
}
}
void _onStateChanged(
AuthStateChanged event,
Emitter<AuthState> emit,
) {
final newState = event.authState as AuthState;
if (newState != state) {
emit(newState);
}
}
@override
Future<void> close() {
_authStateSubscription.cancel();
_authService.dispose();
return super.close();
}
}

View File

@@ -0,0 +1,140 @@
import 'package:equatable/equatable.dart';
import 'user_info.dart';
/// États d'authentification possibles
enum AuthStatus {
unknown, // État initial
checking, // Vérification en cours
authenticated,// Utilisateur connecté
unauthenticated, // Utilisateur non connecté
expired, // Session expirée
error, // Erreur d'authentification
}
/// État d'authentification de l'application
class AuthState extends Equatable {
final AuthStatus status;
final UserInfo? user;
final String? accessToken;
final String? refreshToken;
final DateTime? expiresAt;
final String? errorMessage;
final bool isLoading;
const AuthState({
this.status = AuthStatus.unknown,
this.user,
this.accessToken,
this.refreshToken,
this.expiresAt,
this.errorMessage,
this.isLoading = false,
});
/// État initial inconnu
const AuthState.unknown() : this(status: AuthStatus.unknown);
/// État de vérification
const AuthState.checking() : this(
status: AuthStatus.checking,
isLoading: true,
);
/// État authentifié
const AuthState.authenticated({
required UserInfo user,
required String accessToken,
required String refreshToken,
required DateTime expiresAt,
}) : this(
status: AuthStatus.authenticated,
user: user,
accessToken: accessToken,
refreshToken: refreshToken,
expiresAt: expiresAt,
isLoading: false,
);
/// État non authentifié
const AuthState.unauthenticated({String? errorMessage}) : this(
status: AuthStatus.unauthenticated,
errorMessage: errorMessage,
isLoading: false,
);
/// État de session expirée
const AuthState.expired() : this(
status: AuthStatus.expired,
isLoading: false,
);
/// État d'erreur
const AuthState.error(String errorMessage) : this(
status: AuthStatus.error,
errorMessage: errorMessage,
isLoading: false,
);
/// Vérifie si l'utilisateur est connecté
bool get isAuthenticated => status == AuthStatus.authenticated;
/// Vérifie si la session est valide
bool get isSessionValid {
if (!isAuthenticated || expiresAt == null) return false;
return DateTime.now().isBefore(expiresAt!);
}
/// Vérifie si la session expire bientôt
bool get isExpiringSoon {
if (!isAuthenticated || expiresAt == null) return false;
final threshold = DateTime.now().add(const Duration(minutes: 5));
return expiresAt!.isBefore(threshold);
}
/// Crée une copie avec des modifications
AuthState copyWith({
AuthStatus? status,
UserInfo? user,
String? accessToken,
String? refreshToken,
DateTime? expiresAt,
String? errorMessage,
bool? isLoading,
}) {
return AuthState(
status: status ?? this.status,
user: user ?? this.user,
accessToken: accessToken ?? this.accessToken,
refreshToken: refreshToken ?? this.refreshToken,
expiresAt: expiresAt ?? this.expiresAt,
errorMessage: errorMessage ?? this.errorMessage,
isLoading: isLoading ?? this.isLoading,
);
}
/// Crée une copie en effaçant les données sensibles
AuthState clearSensitiveData() {
return AuthState(
status: status,
user: user,
errorMessage: errorMessage,
isLoading: isLoading,
);
}
@override
List<Object?> get props => [
status,
user,
accessToken,
refreshToken,
expiresAt,
errorMessage,
isLoading,
];
@override
String toString() {
return 'AuthState(status: $status, user: ${user?.email}, isLoading: $isLoading, error: $errorMessage)';
}
}

View File

@@ -0,0 +1,50 @@
import 'package:equatable/equatable.dart';
/// Modèle de requête de connexion
class LoginRequest extends Equatable {
final String email;
final String password;
final bool rememberMe;
const LoginRequest({
required this.email,
required this.password,
this.rememberMe = false,
});
Map<String, dynamic> toJson() {
return {
'email': email,
'password': password,
'rememberMe': rememberMe,
};
}
factory LoginRequest.fromJson(Map<String, dynamic> json) {
return LoginRequest(
email: json['email'] ?? '',
password: json['password'] ?? '',
rememberMe: json['rememberMe'] ?? false,
);
}
LoginRequest copyWith({
String? email,
String? password,
bool? rememberMe,
}) {
return LoginRequest(
email: email ?? this.email,
password: password ?? this.password,
rememberMe: rememberMe ?? this.rememberMe,
);
}
@override
List<Object?> get props => [email, password, rememberMe];
@override
String toString() {
return 'LoginRequest(email: $email, rememberMe: $rememberMe)';
}
}

View File

@@ -0,0 +1,96 @@
import 'package:equatable/equatable.dart';
import 'user_info.dart';
/// Modèle de réponse de connexion
class LoginResponse extends Equatable {
final String accessToken;
final String refreshToken;
final String tokenType;
final DateTime expiresAt;
final DateTime refreshExpiresAt;
final UserInfo user;
const LoginResponse({
required this.accessToken,
required this.refreshToken,
required this.tokenType,
required this.expiresAt,
required this.refreshExpiresAt,
required this.user,
});
/// Vérifie si le token d'accès est expiré
bool get isAccessTokenExpired {
return DateTime.now().isAfter(expiresAt);
}
/// Vérifie si le refresh token est expiré
bool get isRefreshTokenExpired {
return DateTime.now().isAfter(refreshExpiresAt);
}
/// Vérifie si le token expire dans les prochaines minutes
bool isExpiringSoon({int minutes = 5}) {
final threshold = DateTime.now().add(Duration(minutes: minutes));
return expiresAt.isBefore(threshold);
}
factory LoginResponse.fromJson(Map<String, dynamic> json) {
return LoginResponse(
accessToken: json['accessToken'] ?? '',
refreshToken: json['refreshToken'] ?? '',
tokenType: json['tokenType'] ?? 'Bearer',
expiresAt: json['expiresAt'] != null
? DateTime.parse(json['expiresAt'])
: DateTime.now().add(const Duration(minutes: 15)),
refreshExpiresAt: json['refreshExpiresAt'] != null
? DateTime.parse(json['refreshExpiresAt'])
: DateTime.now().add(const Duration(days: 7)),
user: UserInfo.fromJson(json['user'] ?? {}),
);
}
Map<String, dynamic> toJson() {
return {
'accessToken': accessToken,
'refreshToken': refreshToken,
'tokenType': tokenType,
'expiresAt': expiresAt.toIso8601String(),
'refreshExpiresAt': refreshExpiresAt.toIso8601String(),
'user': user.toJson(),
};
}
LoginResponse copyWith({
String? accessToken,
String? refreshToken,
String? tokenType,
DateTime? expiresAt,
DateTime? refreshExpiresAt,
UserInfo? user,
}) {
return LoginResponse(
accessToken: accessToken ?? this.accessToken,
refreshToken: refreshToken ?? this.refreshToken,
tokenType: tokenType ?? this.tokenType,
expiresAt: expiresAt ?? this.expiresAt,
refreshExpiresAt: refreshExpiresAt ?? this.refreshExpiresAt,
user: user ?? this.user,
);
}
@override
List<Object?> get props => [
accessToken,
refreshToken,
tokenType,
expiresAt,
refreshExpiresAt,
user,
];
@override
String toString() {
return 'LoginResponse(tokenType: $tokenType, user: ${user.email}, expiresAt: $expiresAt)';
}
}

View File

@@ -0,0 +1,5 @@
// Export all auth models
export 'auth_state.dart';
export 'login_request.dart';
export 'login_response.dart';
export 'user_info.dart';

View File

@@ -0,0 +1,90 @@
import 'package:equatable/equatable.dart';
/// Modèle des informations utilisateur
class UserInfo extends Equatable {
final String id;
final String email;
final String firstName;
final String lastName;
final String role;
final String? profilePicture;
final bool isActive;
const UserInfo({
required this.id,
required this.email,
required this.firstName,
required this.lastName,
required this.role,
this.profilePicture,
required this.isActive,
});
String get fullName => '$firstName $lastName';
String get initials {
final f = firstName.isNotEmpty ? firstName[0] : '';
final l = lastName.isNotEmpty ? lastName[0] : '';
return '$f$l'.toUpperCase();
}
factory UserInfo.fromJson(Map<String, dynamic> json) {
return UserInfo(
id: json['id'] ?? '',
email: json['email'] ?? '',
firstName: json['firstName'] ?? '',
lastName: json['lastName'] ?? '',
role: json['role'] ?? 'membre',
profilePicture: json['profilePicture'],
isActive: json['isActive'] ?? true,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'firstName': firstName,
'lastName': lastName,
'role': role,
'profilePicture': profilePicture,
'isActive': isActive,
};
}
UserInfo copyWith({
String? id,
String? email,
String? firstName,
String? lastName,
String? role,
String? profilePicture,
bool? isActive,
}) {
return UserInfo(
id: id ?? this.id,
email: email ?? this.email,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
role: role ?? this.role,
profilePicture: profilePicture ?? this.profilePicture,
isActive: isActive ?? this.isActive,
);
}
@override
List<Object?> get props => [
id,
email,
firstName,
lastName,
role,
profilePicture,
isActive,
];
@override
String toString() {
return 'UserInfo(id: $id, email: $email, fullName: $fullName, role: $role, isActive: $isActive)';
}
}

View File

@@ -0,0 +1,306 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import '../../../core/network/dio_client.dart';
import '../models/login_request.dart';
import '../models/login_response.dart';
/// Service API pour l'authentification
@singleton
class AuthApiService {
final DioClient _dioClient;
late final Dio _dio;
AuthApiService(this._dioClient) {
_dio = _dioClient.dio;
}
/// Effectue la connexion utilisateur
Future<LoginResponse> login(LoginRequest request) async {
try {
final response = await _dio.post(
'/api/auth/login',
data: request.toJson(),
options: Options(
headers: {
'Content-Type': 'application/json',
},
// Désactiver l'interceptor d'auth pour cette requête
extra: {'skipAuth': true},
),
);
if (response.statusCode == 200) {
return LoginResponse.fromJson(response.data);
} else {
throw AuthApiException(
'Erreur de connexion',
statusCode: response.statusCode,
response: response.data,
);
}
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AuthApiException('Erreur inattendue lors de la connexion: $e');
}
}
/// Rafraîchit le token d'accès
Future<LoginResponse> refreshToken(String refreshToken) async {
try {
final response = await _dio.post(
'/api/auth/refresh',
data: {'refreshToken': refreshToken},
options: Options(
headers: {
'Content-Type': 'application/json',
},
extra: {'skipAuth': true},
),
);
if (response.statusCode == 200) {
return LoginResponse.fromJson(response.data);
} else {
throw AuthApiException(
'Erreur lors du rafraîchissement du token',
statusCode: response.statusCode,
response: response.data,
);
}
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AuthApiException('Erreur inattendue lors du rafraîchissement: $e');
}
}
/// Effectue la déconnexion
Future<void> logout(String? refreshToken) async {
try {
await _dio.post(
'/api/auth/logout',
data: refreshToken != null ? {'refreshToken': refreshToken} : {},
options: Options(
headers: {
'Content-Type': 'application/json',
},
extra: {'skipAuth': true},
),
);
} on DioException catch (e) {
// Ignorer les erreurs de déconnexion côté serveur
// La déconnexion locale est plus importante
print('Erreur lors de la déconnexion serveur: ${e.message}');
} catch (e) {
print('Erreur inattendue lors de la déconnexion: $e');
}
}
/// Valide un token côté serveur
Future<bool> validateToken(String accessToken) async {
try {
final response = await _dio.get(
'/api/auth/validate',
options: Options(
headers: {
'Authorization': 'Bearer $accessToken',
'Content-Type': 'application/json',
},
extra: {'skipAuth': true},
),
);
return response.statusCode == 200;
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
return false;
}
throw _handleDioException(e);
} catch (e) {
throw AuthApiException('Erreur lors de la validation du token: $e');
}
}
/// Récupère les informations de l'API d'authentification
Future<Map<String, dynamic>> getAuthInfo() async {
try {
final response = await _dio.get(
'/api/auth/info',
options: Options(
extra: {'skipAuth': true},
),
);
if (response.statusCode == 200) {
return response.data as Map<String, dynamic>;
} else {
throw AuthApiException(
'Erreur lors de la récupération des informations',
statusCode: response.statusCode,
);
}
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
throw AuthApiException('Erreur inattendue: $e');
}
}
/// Gestion centralisée des erreurs Dio
AuthApiException _handleDioException(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return AuthApiException(
'Délai d\'attente dépassé. Vérifiez votre connexion internet.',
type: AuthErrorType.timeout,
);
case DioExceptionType.connectionError:
return AuthApiException(
'Impossible de se connecter au serveur. Vérifiez votre connexion internet.',
type: AuthErrorType.network,
);
case DioExceptionType.badResponse:
final statusCode = e.response?.statusCode;
final data = e.response?.data;
switch (statusCode) {
case 400:
return AuthApiException(
_extractErrorMessage(data) ?? 'Données de requête invalides',
statusCode: statusCode,
type: AuthErrorType.validation,
response: data,
);
case 401:
return AuthApiException(
_extractErrorMessage(data) ?? 'Identifiants invalides',
statusCode: statusCode,
type: AuthErrorType.unauthorized,
response: data,
);
case 403:
return AuthApiException(
_extractErrorMessage(data) ?? 'Accès interdit',
statusCode: statusCode,
type: AuthErrorType.forbidden,
response: data,
);
case 429:
return AuthApiException(
'Trop de tentatives. Veuillez réessayer plus tard.',
statusCode: statusCode,
type: AuthErrorType.rateLimited,
response: data,
);
case 500:
case 502:
case 503:
case 504:
return AuthApiException(
'Erreur serveur temporaire. Veuillez réessayer.',
statusCode: statusCode,
type: AuthErrorType.server,
response: data,
);
default:
return AuthApiException(
_extractErrorMessage(data) ?? 'Erreur serveur inconnue',
statusCode: statusCode,
response: data,
);
}
case DioExceptionType.cancel:
return AuthApiException(
'Requête annulée',
type: AuthErrorType.cancelled,
);
default:
return AuthApiException(
'Erreur réseau: ${e.message}',
type: AuthErrorType.unknown,
);
}
}
/// Extrait le message d'erreur de la réponse
String? _extractErrorMessage(dynamic data) {
if (data == null) return null;
if (data is Map<String, dynamic>) {
return data['message'] ?? data['error'] ?? data['detail'];
}
if (data is String) {
try {
final json = jsonDecode(data) as Map<String, dynamic>;
return json['message'] ?? json['error'] ?? json['detail'];
} catch (_) {
return data;
}
}
return null;
}
}
/// Types d'erreurs d'authentification
enum AuthErrorType {
validation,
unauthorized,
forbidden,
timeout,
network,
server,
rateLimited,
cancelled,
unknown,
}
/// Exception spécifique à l'API d'authentification
class AuthApiException implements Exception {
final String message;
final int? statusCode;
final AuthErrorType type;
final dynamic response;
const AuthApiException(
this.message, {
this.statusCode,
this.type = AuthErrorType.unknown,
this.response,
});
bool get isNetworkError =>
type == AuthErrorType.network ||
type == AuthErrorType.timeout;
bool get isServerError => type == AuthErrorType.server;
bool get isClientError =>
type == AuthErrorType.validation ||
type == AuthErrorType.unauthorized ||
type == AuthErrorType.forbidden;
bool get isRetryable =>
isNetworkError ||
isServerError ||
type == AuthErrorType.rateLimited;
@override
String toString() {
return 'AuthApiException: $message ${statusCode != null ? '(Status: $statusCode)' : ''}';
}
}

View File

@@ -0,0 +1,318 @@
import 'dart:async';
import 'package:injectable/injectable.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import '../models/auth_state.dart';
import '../models/login_request.dart';
import '../models/login_response.dart';
import '../models/user_info.dart';
import '../storage/secure_token_storage.dart';
import 'auth_api_service.dart';
import '../../network/auth_interceptor.dart';
import '../../network/dio_client.dart';
/// Service principal d'authentification
@singleton
class AuthService {
final SecureTokenStorage _tokenStorage;
final AuthApiService _apiService;
final AuthInterceptor _authInterceptor;
final DioClient _dioClient;
// Stream controllers pour notifier les changements d'état
final _authStateController = StreamController<AuthState>.broadcast();
final _tokenRefreshController = StreamController<void>.broadcast();
// Timers pour la gestion automatique des tokens
Timer? _tokenRefreshTimer;
Timer? _sessionExpiryTimer;
// État actuel
AuthState _currentState = const AuthState.unknown();
AuthService(
this._tokenStorage,
this._apiService,
this._authInterceptor,
this._dioClient,
) {
_initializeAuthInterceptor();
}
// Getters
Stream<AuthState> get authStateStream => _authStateController.stream;
AuthState get currentState => _currentState;
bool get isAuthenticated => _currentState.isAuthenticated;
UserInfo? get currentUser => _currentState.user;
/// Initialise l'interceptor d'authentification
void _initializeAuthInterceptor() {
_authInterceptor.setCallbacks(
onTokenRefreshNeeded: () => _refreshTokenSilently(),
onAuthenticationFailed: () => logout(),
);
_dioClient.addAuthInterceptor(_authInterceptor);
}
/// Initialise le service d'authentification
Future<void> initialize() async {
_updateState(const AuthState.checking());
try {
// Vérifier si des tokens existent
final hasTokens = await _tokenStorage.hasAuthData();
if (!hasTokens) {
_updateState(const AuthState.unauthenticated());
return;
}
// Récupérer les données d'authentification
final authData = await _tokenStorage.getAuthData();
if (authData == null) {
_updateState(const AuthState.unauthenticated());
return;
}
// Vérifier si les tokens sont expirés
if (authData.isRefreshTokenExpired) {
await _tokenStorage.clearAuthData();
_updateState(const AuthState.unauthenticated());
return;
}
// Si le token d'accès est expiré, essayer de le rafraîchir
if (authData.isAccessTokenExpired) {
await _refreshToken();
return;
}
// Valider le token côté serveur
final isValid = await _validateTokenWithServer(authData.accessToken);
if (!isValid) {
await _refreshToken();
return;
}
// Tout est OK, restaurer la session
_updateState(AuthState.authenticated(
user: authData.user,
accessToken: authData.accessToken,
refreshToken: authData.refreshToken,
expiresAt: authData.expiresAt,
));
_scheduleTokenRefresh();
_scheduleSessionExpiry();
} catch (e) {
print('Erreur lors de l\'initialisation de l\'auth: $e');
await _tokenStorage.clearAuthData();
_updateState(AuthState.error('Erreur d\'initialisation: $e'));
}
}
/// Connecte un utilisateur
Future<void> login(LoginRequest request) async {
_updateState(_currentState.copyWith(isLoading: true));
try {
// Appel API de connexion
final response = await _apiService.login(request);
// Sauvegarder les tokens
await _tokenStorage.saveAuthData(response);
// Mettre à jour l'état
_updateState(AuthState.authenticated(
user: response.user,
accessToken: response.accessToken,
refreshToken: response.refreshToken,
expiresAt: response.expiresAt,
));
// Programmer les rafraîchissements
_scheduleTokenRefresh();
_scheduleSessionExpiry();
} on AuthApiException catch (e) {
_updateState(AuthState.error(e.message));
rethrow;
} catch (e) {
final errorMessage = 'Erreur de connexion: $e';
_updateState(AuthState.error(errorMessage));
rethrow;
}
}
/// Déconnecte l'utilisateur
Future<void> logout() async {
try {
// Récupérer le refresh token pour l'invalider côté serveur
final refreshToken = await _tokenStorage.getRefreshToken();
// Appel API de déconnexion (optionnel)
await _apiService.logout(refreshToken);
} catch (e) {
print('Erreur lors de la déconnexion serveur: $e');
}
// Nettoyage local (toujours fait)
await _tokenStorage.clearAuthData();
_cancelTimers();
_updateState(const AuthState.unauthenticated());
}
/// Rafraîchit le token d'accès
Future<void> _refreshToken() async {
try {
final refreshToken = await _tokenStorage.getRefreshToken();
if (refreshToken == null) {
throw Exception('Aucun refresh token disponible');
}
// Vérifier si le refresh token est expiré
final refreshExpiresAt = await _tokenStorage.getRefreshTokenExpirationDate();
if (refreshExpiresAt != null && DateTime.now().isAfter(refreshExpiresAt)) {
throw Exception('Refresh token expiré');
}
// Appel API de refresh
final response = await _apiService.refreshToken(refreshToken);
// Mettre à jour le stockage
await _tokenStorage.updateAccessToken(response.accessToken, response.expiresAt);
// Mettre à jour l'état
if (_currentState.isAuthenticated) {
_updateState(_currentState.copyWith(
accessToken: response.accessToken,
expiresAt: response.expiresAt,
));
} else {
_updateState(AuthState.authenticated(
user: response.user,
accessToken: response.accessToken,
refreshToken: response.refreshToken,
expiresAt: response.expiresAt,
));
}
// Reprogrammer les timers
_scheduleTokenRefresh();
} catch (e) {
print('Erreur lors du rafraîchissement du token: $e');
await logout();
}
}
/// Rafraîchit le token silencieusement (sans changer l'état de loading)
Future<void> _refreshTokenSilently() async {
try {
await _refreshToken();
_tokenRefreshController.add(null);
} catch (e) {
print('Erreur lors du rafraîchissement silencieux: $e');
}
}
/// Valide un token côté serveur
Future<bool> _validateTokenWithServer(String accessToken) async {
try {
return await _apiService.validateToken(accessToken);
} catch (e) {
print('Erreur lors de la validation du token: $e');
return false;
}
}
/// Programme le rafraîchissement automatique du token
void _scheduleTokenRefresh() {
_tokenRefreshTimer?.cancel();
if (!_currentState.isAuthenticated || _currentState.expiresAt == null) {
return;
}
// Rafraîchir 5 minutes avant l'expiration
final refreshTime = _currentState.expiresAt!.subtract(const Duration(minutes: 5));
final delay = refreshTime.difference(DateTime.now());
if (delay.isNegative) {
// Le token expire bientôt, rafraîchir immédiatement
_refreshTokenSilently();
return;
}
_tokenRefreshTimer = Timer(delay, () => _refreshTokenSilently());
}
/// Programme l'expiration de la session
void _scheduleSessionExpiry() {
_sessionExpiryTimer?.cancel();
if (!_currentState.isAuthenticated || _currentState.expiresAt == null) {
return;
}
final delay = _currentState.expiresAt!.difference(DateTime.now());
if (delay.isNegative) {
logout();
return;
}
_sessionExpiryTimer = Timer(delay, () {
_updateState(const AuthState.expired());
});
}
/// Annule tous les timers
void _cancelTimers() {
_tokenRefreshTimer?.cancel();
_sessionExpiryTimer?.cancel();
_tokenRefreshTimer = null;
_sessionExpiryTimer = null;
}
/// Met à jour l'état et notifie les listeners
void _updateState(AuthState newState) {
_currentState = newState;
_authStateController.add(newState);
}
/// Nettoie les ressources
void dispose() {
_cancelTimers();
_authStateController.close();
_tokenRefreshController.close();
}
/// Vérifie les permissions de l'utilisateur
bool hasRole(String role) {
return _currentState.user?.role == role;
}
/// Vérifie si l'utilisateur a un des rôles spécifiés
bool hasAnyRole(List<String> roles) {
final userRole = _currentState.user?.role;
return userRole != null && roles.contains(userRole);
}
/// Décode un token JWT (utilitaire)
Map<String, dynamic>? decodeToken(String token) {
try {
return JwtDecoder.decode(token);
} catch (e) {
print('Erreur lors du décodage du token: $e');
return null;
}
}
/// Vérifie si un token est expiré
bool isTokenExpired(String token) {
try {
return JwtDecoder.isExpired(token);
} catch (e) {
return true;
}
}
}

View File

@@ -0,0 +1,70 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/auth_state.dart';
import '../models/login_request.dart';
import '../models/user_info.dart';
/// Service d'authentification temporaire pour test sans dépendances
class TempAuthService {
final _authStateController = StreamController<AuthState>.broadcast();
AuthState _currentState = const AuthState.unknown();
Stream<AuthState> get authStateStream => _authStateController.stream;
AuthState get currentState => _currentState;
bool get isAuthenticated => _currentState.isAuthenticated;
UserInfo? get currentUser => _currentState.user;
Future<void> initialize() async {
_updateState(const AuthState.checking());
// Simuler une vérification
await Future.delayed(const Duration(seconds: 2));
_updateState(const AuthState.unauthenticated());
}
Future<void> login(LoginRequest request) async {
_updateState(_currentState.copyWith(isLoading: true));
try {
// Simulation d'appel API
await Future.delayed(const Duration(seconds: 1));
// Vérification simple pour la démo
if (request.email == 'admin@unionflow.dev' && request.password == 'admin123') {
final user = UserInfo(
id: '1',
email: request.email,
firstName: 'Admin',
lastName: 'UnionFlow',
role: 'admin',
isActive: true,
);
_updateState(AuthState.authenticated(
user: user,
accessToken: 'fake_access_token',
refreshToken: 'fake_refresh_token',
expiresAt: DateTime.now().add(const Duration(hours: 1)),
));
} else {
throw Exception('Identifiants invalides');
}
} catch (e) {
_updateState(AuthState.error(e.toString()));
}
}
Future<void> logout() async {
_updateState(const AuthState.unauthenticated());
}
void _updateState(AuthState newState) {
_currentState = newState;
_authStateController.add(newState);
}
void dispose() {
_authStateController.close();
}
}

View File

@@ -0,0 +1,86 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/auth_state.dart';
import '../models/login_request.dart';
import '../models/user_info.dart';
/// Service d'authentification ultra-simple sans aucune dépendance externe
class UltraSimpleAuthService {
final _authStateController = StreamController<AuthState>.broadcast();
AuthState _currentState = const AuthState.unknown();
Stream<AuthState> get authStateStream => _authStateController.stream;
AuthState get currentState => _currentState;
bool get isAuthenticated => _currentState.isAuthenticated;
UserInfo? get currentUser => _currentState.user;
Future<void> initialize() async {
_updateState(const AuthState.checking());
// Simuler une vérification
await Future.delayed(const Duration(seconds: 2));
_updateState(const AuthState.unauthenticated());
}
Future<void> login(LoginRequest request) async {
_updateState(_currentState.copyWith(isLoading: true));
try {
// Simulation d'appel API
await Future.delayed(const Duration(seconds: 1));
// Vérification simple pour la démo
if (request.email == 'admin@unionflow.dev' && request.password == 'admin123') {
final user = UserInfo(
id: '1',
email: request.email,
firstName: 'Admin',
lastName: 'UnionFlow',
role: 'admin',
isActive: true,
);
_updateState(AuthState.authenticated(
user: user,
accessToken: 'fake_access_token_${DateTime.now().millisecondsSinceEpoch}',
refreshToken: 'fake_refresh_token_${DateTime.now().millisecondsSinceEpoch}',
expiresAt: DateTime.now().add(const Duration(hours: 1)),
));
} else if (request.email == 'president@lions.org' && request.password == 'admin123') {
final user = UserInfo(
id: '2',
email: request.email,
firstName: 'Jean',
lastName: 'Dupont',
role: 'président',
isActive: true,
);
_updateState(AuthState.authenticated(
user: user,
accessToken: 'fake_access_token_${DateTime.now().millisecondsSinceEpoch}',
refreshToken: 'fake_refresh_token_${DateTime.now().millisecondsSinceEpoch}',
expiresAt: DateTime.now().add(const Duration(hours: 1)),
));
} else {
throw Exception('Identifiants invalides');
}
} catch (e) {
_updateState(AuthState.error(e.toString()));
}
}
Future<void> logout() async {
_updateState(const AuthState.unauthenticated());
}
void _updateState(AuthState newState) {
_currentState = newState;
_authStateController.add(newState);
}
void dispose() {
_authStateController.close();
}
}

View File

@@ -0,0 +1,117 @@
import 'dart:convert';
import '../models/login_response.dart';
import '../models/user_info.dart';
/// Service de stockage en mémoire des tokens (temporaire pour contourner Java 21)
class MemoryTokenStorage {
static final MemoryTokenStorage _instance = MemoryTokenStorage._internal();
factory MemoryTokenStorage() => _instance;
MemoryTokenStorage._internal();
// Stockage en mémoire
final Map<String, String> _storage = {};
static const String _accessTokenKey = 'access_token';
static const String _refreshTokenKey = 'refresh_token';
static const String _userInfoKey = 'user_info';
static const String _expiresAtKey = 'expires_at';
static const String _refreshExpiresAtKey = 'refresh_expires_at';
/// Sauvegarde les données d'authentification
Future<void> saveAuthData(LoginResponse loginResponse) async {
try {
_storage[_accessTokenKey] = loginResponse.accessToken;
_storage[_refreshTokenKey] = loginResponse.refreshToken;
_storage[_userInfoKey] = jsonEncode(loginResponse.user.toJson());
_storage[_expiresAtKey] = loginResponse.expiresAt.toIso8601String();
_storage[_refreshExpiresAtKey] = loginResponse.refreshExpiresAt.toIso8601String();
} catch (e) {
throw StorageException('Erreur lors de la sauvegarde des données d\'authentification: $e');
}
}
/// Récupère le token d'accès
Future<String?> getAccessToken() async {
return _storage[_accessTokenKey];
}
/// Récupère le refresh token
Future<String?> getRefreshToken() async {
return _storage[_refreshTokenKey];
}
/// Récupère les informations utilisateur
Future<UserInfo?> getUserInfo() async {
try {
final userJson = _storage[_userInfoKey];
if (userJson == null) return null;
final userMap = jsonDecode(userJson) as Map<String, dynamic>;
return UserInfo.fromJson(userMap);
} catch (e) {
throw StorageException('Erreur lors de la récupération des informations utilisateur: $e');
}
}
/// Récupère la date d'expiration du token d'accès
Future<DateTime?> getTokenExpirationDate() async {
try {
final expiresAtString = _storage[_expiresAtKey];
if (expiresAtString == null) return null;
return DateTime.parse(expiresAtString);
} catch (e) {
throw StorageException('Erreur lors de la récupération de la date d\'expiration: $e');
}
}
/// Récupère la date d'expiration du refresh token
Future<DateTime?> getRefreshTokenExpirationDate() async {
try {
final expiresAtString = _storage[_refreshExpiresAtKey];
if (expiresAtString == null) return null;
return DateTime.parse(expiresAtString);
} catch (e) {
throw StorageException('Erreur lors de la récupération de la date d\'expiration du refresh token: $e');
}
}
/// Vérifie si l'utilisateur est authentifié
Future<bool> hasValidToken() async {
final token = await getAccessToken();
if (token == null) return false;
final expirationDate = await getTokenExpirationDate();
if (expirationDate == null) return false;
return DateTime.now().isBefore(expirationDate);
}
/// Efface toutes les données d'authentification
Future<void> clearAll() async {
_storage.clear();
}
/// Met à jour uniquement les tokens
Future<void> updateTokens({
required String accessToken,
required String refreshToken,
required DateTime expiresAt,
required DateTime refreshExpiresAt,
}) async {
_storage[_accessTokenKey] = accessToken;
_storage[_refreshTokenKey] = refreshToken;
_storage[_expiresAtKey] = expiresAt.toIso8601String();
_storage[_refreshExpiresAtKey] = refreshExpiresAt.toIso8601String();
}
}
/// Exception personnalisée pour les erreurs de stockage
class StorageException implements Exception {
final String message;
StorageException(this.message);
@override
String toString() => 'StorageException: $message';
}

View File

@@ -0,0 +1,246 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:injectable/injectable.dart';
import '../models/login_response.dart';
import '../models/user_info.dart';
/// Service de stockage sécurisé des tokens d'authentification
@singleton
class SecureTokenStorage {
static const String _accessTokenKey = 'access_token';
static const String _refreshTokenKey = 'refresh_token';
static const String _userInfoKey = 'user_info';
static const String _expiresAtKey = 'expires_at';
static const String _refreshExpiresAtKey = 'refresh_expires_at';
static const String _biometricEnabledKey = 'biometric_enabled';
// Utilise SharedPreferences temporairement pour Android
Future<SharedPreferences> get _prefs => SharedPreferences.getInstance();
/// Sauvegarde les données d'authentification
Future<void> saveAuthData(LoginResponse loginResponse) async {
try {
final prefs = await _prefs;
await Future.wait([
prefs.setString(_accessTokenKey, loginResponse.accessToken),
prefs.setString(_refreshTokenKey, loginResponse.refreshToken),
prefs.setString(_userInfoKey, jsonEncode(loginResponse.user.toJson())),
prefs.setString(_expiresAtKey, loginResponse.expiresAt.toIso8601String()),
prefs.setString(_refreshExpiresAtKey, loginResponse.refreshExpiresAt.toIso8601String()),
]);
} catch (e) {
throw StorageException('Erreur lors de la sauvegarde des données d\'authentification: $e');
}
}
/// Récupère le token d'accès
Future<String?> getAccessToken() async {
try {
final prefs = await _prefs;
return prefs.getString(_accessTokenKey);
} catch (e) {
throw StorageException('Erreur lors de la récupération du token d\'accès: $e');
}
}
/// Récupère le refresh token
Future<String?> getRefreshToken() async {
try {
final prefs = await _prefs;
return prefs.getString(_refreshTokenKey);
} catch (e) {
throw StorageException('Erreur lors de la récupération du refresh token: $e');
}
}
/// Récupère les informations utilisateur
Future<UserInfo?> getUserInfo() async {
try {
final prefs = await _prefs;
final userJson = prefs.getString(_userInfoKey);
if (userJson == null) return null;
final userMap = jsonDecode(userJson) as Map<String, dynamic>;
return UserInfo.fromJson(userMap);
} catch (e) {
throw StorageException('Erreur lors de la récupération des informations utilisateur: $e');
}
}
/// Récupère la date d'expiration du token d'accès
Future<DateTime?> getTokenExpirationDate() async {
try {
final prefs = await _prefs;
final expiresAtString = prefs.getString(_expiresAtKey);
if (expiresAtString == null) return null;
return DateTime.parse(expiresAtString);
} catch (e) {
throw StorageException('Erreur lors de la récupération de la date d\'expiration: $e');
}
}
/// Récupère la date d'expiration du refresh token
Future<DateTime?> getRefreshTokenExpirationDate() async {
try {
final expiresAtString = await _storage.read(key: _refreshExpiresAtKey);
if (expiresAtString == null) return null;
return DateTime.parse(expiresAtString);
} catch (e) {
throw StorageException('Erreur lors de la récupération de la date d\'expiration du refresh token: $e');
}
}
/// Récupère toutes les données d'authentification
Future<LoginResponse?> getAuthData() async {
try {
final results = await Future.wait([
getAccessToken(),
getRefreshToken(),
getUserInfo(),
getTokenExpirationDate(),
getRefreshTokenExpirationDate(),
]);
final accessToken = results[0] as String?;
final refreshToken = results[1] as String?;
final userInfo = results[2] as UserInfo?;
final expiresAt = results[3] as DateTime?;
final refreshExpiresAt = results[4] as DateTime?;
if (accessToken == null ||
refreshToken == null ||
userInfo == null ||
expiresAt == null ||
refreshExpiresAt == null) {
return null;
}
return LoginResponse(
accessToken: accessToken,
refreshToken: refreshToken,
tokenType: 'Bearer',
expiresAt: expiresAt,
refreshExpiresAt: refreshExpiresAt,
user: userInfo,
);
} catch (e) {
throw StorageException('Erreur lors de la récupération des données d\'authentification: $e');
}
}
/// Met à jour le token d'accès
Future<void> updateAccessToken(String accessToken, DateTime expiresAt) async {
try {
await Future.wait([
_storage.write(key: _accessTokenKey, value: accessToken),
_storage.write(key: _expiresAtKey, value: expiresAt.toIso8601String()),
]);
} catch (e) {
throw StorageException('Erreur lors de la mise à jour du token d\'accès: $e');
}
}
/// Vérifie si les données d'authentification existent
Future<bool> hasAuthData() async {
try {
final accessToken = await _storage.read(key: _accessTokenKey);
final refreshToken = await _storage.read(key: _refreshTokenKey);
return accessToken != null && refreshToken != null;
} catch (e) {
return false;
}
}
/// Vérifie si les tokens sont expirés
Future<bool> areTokensExpired() async {
try {
final expiresAt = await getTokenExpirationDate();
final refreshExpiresAt = await getRefreshTokenExpirationDate();
if (expiresAt == null || refreshExpiresAt == null) return true;
final now = DateTime.now();
return refreshExpiresAt.isBefore(now);
} catch (e) {
return true;
}
}
/// Vérifie si le token d'accès expire bientôt
Future<bool> isAccessTokenExpiringSoon({int minutes = 5}) async {
try {
final expiresAt = await getTokenExpirationDate();
if (expiresAt == null) return true;
final threshold = DateTime.now().add(Duration(minutes: minutes));
return expiresAt.isBefore(threshold);
} catch (e) {
return true;
}
}
/// Efface toutes les données d'authentification
Future<void> clearAuthData() async {
try {
await Future.wait([
_storage.delete(key: _accessTokenKey),
_storage.delete(key: _refreshTokenKey),
_storage.delete(key: _userInfoKey),
_storage.delete(key: _expiresAtKey),
_storage.delete(key: _refreshExpiresAtKey),
]);
} catch (e) {
throw StorageException('Erreur lors de l\'effacement des données d\'authentification: $e');
}
}
/// Active/désactive l'authentification biométrique
Future<void> setBiometricEnabled(bool enabled) async {
try {
await _storage.write(key: _biometricEnabledKey, value: enabled.toString());
} catch (e) {
throw StorageException('Erreur lors de la configuration biométrique: $e');
}
}
/// Vérifie si l'authentification biométrique est activée
Future<bool> isBiometricEnabled() async {
try {
final enabled = await _storage.read(key: _biometricEnabledKey);
return enabled == 'true';
} catch (e) {
return false;
}
}
/// Efface toutes les données stockées
Future<void> clearAll() async {
try {
await _storage.deleteAll();
} catch (e) {
throw StorageException('Erreur lors de l\'effacement de toutes les données: $e');
}
}
/// Vérifie si le stockage sécurisé est disponible
Future<bool> isAvailable() async {
try {
await _storage.containsKey(key: 'test');
return true;
} catch (e) {
return false;
}
}
}
/// Exception liée au stockage
class StorageException implements Exception {
final String message;
const StorageException(this.message);
@override
String toString() => 'StorageException: $message';
}

View File

@@ -0,0 +1,74 @@
class AppConstants {
// API Configuration
static const String baseUrl = 'http://localhost:8099'; // Backend UnionFlow
static const String apiVersion = '/api/v1';
// Timeout
static const Duration connectTimeout = Duration(seconds: 30);
static const Duration receiveTimeout = Duration(seconds: 30);
// Storage Keys
static const String authTokenKey = 'auth_token';
static const String refreshTokenKey = 'refresh_token';
static const String userDataKey = 'user_data';
static const String appSettingsKey = 'app_settings';
// API Endpoints
static const String loginEndpoint = '/auth/login';
static const String refreshEndpoint = '/auth/refresh';
static const String membresEndpoint = '/membres';
static const String cotisationsEndpoint = '/finance/cotisations';
static const String evenementsEndpoint = '/evenements';
static const String statistiquesEndpoint = '/statistiques';
// App Configuration
static const String appName = 'UnionFlow';
static const String appVersion = '2.0.0';
static const int maxRetryAttempts = 3;
// Pagination
static const int defaultPageSize = 20;
static const int maxPageSize = 100;
// File Upload
static const int maxFileSize = 10 * 1024 * 1024; // 10MB
static const List<String> allowedImageTypes = ['jpg', 'jpeg', 'png', 'gif'];
static const List<String> allowedDocumentTypes = ['pdf', 'doc', 'docx'];
// Chart Colors
static const List<String> chartColors = [
'#2196F3', '#4CAF50', '#FF9800', '#F44336',
'#9C27B0', '#00BCD4', '#8BC34A', '#FFEB3B'
];
}
class ApiEndpoints {
// Authentication
static const String login = '/auth/login';
static const String logout = '/auth/logout';
static const String register = '/auth/register';
static const String refreshToken = '/auth/refresh';
static const String forgotPassword = '/auth/forgot-password';
// Membres
static const String membres = '/membres';
static const String membreProfile = '/membres/profile';
static const String membreSearch = '/membres/search';
static const String membreStats = '/membres/statistiques';
// Cotisations
static const String cotisations = '/finance/cotisations';
static const String cotisationsPay = '/finance/cotisations/payer';
static const String cotisationsHistory = '/finance/cotisations/historique';
static const String cotisationsStats = '/finance/cotisations/statistiques';
// Événements
static const String evenements = '/evenements';
static const String evenementParticipants = '/evenements/{id}/participants';
static const String evenementDocuments = '/evenements/{id}/documents';
// Dashboard
static const String dashboardStats = '/dashboard/statistiques';
static const String dashboardCharts = '/dashboard/charts';
static const String dashboardNotifications = '/dashboard/notifications';
}

View File

@@ -0,0 +1,45 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// InjectableConfigGenerator
// **************************************************************************
// ignore_for_file: unnecessary_lambdas
// ignore_for_file: lines_longer_than_80_chars
// coverage:ignore-file
import 'package:get_it/get_it.dart' as _i1;
import 'package:injectable/injectable.dart' as _i2;
import '../auth/bloc/auth_bloc.dart' as _i7;
import '../auth/services/auth_api_service.dart' as _i4;
import '../auth/services/auth_service.dart' as _i6;
import '../auth/storage/secure_token_storage.dart' as _i3;
import '../network/auth_interceptor.dart' as _i5;
import '../network/dio_client.dart' as _i8;
extension GetItInjectableX on _i1.GetIt {
// initializes the registration of main-scope dependencies inside of GetIt
Future<_i1.GetIt> init({
String? environment,
_i2.EnvironmentFilter? environmentFilter,
}) async {
final gh = _i2.GetItHelper(
this,
environment,
environmentFilter,
);
gh.singleton<_i3.SecureTokenStorage>(() => _i3.SecureTokenStorage());
gh.singleton<_i8.DioClient>(() => _i8.DioClient());
gh.singleton<_i5.AuthInterceptor>(() => _i5.AuthInterceptor(gh<_i3.SecureTokenStorage>()));
gh.singleton<_i4.AuthApiService>(() => _i4.AuthApiService(gh<_i8.DioClient>()));
gh.singleton<_i6.AuthService>(() => _i6.AuthService(
gh<_i3.SecureTokenStorage>(),
gh<_i4.AuthApiService>(),
gh<_i5.AuthInterceptor>(),
gh<_i8.DioClient>(),
));
gh.singleton<_i7.AuthBloc>(() => _i7.AuthBloc(gh<_i6.AuthService>()));
return this;
}
}

View File

@@ -0,0 +1,19 @@
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injection.config.dart';
/// Instance globale de GetIt pour l'injection de dépendances
final GetIt getIt = GetIt.instance;
/// Configure l'injection de dépendances
@InjectableInit()
Future<void> configureDependencies() async {
await getIt.init();
}
/// Réinitialise les dépendances (utile pour les tests)
Future<void> resetDependencies() async {
await getIt.reset();
await configureDependencies();
}

View File

@@ -0,0 +1,122 @@
import 'package:equatable/equatable.dart';
abstract class Failure extends Equatable {
const Failure({required this.message, this.code});
final String message;
final String? code;
@override
List<Object?> get props => [message, code];
}
class ServerFailure extends Failure {
const ServerFailure({
required super.message,
super.code,
this.statusCode,
});
final int? statusCode;
@override
List<Object?> get props => [message, code, statusCode];
}
class NetworkFailure extends Failure {
const NetworkFailure({
required super.message,
super.code = 'NETWORK_ERROR',
});
}
class AuthFailure extends Failure {
const AuthFailure({
required super.message,
super.code = 'AUTH_ERROR',
});
}
class ValidationFailure extends Failure {
const ValidationFailure({
required super.message,
super.code = 'VALIDATION_ERROR',
this.field,
});
final String? field;
@override
List<Object?> get props => [message, code, field];
}
class CacheFailure extends Failure {
const CacheFailure({
required super.message,
super.code = 'CACHE_ERROR',
});
}
class UnknownFailure extends Failure {
const UnknownFailure({
required super.message,
super.code = 'UNKNOWN_ERROR',
});
}
// Extension pour convertir les exceptions en failures
extension ExceptionToFailure on Exception {
Failure toFailure() {
if (this is NetworkException) {
final ex = this as NetworkException;
return NetworkFailure(message: ex.message);
} else if (this is ServerException) {
final ex = this as ServerException;
return ServerFailure(
message: ex.message,
statusCode: ex.statusCode,
);
} else if (this is AuthException) {
final ex = this as AuthException;
return AuthFailure(message: ex.message);
} else if (this is ValidationException) {
final ex = this as ValidationException;
return ValidationFailure(
message: ex.message,
field: ex.field,
);
} else if (this is CacheException) {
final ex = this as CacheException;
return CacheFailure(message: ex.message);
}
return UnknownFailure(message: toString());
}
}
// Exceptions personnalisées
class NetworkException implements Exception {
const NetworkException(this.message);
final String message;
}
class ServerException implements Exception {
const ServerException(this.message, {this.statusCode});
final String message;
final int? statusCode;
}
class AuthException implements Exception {
const AuthException(this.message);
final String message;
}
class ValidationException implements Exception {
const ValidationException(this.message, {this.field});
final String message;
final String? field;
}
class CacheException implements Exception {
const CacheException(this.message);
final String message;
}

View File

@@ -0,0 +1,167 @@
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import '../auth/storage/secure_token_storage.dart';
/// Interceptor pour gérer l'authentification automatique
@singleton
class AuthInterceptor extends Interceptor {
final SecureTokenStorage _tokenStorage;
// Callback pour déclencher le refresh token
void Function()? onTokenRefreshNeeded;
// Callback pour déconnecter l'utilisateur
void Function()? onAuthenticationFailed;
AuthInterceptor(this._tokenStorage);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
// Ignorer l'authentification pour certaines routes
if (_shouldSkipAuth(options)) {
handler.next(options);
return;
}
try {
// Récupérer le token d'accès
final accessToken = await _tokenStorage.getAccessToken();
if (accessToken != null) {
// Vérifier si le token expire bientôt
final isExpiringSoon = await _tokenStorage.isAccessTokenExpiringSoon();
if (isExpiringSoon) {
// Déclencher le refresh token si nécessaire
onTokenRefreshNeeded?.call();
}
// Ajouter le token à l'en-tête Authorization
options.headers['Authorization'] = 'Bearer $accessToken';
}
handler.next(options);
} catch (e) {
// En cas d'erreur, continuer sans token
print('Erreur lors de la récupération du token: $e');
handler.next(options);
}
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
// Traitement des réponses réussies
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// Gestion des erreurs d'authentification
if (err.response?.statusCode == 401) {
await _handle401Error(err, handler);
} else if (err.response?.statusCode == 403) {
await _handle403Error(err, handler);
} else {
handler.next(err);
}
}
/// Gère les erreurs 401 (Non autorisé)
Future<void> _handle401Error(DioException err, ErrorInterceptorHandler handler) async {
try {
// Vérifier si on a un refresh token valide
final refreshToken = await _tokenStorage.getRefreshToken();
final refreshExpiresAt = await _tokenStorage.getRefreshTokenExpirationDate();
if (refreshToken != null &&
refreshExpiresAt != null &&
DateTime.now().isBefore(refreshExpiresAt)) {
// Tentative de refresh du token
onTokenRefreshNeeded?.call();
// Attendre un peu pour laisser le temps au refresh
await Future.delayed(const Duration(milliseconds: 100));
// Retry de la requête originale avec le nouveau token
final newAccessToken = await _tokenStorage.getAccessToken();
if (newAccessToken != null) {
final newRequest = await _retryRequest(err.requestOptions, newAccessToken);
handler.resolve(newRequest);
return;
}
}
// Si le refresh n'est pas possible ou a échoué, déconnecter l'utilisateur
await _tokenStorage.clearAuthData();
onAuthenticationFailed?.call();
} catch (e) {
print('Erreur lors de la gestion de l\'erreur 401: $e');
await _tokenStorage.clearAuthData();
onAuthenticationFailed?.call();
}
handler.next(err);
}
/// Gère les erreurs 403 (Interdit)
Future<void> _handle403Error(DioException err, ErrorInterceptorHandler handler) async {
// L'utilisateur n'a pas les permissions suffisantes
// On peut logger cela ou rediriger vers une page d'erreur
print('Accès interdit (403) pour: ${err.requestOptions.path}');
handler.next(err);
}
/// Retry une requête avec un nouveau token
Future<Response> _retryRequest(RequestOptions options, String newAccessToken) async {
final dio = Dio();
// Copier les options originales
final newOptions = Options(
method: options.method,
headers: {
...options.headers,
'Authorization': 'Bearer $newAccessToken',
},
extra: {'skipAuth': true}, // Éviter la récursion infinie
);
// Effectuer la nouvelle requête
return await dio.request(
options.path,
data: options.data,
queryParameters: options.queryParameters,
options: newOptions,
);
}
/// Détermine si l'authentification doit être ignorée pour une requête
bool _shouldSkipAuth(RequestOptions options) {
// Ignorer l'auth pour les routes publiques
final publicPaths = [
'/api/auth/login',
'/api/auth/refresh',
'/api/auth/info',
'/api/auth/register',
'/api/health',
];
// Vérifier si le path est dans la liste des routes publiques
final isPublicPath = publicPaths.any((path) => options.path.contains(path));
// Vérifier si l'option skipAuth est activée
final skipAuth = options.extra['skipAuth'] == true;
return isPublicPath || skipAuth;
}
/// Configuration des callbacks
void setCallbacks({
void Function()? onTokenRefreshNeeded,
void Function()? onAuthenticationFailed,
}) {
this.onTokenRefreshNeeded = onTokenRefreshNeeded;
this.onAuthenticationFailed = onAuthenticationFailed;
}
}

View File

@@ -0,0 +1,113 @@
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import 'auth_interceptor.dart';
/// Configuration centralisée du client HTTP Dio
@singleton
class DioClient {
late final Dio _dio;
DioClient() {
_dio = Dio();
_setupInterceptors();
_configureOptions();
}
Dio get dio => _dio;
void _configureOptions() {
_dio.options = BaseOptions(
// URL de base de l'API
baseUrl: 'http://localhost:8081', // Adresse de votre API Quarkus
// Timeouts
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
// Headers par défaut
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'UnionFlow-Mobile/1.0.0',
},
// Validation des codes de statut
validateStatus: (status) {
return status != null && status < 500;
},
// Suivre les redirections
followRedirects: true,
maxRedirects: 3,
// Politique de persistance des cookies
persistentConnection: true,
// Format de réponse par défaut
responseType: ResponseType.json,
);
}
void _setupInterceptors() {
// Interceptor de logging (seulement en debug)
_dio.interceptors.add(
PrettyDioLogger(
requestHeader: true,
requestBody: true,
responseBody: true,
responseHeader: false,
error: true,
compact: true,
maxWidth: 90,
filter: (options, args) {
// Ne pas logger les mots de passe
if (options.path.contains('/auth/login')) {
return false;
}
return true;
},
),
);
// Interceptor d'authentification (sera injecté plus tard)
// Il sera ajouté dans AuthService pour éviter les dépendances circulaires
}
/// Ajoute l'interceptor d'authentification
void addAuthInterceptor(AuthInterceptor authInterceptor) {
_dio.interceptors.add(authInterceptor);
}
/// Configure l'URL de base
void setBaseUrl(String baseUrl) {
_dio.options.baseUrl = baseUrl;
}
/// Ajoute un header global
void addHeader(String key, String value) {
_dio.options.headers[key] = value;
}
/// Supprime un header global
void removeHeader(String key) {
_dio.options.headers.remove(key);
}
/// Configure les timeouts
void setTimeout({
Duration? connect,
Duration? receive,
Duration? send,
}) {
if (connect != null) _dio.options.connectTimeout = connect;
if (receive != null) _dio.options.receiveTimeout = receive;
if (send != null) _dio.options.sendTimeout = send;
}
/// Nettoie et ferme le client
void dispose() {
_dio.close();
}
}

View File

@@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
/// Utilitaires pour rendre l'app responsive
class ResponsiveUtils {
static late MediaQueryData _mediaQueryData;
static late double screenWidth;
static late double screenHeight;
static late double blockSizeHorizontal;
static late double blockSizeVertical;
static late double safeAreaHorizontal;
static late double safeAreaVertical;
static late double safeBlockHorizontal;
static late double safeBlockVertical;
static late double textScaleFactor;
static void init(BuildContext context) {
_mediaQueryData = MediaQuery.of(context);
screenWidth = _mediaQueryData.size.width;
screenHeight = _mediaQueryData.size.height;
blockSizeHorizontal = screenWidth / 100;
blockSizeVertical = screenHeight / 100;
final safeAreaPadding = _mediaQueryData.padding;
safeAreaHorizontal = screenWidth - safeAreaPadding.left - safeAreaPadding.right;
safeAreaVertical = screenHeight - safeAreaPadding.top - safeAreaPadding.bottom;
safeBlockHorizontal = safeAreaHorizontal / 100;
safeBlockVertical = safeAreaVertical / 100;
textScaleFactor = _mediaQueryData.textScaleFactor;
}
// Responsive width
static double wp(double percentage) => blockSizeHorizontal * percentage;
// Responsive height
static double hp(double percentage) => blockSizeVertical * percentage;
// Responsive font size (basé sur la largeur)
static double fs(double percentage) => safeBlockHorizontal * percentage;
// Responsive spacing
static double sp(double percentage) => safeBlockHorizontal * percentage;
// Responsive padding/margin
static EdgeInsets paddingAll(double percentage) =>
EdgeInsets.all(sp(percentage));
static EdgeInsets paddingSymmetric({double? horizontal, double? vertical}) =>
EdgeInsets.symmetric(
horizontal: horizontal != null ? sp(horizontal) : 0,
vertical: vertical != null ? hp(vertical) : 0,
);
static EdgeInsets paddingOnly({
double? left,
double? top,
double? right,
double? bottom,
}) =>
EdgeInsets.only(
left: left != null ? sp(left) : 0,
top: top != null ? hp(top) : 0,
right: right != null ? sp(right) : 0,
bottom: bottom != null ? hp(bottom) : 0,
);
// Adaptive values based on screen size
static double adaptive({
required double small, // < 600px (phones)
required double medium, // 600-900px (tablets)
required double large, // > 900px (desktop)
}) {
if (screenWidth < 600) return small;
if (screenWidth < 900) return medium;
return large;
}
// Check device type
static bool get isMobile => screenWidth < 600;
static bool get isTablet => screenWidth >= 600 && screenWidth < 900;
static bool get isDesktop => screenWidth >= 900;
// Responsive border radius
static BorderRadius borderRadius(double percentage) =>
BorderRadius.circular(sp(percentage));
// Responsive icon size
static double iconSize(double percentage) =>
adaptive(
small: sp(percentage),
medium: sp(percentage * 0.9),
large: sp(percentage * 0.8),
);
}
// Extension pour faciliter l'utilisation
extension ResponsiveExtension on num {
// Width percentage
double get wp => ResponsiveUtils.wp(toDouble());
// Height percentage
double get hp => ResponsiveUtils.hp(toDouble());
// Font size
double get fs => ResponsiveUtils.fs(toDouble());
// Spacing
double get sp => ResponsiveUtils.sp(toDouble());
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import 'welcome_screen.dart';
class AuthWrapper extends StatefulWidget {
const AuthWrapper({super.key});
@override
State<AuthWrapper> createState() => _AuthWrapperState();
}
class _AuthWrapperState extends State<AuthWrapper> {
bool _isLoading = true;
bool _isAuthenticated = false;
@override
void initState() {
super.initState();
_checkAuthenticationStatus();
}
Future<void> _checkAuthenticationStatus() async {
// Simulation de vérification d'authentification
// En production : vérifier le token JWT, SharedPreferences, etc.
await Future.delayed(const Duration(milliseconds: 500));
setState(() {
_isLoading = false;
// Pour le moment, toujours false (pas d'utilisateur connecté)
_isAuthenticated = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return _buildLoadingScreen();
}
if (_isAuthenticated) {
// TODO: Retourner vers la navigation principale
return _buildLoadingScreen(); // Temporaire
} else {
return const WelcomeScreen();
}
}
Widget _buildLoadingScreen() {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppTheme.primaryColor,
AppTheme.primaryDark,
],
),
),
child: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
),
);
}
}

View File

@@ -0,0 +1,489 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/custom_text_field.dart';
import '../../../../shared/widgets/loading_button.dart';
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
@override
State<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
}
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
bool _isLoading = false;
bool _emailSent = false;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 100));
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_emailController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: AppTheme.textPrimary),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: SlideTransition(
position: _slideAnimation,
child: _emailSent ? _buildSuccessView() : _buildFormView(),
),
),
);
},
),
),
);
}
Widget _buildFormView() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 40),
_buildInstructions(),
const SizedBox(height: 32),
_buildForm(),
const SizedBox(height: 32),
_buildSendButton(),
const SizedBox(height: 24),
_buildBackToLogin(),
],
);
}
Widget _buildSuccessView() {
return Column(
children: [
const SizedBox(height: 60),
// Icône de succès
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppTheme.successColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(60),
border: Border.all(
color: AppTheme.successColor.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.mark_email_read_rounded,
size: 60,
color: AppTheme.successColor,
),
),
const SizedBox(height: 32),
// Titre de succès
const Text(
'Email envoyé !',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Message de succès
Text(
'Nous avons envoyé un lien de réinitialisation à :',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
_emailController.text,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.primaryColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Instructions
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.infoColor.withOpacity(0.2),
),
),
child: Column(
children: [
const Icon(
Icons.info_outline,
color: AppTheme.infoColor,
size: 24,
),
const SizedBox(height: 12),
const Text(
'Instructions',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(
'1. Vérifiez votre boîte email (et vos spams)\n'
'2. Cliquez sur le lien de réinitialisation\n'
'3. Créez un nouveau mot de passe sécurisé',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
height: 1.5,
),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 32),
// Boutons d'action
Column(
children: [
LoadingButton(
onPressed: _handleResendEmail,
text: 'Renvoyer l\'email',
width: double.infinity,
height: 48,
backgroundColor: AppTheme.secondaryColor,
),
const SizedBox(height: 12),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Retour à la connexion',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icône
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppTheme.warningColor,
borderRadius: BorderRadius.circular(15),
),
child: const Icon(
Icons.lock_reset_rounded,
color: Colors.white,
size: 30,
),
),
const SizedBox(height: 24),
// Titre
const Text(
'Mot de passe oublié ?',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
// Sous-titre
Text(
'Pas de problème ! Nous allons vous aider à le récupérer.',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
),
],
);
}
Widget _buildInstructions() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.primaryColor.withOpacity(0.1),
),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.email_outlined,
color: AppTheme.primaryColor,
size: 20,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Comment ça marche ?',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
'Saisissez votre email et nous vous enverrons un lien sécurisé pour réinitialiser votre mot de passe.',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
height: 1.4,
),
),
],
),
),
],
),
);
}
Widget _buildForm() {
return Form(
key: _formKey,
child: CustomTextField(
controller: _emailController,
label: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.done,
validator: _validateEmail,
onFieldSubmitted: (_) => _handleSendResetEmail(),
autofocus: true,
),
);
}
Widget _buildSendButton() {
return LoadingButton(
onPressed: _handleSendResetEmail,
isLoading: _isLoading,
text: 'Envoyer le lien de réinitialisation',
width: double.infinity,
height: 56,
);
}
Widget _buildBackToLogin() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Vous vous souvenez de votre mot de passe ? ',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text(
'Se connecter',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
);
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre adresse email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez saisir une adresse email valide';
}
return null;
}
Future<void> _handleSendResetEmail() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
// Simulation d'envoi d'email
await Future.delayed(const Duration(seconds: 2));
// Vibration de succès
HapticFeedback.lightImpact();
// Transition vers la vue de succès
setState(() {
_emailSent = true;
_isLoading = false;
});
} catch (e) {
// Gestion d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de l\'envoi: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
setState(() {
_isLoading = false;
});
}
}
}
Future<void> _handleResendEmail() async {
try {
// Simulation de renvoi d'email
await Future.delayed(const Duration(seconds: 1));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Email renvoyé avec succès !'),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors du renvoi: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
}
}
}
}

View File

@@ -0,0 +1,321 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/auth/bloc/auth_bloc.dart';
import '../../../../core/auth/bloc/auth_event.dart';
import '../../../../core/auth/models/auth_state.dart';
import '../../../../core/auth/models/login_request.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/buttons/buttons.dart';
import '../widgets/login_form.dart';
import '../widgets/login_header.dart';
import '../widgets/login_footer.dart';
/// Écran de connexion avec interface sophistiquée
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late AnimationController _shakeController;
late Animation<double> _fadeAnimation;
late Animation<double> _slideAnimation;
late Animation<double> _shakeAnimation;
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
bool _rememberMe = false;
bool _isLoading = false;
@override
void initState() {
super.initState();
_setupAnimations();
_startEntryAnimation();
}
void _setupAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_shakeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
));
_slideAnimation = Tween<double>(
begin: 50.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 0.8, curve: Curves.easeOut),
));
_shakeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _shakeController,
curve: Curves.elasticInOut,
));
}
void _startEntryAnimation() {
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
_animationController.forward();
}
});
}
@override
void dispose() {
_animationController.dispose();
_shakeController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: BlocListener<AuthBloc, AuthState>(
listener: _handleAuthStateChange,
child: SafeArea(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: Transform.translate(
offset: Offset(0, _slideAnimation.value),
child: _buildLoginContent(),
),
);
},
),
),
),
);
}
Widget _buildLoginContent() {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const SizedBox(height: 60),
// Header avec logo et titre
LoginHeader(
onAnimationComplete: () {},
),
const SizedBox(height: 60),
// Formulaire de connexion
AnimatedBuilder(
animation: _shakeAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(
_shakeAnimation.value * 10 *
(1 - _shakeAnimation.value) *
(1 - _shakeAnimation.value),
0,
),
child: LoginForm(
formKey: _formKey,
emailController: _emailController,
passwordController: _passwordController,
obscurePassword: _obscurePassword,
rememberMe: _rememberMe,
isLoading: _isLoading,
onObscureToggle: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
HapticFeedback.selectionClick();
},
onRememberMeToggle: (value) {
setState(() {
_rememberMe = value;
});
HapticFeedback.selectionClick();
},
onSubmit: _handleLogin,
),
),
),
),
const SizedBox(height: 40),
// Footer avec liens et informations
const LoginFooter(),
const SizedBox(height: 20),
],
),
);
}
void _handleAuthStateChange(BuildContext context, AuthState state) {
setState(() {
_isLoading = state.isLoading;
});
if (state.status == AuthStatus.authenticated) {
// Connexion réussie - navigation gérée par l'app principal
_showSuccessMessage();
HapticFeedback.heavyImpact();
} else if (state.status == AuthStatus.error) {
// Erreur de connexion
_handleLoginError(state.errorMessage ?? 'Erreur inconnue');
} else if (state.status == AuthStatus.unauthenticated && state.errorMessage != null) {
// Échec de connexion
_handleLoginError(state.errorMessage!);
}
}
void _handleLogin() {
if (!_formKey.currentState!.validate()) {
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
return;
}
final email = _emailController.text.trim();
final password = _passwordController.text;
if (email.isEmpty || password.isEmpty) {
_showErrorMessage('Veuillez remplir tous les champs');
_triggerShakeAnimation();
return;
}
// Déclencher la connexion
final loginRequest = LoginRequest(
email: email,
password: password,
rememberMe: _rememberMe,
);
context.read<AuthBloc>().add(AuthLoginRequested(loginRequest));
// Feedback haptique
HapticFeedback.lightImpact();
}
void _handleLoginError(String errorMessage) {
_showErrorMessage(errorMessage);
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
// Effacer l'erreur après affichage
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
context.read<AuthBloc>().add(const AuthErrorCleared());
}
});
}
void _triggerShakeAnimation() {
_shakeController.reset();
_shakeController.forward();
}
void _showSuccessMessage() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
Icons.check_circle,
color: Colors.white,
size: 24,
),
const SizedBox(width: 12),
const Text(
'Connexion réussie !',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
],
),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16),
duration: const Duration(seconds: 2),
),
);
}
void _showErrorMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(
Icons.error_outline,
color: Colors.white,
size: 24,
),
const SizedBox(width: 12),
Expanded(
child: Text(
message,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(16),
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: 'OK',
textColor: Colors.white,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
),
),
);
}
}

View File

@@ -0,0 +1,478 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/auth/bloc/temp_auth_bloc.dart';
import '../../../../core/auth/bloc/auth_event.dart';
import '../../../../core/auth/models/auth_state.dart';
import '../../../../core/auth/models/login_request.dart';
import '../../../../shared/theme/app_theme.dart';
import '../widgets/login_header.dart';
import '../widgets/login_footer.dart';
/// Écran de connexion temporaire simplifié
class TempLoginPage extends StatefulWidget {
const TempLoginPage({super.key});
@override
State<TempLoginPage> createState() => _TempLoginPageState();
}
class _TempLoginPageState extends State<TempLoginPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late AnimationController _shakeController;
late Animation<double> _fadeAnimation;
late Animation<double> _slideAnimation;
late Animation<double> _shakeAnimation;
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController(text: 'admin@unionflow.dev');
final _passwordController = TextEditingController(text: 'admin123');
bool _obscurePassword = true;
bool _rememberMe = false;
bool _isLoading = false;
@override
void initState() {
super.initState();
_setupAnimations();
_startEntryAnimation();
}
void _setupAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_shakeController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
));
_slideAnimation = Tween<double>(
begin: 50.0,
end: 0.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 0.8, curve: Curves.easeOut),
));
_shakeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _shakeController,
curve: Curves.elasticInOut,
));
}
void _startEntryAnimation() {
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
_animationController.forward();
}
});
}
@override
void dispose() {
_animationController.dispose();
_shakeController.dispose();
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
body: BlocListener<TempAuthBloc, AuthState>(
listener: _handleAuthStateChange,
child: SafeArea(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: Transform.translate(
offset: Offset(0, _slideAnimation.value),
child: _buildLoginContent(),
),
);
},
),
),
),
);
}
Widget _buildLoginContent() {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const SizedBox(height: 60),
// Header avec logo et titre
const LoginHeader(),
const SizedBox(height: 60),
// Formulaire de connexion
AnimatedBuilder(
animation: _shakeAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(
_shakeAnimation.value * 10 *
(1 - _shakeAnimation.value) *
(1 - _shakeAnimation.value),
0,
),
child: _buildLoginForm(),
);
},
),
const SizedBox(height: 40),
// Footer avec liens et informations
const LoginFooter(),
const SizedBox(height: 20),
],
),
);
}
Widget _buildLoginForm() {
return Form(
key: _formKey,
child: Column(
children: [
// Champ email
_buildEmailField(),
const SizedBox(height: 20),
// Champ mot de passe
_buildPasswordField(),
const SizedBox(height: 16),
// Options
_buildOptionsRow(),
const SizedBox(height: 32),
// Bouton de connexion
_buildLoginButton(),
],
),
);
}
Widget _buildEmailField() {
return TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: Icon(
Icons.email_outlined,
color: AppTheme.primaryColor,
),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.primaryColor,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre email';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Format d\'email invalide';
}
return null;
},
);
}
Widget _buildPasswordField() {
return TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
enabled: !_isLoading,
onFieldSubmitted: (_) => _handleLogin(),
decoration: InputDecoration(
labelText: 'Mot de passe',
hintText: 'Saisissez votre mot de passe',
prefixIcon: Icon(
Icons.lock_outlined,
color: AppTheme.primaryColor,
),
suffixIcon: IconButton(
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
HapticFeedback.selectionClick();
},
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: AppTheme.primaryColor,
),
),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(
color: AppTheme.primaryColor,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
},
);
}
Widget _buildOptionsRow() {
return Row(
children: [
// Se souvenir de moi
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_rememberMe = !_rememberMe;
});
HapticFeedback.selectionClick();
},
child: Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: _rememberMe
? AppTheme.primaryColor
: AppTheme.textSecondary,
width: 2,
),
color: _rememberMe
? AppTheme.primaryColor
: Colors.transparent,
),
child: _rememberMe
? const Icon(
Icons.check,
size: 14,
color: Colors.white,
)
: null,
),
const SizedBox(width: 8),
Text(
'Se souvenir de moi',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
// Compte de test
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.infoColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Compte de test',
style: TextStyle(
fontSize: 12,
color: AppTheme.infoColor,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
Widget _buildLoginButton() {
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 4,
),
child: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.login, size: 20),
const SizedBox(width: 8),
const Text(
'Se connecter',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
void _handleAuthStateChange(BuildContext context, AuthState state) {
setState(() {
_isLoading = state.isLoading;
});
if (state.status == AuthStatus.authenticated) {
_showSuccessMessage();
HapticFeedback.heavyImpact();
} else if (state.status == AuthStatus.error) {
_handleLoginError(state.errorMessage ?? 'Erreur inconnue');
}
}
void _handleLogin() {
if (!_formKey.currentState!.validate()) {
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
return;
}
final email = _emailController.text.trim();
final password = _passwordController.text;
final loginRequest = LoginRequest(
email: email,
password: password,
rememberMe: _rememberMe,
);
context.read<TempAuthBloc>().add(AuthLoginRequested(loginRequest));
HapticFeedback.lightImpact();
}
void _handleLoginError(String errorMessage) {
_showErrorMessage(errorMessage);
_triggerShakeAnimation();
HapticFeedback.mediumImpact();
}
void _triggerShakeAnimation() {
_shakeController.reset();
_shakeController.forward();
}
void _showSuccessMessage() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Row(
children: [
Icon(Icons.check_circle, color: Colors.white),
SizedBox(width: 12),
Text('Connexion réussie !'),
],
),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
}
void _showErrorMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error_outline, color: Colors.white),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
}
}

View File

@@ -0,0 +1,517 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/custom_text_field.dart';
import '../../../../shared/widgets/loading_button.dart';
import '../../../navigation/presentation/pages/main_navigation.dart';
import 'forgot_password_screen.dart';
import 'register_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
bool _isLoading = false;
bool _obscurePassword = true;
bool _rememberMe = false;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 100));
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: AppTheme.textPrimary),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 40),
_buildLoginForm(),
const SizedBox(height: 24),
_buildForgotPassword(),
const SizedBox(height: 32),
_buildLoginButton(),
const SizedBox(height: 24),
_buildDivider(),
const SizedBox(height: 24),
_buildSocialLogin(),
const SizedBox(height: 32),
_buildSignUpLink(),
],
),
),
),
);
},
),
),
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Logo petit
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(15),
),
child: const Icon(
Icons.groups_rounded,
color: Colors.white,
size: 30,
),
),
const SizedBox(height: 24),
// Titre
const Text(
'Bienvenue !',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
// Sous-titre
Text(
'Connectez-vous à votre compte UnionFlow',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
),
],
);
}
Widget _buildLoginForm() {
return Form(
key: _formKey,
child: Column(
children: [
// Champ Email
CustomTextField(
controller: _emailController,
label: 'Adresse email',
hintText: 'votre.email@exemple.com',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: _validateEmail,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
const SizedBox(height: 16),
// Champ Mot de passe
CustomTextField(
controller: _passwordController,
label: 'Mot de passe',
hintText: 'Votre mot de passe',
prefixIcon: Icons.lock_outline,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
validator: _validatePassword,
onFieldSubmitted: (_) => _handleLogin(),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
color: AppTheme.textHint,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
const SizedBox(height: 16),
// Remember me
Row(
children: [
Checkbox(
value: _rememberMe,
onChanged: (value) {
setState(() {
_rememberMe = value ?? false;
});
},
activeColor: AppTheme.primaryColor,
),
const Text(
'Se souvenir de moi',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
],
),
],
),
);
}
Widget _buildForgotPassword() {
return Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => _navigateToForgotPassword(),
child: const Text(
'Mot de passe oublié ?',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
);
}
Widget _buildLoginButton() {
return LoadingButton(
onPressed: _handleLogin,
isLoading: _isLoading,
text: 'Se connecter',
width: double.infinity,
height: 56,
);
}
Widget _buildDivider() {
return Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'ou',
style: TextStyle(
color: AppTheme.textHint,
fontSize: 14,
),
),
),
const Expanded(child: Divider()),
],
);
}
Widget _buildSocialLogin() {
return Column(
children: [
// Google Login
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton.icon(
onPressed: () => _handleGoogleLogin(),
icon: Image.asset(
'assets/icons/google.png',
width: 20,
height: 20,
errorBuilder: (context, error, stackTrace) => const Icon(
Icons.g_mobiledata,
color: Colors.red,
size: 20,
),
),
label: const Text('Continuer avec Google'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textPrimary,
side: const BorderSide(color: AppTheme.borderColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(height: 12),
// Microsoft Login
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton.icon(
onPressed: () => _handleMicrosoftLogin(),
icon: const Icon(
Icons.business,
color: Color(0xFF00A4EF),
size: 20,
),
label: const Text('Continuer avec Microsoft'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textPrimary,
side: const BorderSide(color: AppTheme.borderColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
);
}
Widget _buildSignUpLink() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Pas encore de compte ? ',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
TextButton(
onPressed: () => _navigateToRegister(),
child: const Text(
'S\'inscrire',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
);
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre adresse email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez saisir une adresse email valide';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
}
Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
// Simulation d'authentification
await Future.delayed(const Duration(seconds: 2));
// Vibration de succès
HapticFeedback.lightImpact();
// Navigation vers le dashboard
if (mounted) {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const MainNavigation(),
transitionDuration: const Duration(milliseconds: 600),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
),
);
},
),
);
}
} catch (e) {
// Gestion d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur de connexion: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
void _handleGoogleLogin() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Connexion Google - En cours de développement'),
backgroundColor: AppTheme.infoColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _handleMicrosoftLogin() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Connexion Microsoft - En cours de développement'),
backgroundColor: AppTheme.infoColor,
behavior: SnackBarBehavior.floating,
),
);
}
void _navigateToForgotPassword() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const ForgotPasswordScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
void _navigateToRegister() {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const RegisterScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
}

View File

@@ -0,0 +1,624 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../shared/theme/app_theme.dart';
import '../../../../shared/widgets/custom_text_field.dart';
import '../../../../shared/widgets/loading_button.dart';
import 'login_screen.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
bool _isLoading = false;
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
bool _acceptTerms = false;
bool _acceptNewsletter = false;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 100));
_fadeController.forward();
_slideController.forward();
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.backgroundLight,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: AppTheme.textPrimary),
onPressed: () => Navigator.of(context).pop(),
),
),
body: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: SlideTransition(
position: _slideAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 32),
_buildRegistrationForm(),
const SizedBox(height: 24),
_buildTermsAndConditions(),
const SizedBox(height: 32),
_buildRegisterButton(),
const SizedBox(height: 24),
_buildLoginLink(),
],
),
),
),
);
},
),
),
);
}
Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Logo petit
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppTheme.primaryColor,
borderRadius: BorderRadius.circular(15),
),
child: const Icon(
Icons.person_add_rounded,
color: Colors.white,
size: 30,
),
),
const SizedBox(height: 24),
// Titre
const Text(
'Créer un compte',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
// Sous-titre
Text(
'Rejoignez UnionFlow et gérez votre association',
style: TextStyle(
fontSize: 16,
color: AppTheme.textSecondary,
),
),
],
);
}
Widget _buildRegistrationForm() {
return Form(
key: _formKey,
child: Column(
children: [
// Nom et Prénom
Row(
children: [
Expanded(
child: CustomTextField(
controller: _firstNameController,
label: 'Prénom',
hintText: 'Jean',
prefixIcon: Icons.person_outline,
textInputAction: TextInputAction.next,
validator: _validateFirstName,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
),
const SizedBox(width: 16),
Expanded(
child: CustomTextField(
controller: _lastNameController,
label: 'Nom',
hintText: 'Dupont',
prefixIcon: Icons.person_outline,
textInputAction: TextInputAction.next,
validator: _validateLastName,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
),
],
),
const SizedBox(height: 16),
// Email
CustomTextField(
controller: _emailController,
label: 'Adresse email',
hintText: 'jean.dupont@exemple.com',
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
validator: _validateEmail,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
),
const SizedBox(height: 16),
// Mot de passe
CustomTextField(
controller: _passwordController,
label: 'Mot de passe',
hintText: 'Minimum 8 caractères',
prefixIcon: Icons.lock_outline,
obscureText: _obscurePassword,
textInputAction: TextInputAction.next,
validator: _validatePassword,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
color: AppTheme.textHint,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
const SizedBox(height: 16),
// Confirmer mot de passe
CustomTextField(
controller: _confirmPasswordController,
label: 'Confirmer le mot de passe',
hintText: 'Retapez votre mot de passe',
prefixIcon: Icons.lock_outline,
obscureText: _obscureConfirmPassword,
textInputAction: TextInputAction.done,
validator: _validateConfirmPassword,
onFieldSubmitted: (_) => _handleRegister(),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword ? Icons.visibility_off : Icons.visibility,
color: AppTheme.textHint,
),
onPressed: () {
setState(() {
_obscureConfirmPassword = !_obscureConfirmPassword;
});
},
),
),
const SizedBox(height: 16),
// Indicateur de force du mot de passe
_buildPasswordStrengthIndicator(),
],
),
);
}
Widget _buildPasswordStrengthIndicator() {
final password = _passwordController.text;
final strength = _calculatePasswordStrength(password);
Color strengthColor;
String strengthText;
if (strength < 0.3) {
strengthColor = AppTheme.errorColor;
strengthText = 'Faible';
} else if (strength < 0.7) {
strengthColor = AppTheme.warningColor;
strengthText = 'Moyen';
} else {
strengthColor = AppTheme.successColor;
strengthText = 'Fort';
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Force du mot de passe',
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
if (password.isNotEmpty)
Text(
strengthText,
style: TextStyle(
fontSize: 12,
color: strengthColor,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 8),
Container(
height: 4,
decoration: BoxDecoration(
color: AppTheme.borderColor,
borderRadius: BorderRadius.circular(2),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: password.isEmpty ? 0 : strength,
child: Container(
decoration: BoxDecoration(
color: strengthColor,
borderRadius: BorderRadius.circular(2),
),
),
),
),
],
);
}
double _calculatePasswordStrength(String password) {
if (password.isEmpty) return 0.0;
double strength = 0.0;
// Longueur
if (password.length >= 8) strength += 0.25;
if (password.length >= 12) strength += 0.25;
// Contient des minuscules
if (password.contains(RegExp(r'[a-z]'))) strength += 0.15;
// Contient des majuscules
if (password.contains(RegExp(r'[A-Z]'))) strength += 0.15;
// Contient des chiffres
if (password.contains(RegExp(r'[0-9]'))) strength += 0.1;
// Contient des caractères spéciaux
if (password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) strength += 0.1;
return strength.clamp(0.0, 1.0);
}
Widget _buildTermsAndConditions() {
return Column(
children: [
// Accepter les conditions
Row(
children: [
Checkbox(
value: _acceptTerms,
onChanged: (value) {
setState(() {
_acceptTerms = value ?? false;
});
},
activeColor: AppTheme.primaryColor,
),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
children: [
const TextSpan(text: 'J\'accepte les '),
TextSpan(
text: 'Conditions d\'utilisation',
style: const TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
),
),
const TextSpan(text: ' et la '),
TextSpan(
text: 'Politique de confidentialité',
style: const TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
),
),
],
),
),
),
],
),
// Newsletter (optionnel)
Row(
children: [
Checkbox(
value: _acceptNewsletter,
onChanged: (value) {
setState(() {
_acceptNewsletter = value ?? false;
});
},
activeColor: AppTheme.primaryColor,
),
const Expanded(
child: Text(
'Je souhaite recevoir des actualités et conseils par email (optionnel)',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
),
],
),
],
);
}
Widget _buildRegisterButton() {
return LoadingButton(
onPressed: _acceptTerms ? _handleRegister : null,
isLoading: _isLoading,
text: 'Créer mon compte',
width: double.infinity,
height: 56,
enabled: _acceptTerms,
);
}
Widget _buildLoginLink() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Déjà un compte ? ',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
TextButton(
onPressed: () => _navigateToLogin(),
child: const Text(
'Se connecter',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
],
);
}
String? _validateFirstName(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre prénom';
}
if (value.length < 2) {
return 'Le prénom doit contenir au moins 2 caractères';
}
return null;
}
String? _validateLastName(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre nom';
}
if (value.length < 2) {
return 'Le nom doit contenir au moins 2 caractères';
}
return null;
}
String? _validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir votre adresse email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez saisir une adresse email valide';
}
return null;
}
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez saisir un mot de passe';
}
if (value.length < 8) {
return 'Le mot de passe doit contenir au moins 8 caractères';
}
if (!value.contains(RegExp(r'[A-Z]'))) {
return 'Le mot de passe doit contenir au moins une majuscule';
}
if (!value.contains(RegExp(r'[a-z]'))) {
return 'Le mot de passe doit contenir au moins une minuscule';
}
if (!value.contains(RegExp(r'[0-9]'))) {
return 'Le mot de passe doit contenir au moins un chiffre';
}
return null;
}
String? _validateConfirmPassword(String? value) {
if (value == null || value.isEmpty) {
return 'Veuillez confirmer votre mot de passe';
}
if (value != _passwordController.text) {
return 'Les mots de passe ne correspondent pas';
}
return null;
}
Future<void> _handleRegister() async {
if (!_formKey.currentState!.validate()) {
return;
}
if (!_acceptTerms) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez accepter les conditions d\'utilisation'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
return;
}
setState(() {
_isLoading = true;
});
try {
// Simulation d'inscription
await Future.delayed(const Duration(seconds: 2));
// Vibration de succès
HapticFeedback.lightImpact();
// Afficher message de succès
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Compte créé avec succès ! Vérifiez votre email.'),
backgroundColor: AppTheme.successColor,
behavior: SnackBarBehavior.floating,
),
);
// Navigation vers l'écran de connexion
_navigateToLogin();
}
} catch (e) {
// Gestion d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la création du compte: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
void _navigateToLogin() {
Navigator.of(context).pushReplacement(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const LoginScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(-1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
}

View File

@@ -0,0 +1,400 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
import 'login_screen.dart';
import 'register_screen.dart';
class WelcomeScreen extends StatefulWidget {
const WelcomeScreen({super.key});
@override
State<WelcomeScreen> createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_initializeAnimations();
_startAnimations();
}
void _initializeAnimations() {
_fadeController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.5),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
}
void _startAnimations() async {
await Future.delayed(const Duration(milliseconds: 200));
_fadeController.forward();
await Future.delayed(const Duration(milliseconds: 300));
_slideController.forward();
}
@override
void dispose() {
_fadeController.dispose();
_slideController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.primaryColor,
AppTheme.primaryDark,
const Color(0xFF0D47A1),
],
),
),
child: SafeArea(
child: AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
// Header avec logo
Expanded(
flex: 3,
child: SlideTransition(
position: _slideAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo principal
Container(
width: 140,
height: 140,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(35),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 25,
offset: const Offset(0, 12),
),
],
),
child: const Icon(
Icons.groups_rounded,
size: 70,
color: AppTheme.primaryColor,
),
),
const SizedBox(height: 32),
// Titre principal
const Text(
'UnionFlow',
style: TextStyle(
fontSize: 42,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1.5,
),
),
const SizedBox(height: 16),
// Sous-titre
Text(
'Gestion moderne d\'associations\net de mutuelles',
style: TextStyle(
fontSize: 18,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w300,
height: 1.4,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Points forts
_buildFeatureHighlights(),
],
),
),
),
// Boutons d'action
Expanded(
flex: 2,
child: SlideTransition(
position: _slideAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Bouton Connexion
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: () => _navigateToLogin(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: AppTheme.primaryColor,
elevation: 8,
shadowColor: Colors.black.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.login, size: 20),
SizedBox(width: 8),
Text(
'Se connecter',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
const SizedBox(height: 16),
// Bouton Inscription
SizedBox(
width: double.infinity,
height: 56,
child: OutlinedButton(
onPressed: () => _navigateToRegister(),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: const BorderSide(
color: Colors.white,
width: 2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.person_add, size: 20),
SizedBox(width: 8),
Text(
'Créer un compte',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
const SizedBox(height: 24),
// Lien mode démo
TextButton(
onPressed: () => _navigateToDemo(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.visibility,
size: 16,
color: Colors.white.withOpacity(0.8),
),
const SizedBox(width: 6),
Text(
'Découvrir en mode démo',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
),
// Footer
Padding(
padding: const EdgeInsets.only(top: 20),
child: Column(
children: [
Text(
'Version 1.0.0 • Sécurisé et confidentiel',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
),
const SizedBox(height: 8),
Text(
'© 2024 Lions Club International',
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 10,
),
),
],
),
),
],
),
),
);
},
),
),
),
);
}
Widget _buildFeatureHighlights() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildFeatureItem(Icons.security, 'Sécurisé'),
_buildFeatureItem(Icons.analytics, 'Analytique'),
_buildFeatureItem(Icons.cloud_sync, 'Synchronisé'),
],
),
);
}
Widget _buildFeatureItem(IconData icon, String label) {
return Column(
children: [
Icon(
icon,
color: Colors.white,
size: 20,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
);
}
void _navigateToLogin() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const LoginScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
void _navigateToRegister() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const RegisterScreen(),
transitionDuration: const Duration(milliseconds: 400),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
),
);
}
void _navigateToDemo() {
// TODO: Implémenter la navigation vers le mode démo
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Mode démo - En cours de développement'),
backgroundColor: AppTheme.primaryColor,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
}

View File

@@ -0,0 +1,362 @@
import 'package:flutter/material.dart';
import '../../../../shared/theme/app_theme.dart';
/// Pied de page de la connexion avec informations et liens
class LoginFooter extends StatelessWidget {
const LoginFooter({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// Séparateur
_buildDivider(),
const SizedBox(height: 24),
// Informations sur l'application
_buildAppInfo(),
const SizedBox(height: 20),
// Liens utiles
_buildUsefulLinks(context),
const SizedBox(height: 20),
// Version et copyright
_buildVersionInfo(),
],
);
}
Widget _buildDivider() {
return Row(
children: [
Expanded(
child: Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.transparent,
AppTheme.textSecondary.withOpacity(0.3),
Colors.transparent,
],
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Icon(
Icons.star,
size: 16,
color: AppTheme.textSecondary.withOpacity(0.5),
),
),
Expanded(
child: Container(
height: 1,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.transparent,
AppTheme.textSecondary.withOpacity(0.3),
Colors.transparent,
],
),
),
),
),
],
);
}
Widget _buildAppInfo() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.backgroundLight,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.textSecondary.withOpacity(0.1),
),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.security,
size: 20,
color: AppTheme.successColor,
),
const SizedBox(width: 8),
Text(
'Connexion sécurisée',
style: TextStyle(
fontSize: 14,
color: AppTheme.successColor,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 8),
Text(
'Vos données sont protégées par un cryptage de niveau bancaire',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
);
}
Widget _buildUsefulLinks(BuildContext context) {
return Wrap(
spacing: 20,
runSpacing: 12,
alignment: WrapAlignment.center,
children: [
_buildLinkButton(
icon: Icons.help_outline,
label: 'Aide',
onTap: () => _showHelpDialog(context),
),
_buildLinkButton(
icon: Icons.info_outline,
label: 'À propos',
onTap: () => _showAboutDialog(context),
),
_buildLinkButton(
icon: Icons.privacy_tip_outlined,
label: 'Confidentialité',
onTap: () => _showPrivacyDialog(context),
),
],
);
}
Widget _buildLinkButton({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.textSecondary.withOpacity(0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color: AppTheme.textSecondary,
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
Widget _buildVersionInfo() {
return Column(
children: [
Text(
'UnionFlow Mobile v1.0.0',
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary.withOpacity(0.7),
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'© 2025 Lions Dev Team. Tous droits réservés.',
style: TextStyle(
fontSize: 10,
color: AppTheme.textSecondary.withOpacity(0.5),
),
),
],
);
}
void _showHelpDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(
Icons.help_outline,
color: AppTheme.infoColor,
),
const SizedBox(width: 12),
const Text('Aide'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHelpItem(
'Connexion',
'Utilisez votre email et mot de passe fournis par votre association.',
),
const SizedBox(height: 12),
_buildHelpItem(
'Mot de passe oublié',
'Contactez votre administrateur pour réinitialiser votre mot de passe.',
),
const SizedBox(height: 12),
_buildHelpItem(
'Problèmes techniques',
'Vérifiez votre connexion internet et réessayez.',
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Fermer',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
void _showAboutDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(
Icons.info_outline,
color: AppTheme.primaryColor,
),
const SizedBox(width: 12),
const Text('À propos'),
],
),
content: const Text(
'UnionFlow est une solution complète de gestion d\'associations développée par Lions Dev Team.\n\n'
'Cette application mobile vous permet de gérer vos membres, cotisations, événements et bien plus encore, où que vous soyez.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Fermer',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
void _showPrivacyDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Icon(
Icons.privacy_tip_outlined,
color: AppTheme.warningColor,
),
const SizedBox(width: 12),
const Text('Confidentialité'),
],
),
content: const Text(
'Nous respectons votre vie privée. Toutes vos données sont stockées de manière sécurisée et ne sont jamais partagées avec des tiers.\n\n'
'Les données sont chiffrées en transit et au repos selon les standards de sécurité les plus élevés.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Compris',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
Widget _buildHelpItem(String title, String description) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
description,
style: TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
);
}
}

Some files were not shown because too many files have changed in this diff Show More