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