Initial commit: GBCM Mobile App React Native with authentication and services

This commit is contained in:
dahoud
2025-10-06 18:48:59 +00:00
commit e7a367da36
5 changed files with 1054 additions and 0 deletions

112
.gitignore vendored Normal file
View 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
View 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
View 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
}

View 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
View 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();