Initial commit: GBCM Mobile App React Native with authentication and services
This commit is contained in:
112
.gitignore
vendored
Normal file
112
.gitignore
vendored
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# React Native
|
||||||
|
|
||||||
|
# OSX
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Xcode
|
||||||
|
build/
|
||||||
|
*.pbxuser
|
||||||
|
!default.pbxuser
|
||||||
|
*.mode1v3
|
||||||
|
!default.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
!default.mode2v3
|
||||||
|
*.perspectivev3
|
||||||
|
!default.perspectivev3
|
||||||
|
xcuserdata
|
||||||
|
*.xccheckout
|
||||||
|
*.moved-aside
|
||||||
|
DerivedData
|
||||||
|
*.hmap
|
||||||
|
*.ipa
|
||||||
|
*.xcuserstate
|
||||||
|
project.xcworkspace
|
||||||
|
|
||||||
|
# Android/IntelliJ
|
||||||
|
build/
|
||||||
|
.idea
|
||||||
|
.gradle
|
||||||
|
local.properties
|
||||||
|
*.iml
|
||||||
|
*.hprof
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# node.js
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# BUCK
|
||||||
|
buck-out/
|
||||||
|
\.buckd/
|
||||||
|
*.keystore
|
||||||
|
!debug.keystore
|
||||||
|
|
||||||
|
# Bundle artifacts
|
||||||
|
*.jsbundle
|
||||||
|
|
||||||
|
# CocoaPods
|
||||||
|
/ios/Pods/
|
||||||
|
|
||||||
|
# Expo
|
||||||
|
.expo/
|
||||||
|
web-build/
|
||||||
|
|
||||||
|
# Flipper
|
||||||
|
ios/Flipper-Folly
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Metro
|
||||||
|
.metro-health-check*
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Detox
|
||||||
|
e2e/artifacts/
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Watchman
|
||||||
|
.watchmanconfig
|
||||||
|
|
||||||
|
# React Native CLI
|
||||||
|
.react-native-cli.config.js
|
||||||
|
|
||||||
|
# Fastlane
|
||||||
|
ios/fastlane/report.xml
|
||||||
|
ios/fastlane/Preview.html
|
||||||
|
ios/fastlane/screenshots
|
||||||
|
ios/fastlane/test_output
|
||||||
|
android/fastlane/report.xml
|
||||||
|
android/fastlane/Preview.html
|
||||||
|
android/fastlane/screenshots
|
||||||
|
android/fastlane/test_output
|
||||||
|
|
||||||
|
# Bundle artifact
|
||||||
|
*.jsbundle
|
||||||
|
|
||||||
|
# Ruby / CocoaPods
|
||||||
|
/ios/Pods/
|
||||||
|
/vendor/bundle/
|
||||||
|
|
||||||
|
# Temporary files created by Metro to check the health of the file watcher
|
||||||
|
.metro-health-check*
|
||||||
283
README.md
Normal file
283
README.md
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
# GBCM Mobile App
|
||||||
|
|
||||||
|
Application mobile pour la plateforme GBCM (Global Business Consulting and Management) développée avec React Native.
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Application mobile native iOS et Android permettant aux utilisateurs GBCM d'accéder à leurs services de coaching, ateliers et tableaux de bord depuis leurs appareils mobiles.
|
||||||
|
|
||||||
|
## Technologies
|
||||||
|
|
||||||
|
- **React Native 0.72.6** - Framework mobile cross-platform
|
||||||
|
- **React 18.2.0** - Bibliothèque UI
|
||||||
|
- **React Navigation 6** - Navigation
|
||||||
|
- **Redux Toolkit** - Gestion d'état
|
||||||
|
- **React Native Paper** - Composants Material Design
|
||||||
|
- **React Hook Form** - Gestion des formulaires
|
||||||
|
- **Axios** - Client HTTP
|
||||||
|
- **React Native Keychain** - Stockage sécurisé
|
||||||
|
- **React Native Chart Kit** - Graphiques
|
||||||
|
- **React Native Calendars** - Calendrier
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
### Environnement de développement
|
||||||
|
- Node.js 16+
|
||||||
|
- npm ou yarn
|
||||||
|
- React Native CLI
|
||||||
|
- Java 11+ (Android)
|
||||||
|
- Xcode 14+ (iOS)
|
||||||
|
- Android Studio (Android)
|
||||||
|
|
||||||
|
### Appareils/Simulateurs
|
||||||
|
- iOS Simulator (macOS uniquement)
|
||||||
|
- Android Emulator
|
||||||
|
- Appareils physiques iOS/Android
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Cloner le repository
|
||||||
|
```bash
|
||||||
|
git clone https://git.lions.dev/gbcm/gbcm-mobile-app.git
|
||||||
|
cd gbcm-mobile-app
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Installer les dépendances
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
# ou
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Installation des pods iOS (macOS uniquement)
|
||||||
|
```bash
|
||||||
|
cd ios && pod install && cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Configuration
|
||||||
|
Copier `.env.example` vers `.env` et configurer :
|
||||||
|
```
|
||||||
|
API_BASE_URL=https://api.gbcm.com/v1
|
||||||
|
API_TIMEOUT=10000
|
||||||
|
ENVIRONMENT=development
|
||||||
|
```
|
||||||
|
|
||||||
|
## Développement
|
||||||
|
|
||||||
|
### Démarrage du Metro bundler
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
# ou
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lancement sur iOS
|
||||||
|
```bash
|
||||||
|
npm run ios
|
||||||
|
# ou
|
||||||
|
yarn ios
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lancement sur Android
|
||||||
|
```bash
|
||||||
|
npm run android
|
||||||
|
# ou
|
||||||
|
yarn android
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
# ou
|
||||||
|
yarn test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure du projet
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/ # Composants réutilisables
|
||||||
|
│ ├── common/ # Composants communs
|
||||||
|
│ ├── forms/ # Composants de formulaires
|
||||||
|
│ └── charts/ # Graphiques
|
||||||
|
├── screens/ # Écrans de l'application
|
||||||
|
│ ├── auth/ # Authentification
|
||||||
|
│ ├── dashboard/ # Tableaux de bord
|
||||||
|
│ ├── coaching/ # Sessions de coaching
|
||||||
|
│ ├── workshops/ # Ateliers
|
||||||
|
│ └── profile/ # Profil utilisateur
|
||||||
|
├── navigation/ # Configuration navigation
|
||||||
|
├── services/ # Services API
|
||||||
|
├── store/ # Gestion d'état Redux
|
||||||
|
├── utils/ # Utilitaires
|
||||||
|
└── assets/ # Ressources statiques
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fonctionnalités
|
||||||
|
|
||||||
|
### Authentification
|
||||||
|
- Connexion/déconnexion sécurisée
|
||||||
|
- Authentification biométrique (Touch ID/Face ID)
|
||||||
|
- Stockage sécurisé des tokens
|
||||||
|
- Gestion des sessions
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
- Vue d'ensemble personnalisée
|
||||||
|
- Métriques et KPIs
|
||||||
|
- Notifications push
|
||||||
|
- Accès rapide aux services
|
||||||
|
|
||||||
|
### Coaching
|
||||||
|
- Liste des sessions programmées
|
||||||
|
- Détails des sessions
|
||||||
|
- Historique des sessions
|
||||||
|
- Évaluation des sessions
|
||||||
|
|
||||||
|
### Ateliers
|
||||||
|
- Catalogue des ateliers disponibles
|
||||||
|
- Inscription aux ateliers
|
||||||
|
- Calendrier des événements
|
||||||
|
- Matériel de formation
|
||||||
|
|
||||||
|
### Profil
|
||||||
|
- Informations personnelles
|
||||||
|
- Paramètres de l'application
|
||||||
|
- Préférences de notification
|
||||||
|
- Support et aide
|
||||||
|
|
||||||
|
## Services API
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
```javascript
|
||||||
|
// src/services/api.js
|
||||||
|
const API_BASE_URL = 'https://api.gbcm.com/v1';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Services disponibles
|
||||||
|
- `authService` - Authentification
|
||||||
|
- `userService` - Gestion utilisateur
|
||||||
|
- `coachingService` - Services de coaching
|
||||||
|
- `workshopService` - Gestion des ateliers
|
||||||
|
- `notificationService` - Notifications
|
||||||
|
|
||||||
|
## Gestion d'état
|
||||||
|
|
||||||
|
### Redux Store
|
||||||
|
```javascript
|
||||||
|
// Structure du store
|
||||||
|
{
|
||||||
|
auth: {
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
loading: false
|
||||||
|
},
|
||||||
|
coaching: {
|
||||||
|
sessions: [],
|
||||||
|
currentSession: null
|
||||||
|
},
|
||||||
|
workshops: {
|
||||||
|
available: [],
|
||||||
|
registered: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
### Structure de navigation
|
||||||
|
- **Auth Stack** - Écrans d'authentification
|
||||||
|
- **Main Tab Navigator** - Navigation principale
|
||||||
|
- Dashboard
|
||||||
|
- Coaching
|
||||||
|
- Ateliers
|
||||||
|
- Profil
|
||||||
|
- **Modal Stack** - Écrans modaux
|
||||||
|
|
||||||
|
## Build et déploiement
|
||||||
|
|
||||||
|
### Build Android
|
||||||
|
```bash
|
||||||
|
cd android
|
||||||
|
./gradlew assembleRelease
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build iOS
|
||||||
|
```bash
|
||||||
|
cd ios
|
||||||
|
xcodebuild -workspace GBCMMobile.xcworkspace \
|
||||||
|
-scheme GBCMMobile \
|
||||||
|
-configuration Release \
|
||||||
|
-destination generic/platform=iOS \
|
||||||
|
-archivePath GBCMMobile.xcarchive archive
|
||||||
|
```
|
||||||
|
|
||||||
|
### Distribution
|
||||||
|
- **Android**: Google Play Store
|
||||||
|
- **iOS**: Apple App Store
|
||||||
|
- **Enterprise**: Distribution interne
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### Tests unitaires
|
||||||
|
```bash
|
||||||
|
npm run test:unit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests d'intégration
|
||||||
|
```bash
|
||||||
|
npm run test:integration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests E2E avec Detox
|
||||||
|
```bash
|
||||||
|
npm run test:e2e:ios
|
||||||
|
npm run test:e2e:android
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
### Optimisations
|
||||||
|
- Lazy loading des écrans
|
||||||
|
- Mise en cache des images
|
||||||
|
- Optimisation des re-renders
|
||||||
|
- Bundle splitting
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
- Crash reporting (Crashlytics)
|
||||||
|
- Performance monitoring
|
||||||
|
- Analytics utilisateur
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
### Stockage sécurisé
|
||||||
|
- Tokens dans Keychain/Keystore
|
||||||
|
- Chiffrement des données sensibles
|
||||||
|
- Validation côté client
|
||||||
|
|
||||||
|
### Communication
|
||||||
|
- HTTPS uniquement
|
||||||
|
- Certificate pinning
|
||||||
|
- Validation des certificats
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Variables d'environnement
|
||||||
|
- `API_BASE_URL` - URL de l'API
|
||||||
|
- `API_TIMEOUT` - Timeout des requêtes
|
||||||
|
- `ENVIRONMENT` - Environnement (dev/staging/prod)
|
||||||
|
|
||||||
|
### Configuration par environnement
|
||||||
|
- `config/development.js`
|
||||||
|
- `config/staging.js`
|
||||||
|
- `config/production.js`
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- Email: mobile-support@gbcm.com
|
||||||
|
- Documentation: https://docs.gbcm.com/mobile
|
||||||
|
- Issues: https://git.lions.dev/gbcm/gbcm-mobile-app/issues
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Propriétaire - GBCM LLC © 2024
|
||||||
93
package.json
Normal file
93
package.json
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
{
|
||||||
|
"name": "gbcm-mobile-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Application mobile GBCM pour iOS et Android",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"android": "react-native run-android",
|
||||||
|
"ios": "react-native run-ios",
|
||||||
|
"start": "react-native start",
|
||||||
|
"test": "jest",
|
||||||
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||||
|
"build:android": "cd android && ./gradlew assembleRelease",
|
||||||
|
"build:ios": "cd ios && xcodebuild -workspace GBCMMobile.xcworkspace -scheme GBCMMobile -configuration Release -destination generic/platform=iOS -archivePath GBCMMobile.xcarchive archive",
|
||||||
|
"clean": "react-native clean-project-auto",
|
||||||
|
"postinstall": "cd ios && pod install"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-native": "0.72.6",
|
||||||
|
"@react-navigation/native": "^6.1.9",
|
||||||
|
"@react-navigation/stack": "^6.3.20",
|
||||||
|
"@react-navigation/bottom-tabs": "^6.5.11",
|
||||||
|
"@react-navigation/drawer": "^6.6.6",
|
||||||
|
"react-native-screens": "^3.27.0",
|
||||||
|
"react-native-safe-area-context": "^4.7.4",
|
||||||
|
"react-native-gesture-handler": "^2.13.4",
|
||||||
|
"react-native-reanimated": "^3.5.4",
|
||||||
|
"@reduxjs/toolkit": "^1.9.7",
|
||||||
|
"react-redux": "^8.1.3",
|
||||||
|
"redux-persist": "^6.0.0",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"react-native-keychain": "^8.1.3",
|
||||||
|
"react-native-vector-icons": "^10.0.2",
|
||||||
|
"react-native-paper": "^5.11.1",
|
||||||
|
"react-native-chart-kit": "^6.12.0",
|
||||||
|
"react-native-svg": "^13.14.0",
|
||||||
|
"react-native-calendars": "^1.1301.0",
|
||||||
|
"react-native-image-picker": "^7.0.3",
|
||||||
|
"react-native-permissions": "^3.10.1",
|
||||||
|
"react-native-push-notification": "^8.1.1",
|
||||||
|
"@react-native-async-storage/async-storage": "^1.19.5",
|
||||||
|
"react-native-device-info": "^10.11.0",
|
||||||
|
"react-native-biometrics": "^3.0.1",
|
||||||
|
"react-native-config": "^1.5.1",
|
||||||
|
"react-native-splash-screen": "^3.3.0",
|
||||||
|
"react-hook-form": "^7.47.0",
|
||||||
|
"yup": "^1.3.3",
|
||||||
|
"@hookform/resolvers": "^3.3.2",
|
||||||
|
"date-fns": "^2.30.0",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.20.0",
|
||||||
|
"@babel/preset-env": "^7.20.0",
|
||||||
|
"@babel/runtime": "^7.20.0",
|
||||||
|
"@react-native/eslint-config": "^0.72.2",
|
||||||
|
"@react-native/metro-config": "^0.72.11",
|
||||||
|
"@tsconfig/react-native": "^3.0.0",
|
||||||
|
"@types/react": "^18.0.24",
|
||||||
|
"@types/react-test-renderer": "^18.0.0",
|
||||||
|
"babel-jest": "^29.2.1",
|
||||||
|
"eslint": "^8.19.0",
|
||||||
|
"jest": "^29.2.1",
|
||||||
|
"metro-react-native-babel-preset": "0.76.8",
|
||||||
|
"prettier": "^2.4.1",
|
||||||
|
"react-test-renderer": "18.2.0",
|
||||||
|
"typescript": "4.8.4",
|
||||||
|
"@types/lodash": "^4.14.200",
|
||||||
|
"detox": "^20.13.5",
|
||||||
|
"flipper-plugin-redux-debugger": "^0.8.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "react-native",
|
||||||
|
"setupFilesAfterEnv": ["<rootDir>/jest.setup.js"],
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"node_modules/(?!(react-native|@react-native|react-native-vector-icons|react-native-paper)/)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"react-native",
|
||||||
|
"mobile",
|
||||||
|
"gbcm",
|
||||||
|
"coaching",
|
||||||
|
"consulting",
|
||||||
|
"business"
|
||||||
|
],
|
||||||
|
"author": "GBCM LLC",
|
||||||
|
"license": "Proprietary",
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
264
src/screens/auth/LoginScreen.js
Normal file
264
src/screens/auth/LoginScreen.js
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
StyleSheet,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
ScrollView,
|
||||||
|
Alert,
|
||||||
|
} from 'react-native';
|
||||||
|
import {
|
||||||
|
TextInput,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Title,
|
||||||
|
Paragraph,
|
||||||
|
Checkbox,
|
||||||
|
ActivityIndicator,
|
||||||
|
useTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { loginUser } from '../../store/slices/authSlice';
|
||||||
|
|
||||||
|
// Schéma de validation
|
||||||
|
const loginSchema = yup.object().shape({
|
||||||
|
email: yup
|
||||||
|
.string()
|
||||||
|
.email('Format d\'email invalide')
|
||||||
|
.required('L\'email est obligatoire'),
|
||||||
|
password: yup
|
||||||
|
.string()
|
||||||
|
.min(6, 'Le mot de passe doit contenir au moins 6 caractères')
|
||||||
|
.required('Le mot de passe est obligatoire'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const LoginScreen = ({ navigation }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { loading, error } = useSelector((state) => state.auth);
|
||||||
|
const [rememberMe, setRememberMe] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm({
|
||||||
|
resolver: yupResolver(loginSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
try {
|
||||||
|
const loginData = {
|
||||||
|
...data,
|
||||||
|
rememberMe,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await dispatch(loginUser(loginData)).unwrap();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Navigation gérée automatiquement par le store
|
||||||
|
Alert.alert('Succès', 'Connexion réussie !');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Erreur', error.message || 'Erreur de connexion');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={styles.container}
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
>
|
||||||
|
<ScrollView contentContainerStyle={styles.scrollContainer}>
|
||||||
|
<View style={styles.logoContainer}>
|
||||||
|
<Title style={[styles.title, { color: theme.colors.primary }]}>
|
||||||
|
GBCM
|
||||||
|
</Title>
|
||||||
|
<Paragraph style={styles.subtitle}>
|
||||||
|
Global Business Consulting and Management
|
||||||
|
</Paragraph>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Card style={styles.card}>
|
||||||
|
<Card.Content>
|
||||||
|
<Title style={styles.cardTitle}>Connexion</Title>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="email"
|
||||||
|
render={({ field: { onChange, onBlur, value } }) => (
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
mode="outlined"
|
||||||
|
value={value}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChangeText={onChange}
|
||||||
|
error={!!errors.email}
|
||||||
|
keyboardType="email-address"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="email"
|
||||||
|
style={styles.input}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<Paragraph style={styles.errorText}>
|
||||||
|
{errors.email.message}
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="password"
|
||||||
|
render={({ field: { onChange, onBlur, value } }) => (
|
||||||
|
<TextInput
|
||||||
|
label="Mot de passe"
|
||||||
|
mode="outlined"
|
||||||
|
value={value}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChangeText={onChange}
|
||||||
|
error={!!errors.password}
|
||||||
|
secureTextEntry
|
||||||
|
autoComplete="password"
|
||||||
|
style={styles.input}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.password && (
|
||||||
|
<Paragraph style={styles.errorText}>
|
||||||
|
{errors.password.message}
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.checkboxContainer}>
|
||||||
|
<Checkbox
|
||||||
|
status={rememberMe ? 'checked' : 'unchecked'}
|
||||||
|
onPress={() => setRememberMe(!rememberMe)}
|
||||||
|
/>
|
||||||
|
<Paragraph style={styles.checkboxLabel}>
|
||||||
|
Se souvenir de moi
|
||||||
|
</Paragraph>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Paragraph style={styles.errorText}>
|
||||||
|
{error}
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
mode="contained"
|
||||||
|
onPress={handleSubmit(onSubmit)}
|
||||||
|
disabled={loading}
|
||||||
|
style={styles.loginButton}
|
||||||
|
contentStyle={styles.loginButtonContent}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<ActivityIndicator color={theme.colors.onPrimary} />
|
||||||
|
) : (
|
||||||
|
'Se connecter'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<View style={styles.linkContainer}>
|
||||||
|
<Button
|
||||||
|
mode="text"
|
||||||
|
onPress={() => navigation.navigate('ForgotPassword')}
|
||||||
|
style={styles.linkButton}
|
||||||
|
>
|
||||||
|
Mot de passe oublié ?
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<Paragraph style={styles.footerText}>
|
||||||
|
© 2024 GBCM LLC - Version 1.0.0
|
||||||
|
</Paragraph>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
},
|
||||||
|
scrollContainer: {
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
logoContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 30,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: 'center',
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
elevation: 4,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
cardTitle: {
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: '#d32f2f',
|
||||||
|
fontSize: 12,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
checkboxContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginVertical: 10,
|
||||||
|
},
|
||||||
|
checkboxLabel: {
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
loginButton: {
|
||||||
|
marginTop: 20,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
loginButtonContent: {
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
linkContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 10,
|
||||||
|
},
|
||||||
|
linkButton: {
|
||||||
|
marginVertical: 5,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 20,
|
||||||
|
},
|
||||||
|
footerText: {
|
||||||
|
fontSize: 12,
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default LoginScreen;
|
||||||
302
src/services/authService.js
Normal file
302
src/services/authService.js
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
import api from './api';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import Keychain from 'react-native-keychain';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service d'authentification pour l'application mobile GBCM
|
||||||
|
*/
|
||||||
|
class AuthService {
|
||||||
|
constructor() {
|
||||||
|
this.TOKEN_KEY = 'gbcm_auth_token';
|
||||||
|
this.REFRESH_TOKEN_KEY = 'gbcm_refresh_token';
|
||||||
|
this.USER_KEY = 'gbcm_user_data';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connexion utilisateur
|
||||||
|
*/
|
||||||
|
async login(credentials) {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/auth/login', credentials);
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
const { token, refreshToken, user, expiresAt } = response.data;
|
||||||
|
|
||||||
|
// Stockage sécurisé du token principal
|
||||||
|
await this.storeToken(token);
|
||||||
|
|
||||||
|
// Stockage du refresh token si disponible
|
||||||
|
if (refreshToken) {
|
||||||
|
await this.storeRefreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stockage des données utilisateur
|
||||||
|
await this.storeUserData(user);
|
||||||
|
|
||||||
|
// Configuration du header d'autorisation pour les futures requêtes
|
||||||
|
this.setAuthHeader(token);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
expiresAt,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || 'Erreur de connexion');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la connexion:', error);
|
||||||
|
throw this.handleError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déconnexion utilisateur
|
||||||
|
*/
|
||||||
|
async logout() {
|
||||||
|
try {
|
||||||
|
const token = await this.getToken();
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
// Appel API de déconnexion
|
||||||
|
try {
|
||||||
|
await api.post('/auth/logout', {}, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur lors de la déconnexion côté serveur:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyage local
|
||||||
|
await this.clearAuthData();
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la déconnexion:', error);
|
||||||
|
// Même en cas d'erreur, on nettoie les données locales
|
||||||
|
await this.clearAuthData();
|
||||||
|
throw this.handleError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rafraîchissement du token
|
||||||
|
*/
|
||||||
|
async refreshToken() {
|
||||||
|
try {
|
||||||
|
const refreshToken = await this.getRefreshToken();
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new Error('Aucun token de rafraîchissement disponible');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post('/auth/refresh', {
|
||||||
|
refreshToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
const { token, user, expiresAt } = response.data;
|
||||||
|
|
||||||
|
await this.storeToken(token);
|
||||||
|
await this.storeUserData(user);
|
||||||
|
this.setAuthHeader(token);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
expiresAt,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error('Impossible de rafraîchir le token');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du rafraîchissement:', error);
|
||||||
|
// En cas d'échec, on déconnecte l'utilisateur
|
||||||
|
await this.clearAuthData();
|
||||||
|
throw this.handleError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation du token actuel
|
||||||
|
*/
|
||||||
|
async validateToken() {
|
||||||
|
try {
|
||||||
|
const token = await this.getToken();
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.get('/auth/validate', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
user: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Token invalide:', error);
|
||||||
|
return { valid: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demande de réinitialisation de mot de passe
|
||||||
|
*/
|
||||||
|
async forgotPassword(email) {
|
||||||
|
try {
|
||||||
|
await api.post('/auth/forgot-password', { email });
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la demande de réinitialisation:', error);
|
||||||
|
throw this.handleError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réinitialisation du mot de passe
|
||||||
|
*/
|
||||||
|
async resetPassword(resetToken, newPassword) {
|
||||||
|
try {
|
||||||
|
await api.post('/auth/reset-password', {
|
||||||
|
resetToken,
|
||||||
|
newPassword,
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la réinitialisation:', error);
|
||||||
|
throw this.handleError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes de stockage sécurisé
|
||||||
|
async storeToken(token) {
|
||||||
|
try {
|
||||||
|
await Keychain.setInternetCredentials(
|
||||||
|
this.TOKEN_KEY,
|
||||||
|
'gbcm_user',
|
||||||
|
token
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur Keychain, utilisation AsyncStorage:', error);
|
||||||
|
await AsyncStorage.setItem(this.TOKEN_KEY, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getToken() {
|
||||||
|
try {
|
||||||
|
const credentials = await Keychain.getInternetCredentials(this.TOKEN_KEY);
|
||||||
|
if (credentials) {
|
||||||
|
return credentials.password;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur Keychain, utilisation AsyncStorage:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await AsyncStorage.getItem(this.TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeRefreshToken(refreshToken) {
|
||||||
|
try {
|
||||||
|
await Keychain.setInternetCredentials(
|
||||||
|
this.REFRESH_TOKEN_KEY,
|
||||||
|
'gbcm_user',
|
||||||
|
refreshToken
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
await AsyncStorage.setItem(this.REFRESH_TOKEN_KEY, refreshToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRefreshToken() {
|
||||||
|
try {
|
||||||
|
const credentials = await Keychain.getInternetCredentials(this.REFRESH_TOKEN_KEY);
|
||||||
|
if (credentials) {
|
||||||
|
return credentials.password;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur Keychain pour refresh token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await AsyncStorage.getItem(this.REFRESH_TOKEN_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeUserData(user) {
|
||||||
|
await AsyncStorage.setItem(this.USER_KEY, JSON.stringify(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserData() {
|
||||||
|
const userData = await AsyncStorage.getItem(this.USER_KEY);
|
||||||
|
return userData ? JSON.parse(userData) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearAuthData() {
|
||||||
|
try {
|
||||||
|
await Keychain.resetInternetCredentials(this.TOKEN_KEY);
|
||||||
|
await Keychain.resetInternetCredentials(this.REFRESH_TOKEN_KEY);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Erreur lors du nettoyage Keychain');
|
||||||
|
}
|
||||||
|
|
||||||
|
await AsyncStorage.multiRemove([
|
||||||
|
this.TOKEN_KEY,
|
||||||
|
this.REFRESH_TOKEN_KEY,
|
||||||
|
this.USER_KEY,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Suppression du header d'autorisation
|
||||||
|
delete api.defaults.headers.common['Authorization'];
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthHeader(token) {
|
||||||
|
api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(error) {
|
||||||
|
if (error.response) {
|
||||||
|
// Erreur de réponse du serveur
|
||||||
|
const message = error.response.data?.message || 'Erreur serveur';
|
||||||
|
return new Error(message);
|
||||||
|
} else if (error.request) {
|
||||||
|
// Erreur de réseau
|
||||||
|
return new Error('Erreur de connexion réseau');
|
||||||
|
} else {
|
||||||
|
// Autre erreur
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérification si l'utilisateur est connecté
|
||||||
|
*/
|
||||||
|
async isAuthenticated() {
|
||||||
|
const token = await this.getToken();
|
||||||
|
return !!token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisation du service (à appeler au démarrage de l'app)
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
const token = await this.getToken();
|
||||||
|
if (token) {
|
||||||
|
this.setAuthHeader(token);
|
||||||
|
|
||||||
|
// Validation du token au démarrage
|
||||||
|
const validation = await this.validateToken();
|
||||||
|
if (!validation.valid) {
|
||||||
|
await this.clearAuthData();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AuthService();
|
||||||
Reference in New Issue
Block a user