commit e7a367da362d6e188bb5d82fb6855ec42b7bcd6f Author: dahoud Date: Mon Oct 6 18:48:59 2025 +0000 Initial commit: GBCM Mobile App React Native with authentication and services diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ebe0ad --- /dev/null +++ b/.gitignore @@ -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* diff --git a/README.md b/README.md new file mode 100644 index 0000000..24a3e4e --- /dev/null +++ b/README.md @@ -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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..d0a3753 --- /dev/null +++ b/package.json @@ -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": ["/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 +} diff --git a/src/screens/auth/LoginScreen.js b/src/screens/auth/LoginScreen.js new file mode 100644 index 0000000..a92d455 --- /dev/null +++ b/src/screens/auth/LoginScreen.js @@ -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 ( + + + + + GBCM + + + Global Business Consulting and Management + + + + + + Connexion + + ( + + )} + /> + {errors.email && ( + + {errors.email.message} + + )} + + ( + + )} + /> + {errors.password && ( + + {errors.password.message} + + )} + + + setRememberMe(!rememberMe)} + /> + + Se souvenir de moi + + + + {error && ( + + {error} + + )} + + + + + + + + + + + + © 2024 GBCM LLC - Version 1.0.0 + + + + + ); +}; + +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; diff --git a/src/services/authService.js b/src/services/authService.js new file mode 100644 index 0000000..fc2bf26 --- /dev/null +++ b/src/services/authService.js @@ -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();