first commit
This commit is contained in:
188
unionflow-mobile-apps/src/App.tsx
Normal file
188
unionflow-mobile-apps/src/App.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
View,
|
||||
Alert,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { Provider as PaperProvider } from 'react-native-paper';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
import SplashScreen from 'react-native-splash-screen';
|
||||
import NetInfo from '@react-native-community/netinfo';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
|
||||
import { store, persistor } from './store/store';
|
||||
import { theme } from './theme/theme';
|
||||
import AppNavigator from './navigation/AppNavigator';
|
||||
import LoadingScreen from './components/common/LoadingScreen';
|
||||
import OfflineNotice from './components/common/OfflineNotice';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import { WavePaymentProvider } from './contexts/WavePaymentContext';
|
||||
import { NotificationService } from './services/NotificationService';
|
||||
import { BiometricService } from './services/BiometricService';
|
||||
import { AnalyticsService } from './services/AnalyticsService';
|
||||
|
||||
/**
|
||||
* Application mobile UnionFlow - Point d'entrée principal
|
||||
*
|
||||
* Cette application mobile moderne offre une expérience utilisateur exceptionnelle
|
||||
* pour la gestion d'associations en Côte d'Ivoire avec :
|
||||
*
|
||||
* - Interface ultra moderne et intuitive
|
||||
* - Intégration Wave Money pour les paiements
|
||||
* - Authentification biométrique
|
||||
* - Mode hors-ligne avec synchronisation
|
||||
* - Notifications push intelligentes
|
||||
* - Support multilingue (Français, Baoulé, Dioula).
|
||||
* - Design adaptatif pour tous les écrans
|
||||
*
|
||||
* @author Lions Dev Team
|
||||
* @version 2.0.0
|
||||
* @since 2025-01-16
|
||||
*/
|
||||
const App: React.FC = () => {
|
||||
const [isConnected, setIsConnected] = useState<boolean>(true);
|
||||
const [isAppReady, setIsAppReady] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
initializeApp();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Initialise l'application avec tous les services nécessaires
|
||||
*/
|
||||
const initializeApp = async () => {
|
||||
try {
|
||||
// Initialiser les services
|
||||
await Promise.all([
|
||||
initializeNetworkMonitoring(),
|
||||
initializeNotifications(),
|
||||
initializeBiometrics(),
|
||||
initializeAnalytics(),
|
||||
]);
|
||||
|
||||
// Masquer le splash screen
|
||||
if (Platform.OS === 'android') {
|
||||
SplashScreen.hide();
|
||||
}
|
||||
|
||||
setIsAppReady(true);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'initialisation de l\'app:', error);
|
||||
Alert.alert(
|
||||
'Erreur d\'initialisation',
|
||||
'Une erreur est survenue lors du démarrage de l\'application.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
setIsAppReady(true); // Continuer malgré l'erreur
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise la surveillance de la connectivité réseau
|
||||
*/
|
||||
const initializeNetworkMonitoring = async () => {
|
||||
const unsubscribe = NetInfo.addEventListener(state => {
|
||||
setIsConnected(state.isConnected ?? false);
|
||||
|
||||
if (!state.isConnected) {
|
||||
console.log('Application hors ligne - Mode offline activé');
|
||||
} else {
|
||||
console.log('Connexion rétablie - Synchronisation en cours...');
|
||||
// Déclencher la synchronisation des données
|
||||
// syncService.syncPendingData();
|
||||
}
|
||||
});
|
||||
|
||||
// Vérifier l'état initial
|
||||
const netInfo = await NetInfo.fetch();
|
||||
setIsConnected(netInfo.isConnected ?? false);
|
||||
|
||||
return unsubscribe;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise le service de notifications push
|
||||
*/
|
||||
const initializeNotifications = async () => {
|
||||
try {
|
||||
await NotificationService.initialize();
|
||||
console.log('Service de notifications initialisé');
|
||||
} catch (error) {
|
||||
console.warn('Erreur lors de l\'initialisation des notifications:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise l'authentification biométrique
|
||||
*/
|
||||
const initializeBiometrics = async () => {
|
||||
try {
|
||||
const isAvailable = await BiometricService.isAvailable();
|
||||
if (isAvailable) {
|
||||
console.log('Authentification biométrique disponible');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Erreur lors de l\'initialisation biométrique:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise le service d'analytics
|
||||
*/
|
||||
const initializeAnalytics = async () => {
|
||||
try {
|
||||
await AnalyticsService.initialize();
|
||||
AnalyticsService.trackEvent('app_started', {
|
||||
platform: Platform.OS,
|
||||
version: '2.0.0',
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Erreur lors de l\'initialisation analytics:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isAppReady) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={styles.container}>
|
||||
<ReduxProvider store={store}>
|
||||
<PersistGate loading={<LoadingScreen />} persistor={persistor}>
|
||||
<PaperProvider theme={theme}>
|
||||
<AuthProvider>
|
||||
<WavePaymentProvider>
|
||||
<NavigationContainer theme={theme}>
|
||||
<StatusBar
|
||||
barStyle="light-content"
|
||||
backgroundColor={theme.colors.primary}
|
||||
translucent={false}
|
||||
/>
|
||||
|
||||
<View style={styles.container}>
|
||||
<AppNavigator />
|
||||
|
||||
{/* Indicateur de connexion hors ligne */}
|
||||
{!isConnected && <OfflineNotice />}
|
||||
</View>
|
||||
</NavigationContainer>
|
||||
</WavePaymentProvider>
|
||||
</AuthProvider>
|
||||
</PaperProvider>
|
||||
</PersistGate>
|
||||
</ReduxProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default App;
|
||||
334
unionflow-mobile-apps/src/contexts/WavePaymentContext.tsx
Normal file
334
unionflow-mobile-apps/src/contexts/WavePaymentContext.tsx
Normal file
@@ -0,0 +1,334 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import NetInfo from '@react-native-community/netinfo';
|
||||
import {
|
||||
WavePaymentService,
|
||||
WavePaymentRequest,
|
||||
WavePaymentResult,
|
||||
WaveTransactionStatus
|
||||
} from '../services/WavePaymentService';
|
||||
import { AnalyticsService } from '../services/AnalyticsService';
|
||||
|
||||
/**
|
||||
* Contexte Wave Payment pour la gestion globale des paiements Wave Money
|
||||
*
|
||||
* Ce contexte fournit :
|
||||
* - État global des paiements Wave
|
||||
* - Gestion de la connectivité et synchronisation
|
||||
* - Cache des transactions récentes
|
||||
* - Notifications de statut en temps réel
|
||||
*
|
||||
* @author Lions Dev Team
|
||||
* @version 2.0.0
|
||||
* @since 2025-01-16
|
||||
*/
|
||||
|
||||
interface WavePaymentContextType {
|
||||
// État des paiements
|
||||
isLoading: boolean;
|
||||
currentTransaction: WavePaymentResult | null;
|
||||
recentTransactions: WavePaymentResult[];
|
||||
|
||||
// Connectivité
|
||||
isOnline: boolean;
|
||||
pendingPaymentsCount: number;
|
||||
|
||||
// Actions
|
||||
initiatePayment: (request: WavePaymentRequest) => Promise<WavePaymentResult>;
|
||||
checkTransactionStatus: (transactionId: string) => Promise<WaveTransactionStatus>;
|
||||
refreshTransactions: () => Promise<void>;
|
||||
syncPendingPayments: () => Promise<void>;
|
||||
clearCurrentTransaction: () => void;
|
||||
|
||||
// Utilitaires
|
||||
calculateFees: (amount: string) => Promise<{ base: string; fees: string; total: string }>;
|
||||
getPaymentHistory: () => Promise<WavePaymentResult[]>;
|
||||
}
|
||||
|
||||
const WavePaymentContext = createContext<WavePaymentContextType | undefined>(undefined);
|
||||
|
||||
interface WavePaymentProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const WavePaymentProvider: React.FC<WavePaymentProviderProps> = ({ children }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentTransaction, setCurrentTransaction] = useState<WavePaymentResult | null>(null);
|
||||
const [recentTransactions, setRecentTransactions] = useState<WavePaymentResult[]>([]);
|
||||
const [isOnline, setIsOnline] = useState(true);
|
||||
const [pendingPaymentsCount, setPendingPaymentsCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
initializeContext();
|
||||
setupNetworkListener();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Initialise le contexte avec les données sauvegardées
|
||||
*/
|
||||
const initializeContext = async () => {
|
||||
try {
|
||||
// Charger l'historique des paiements
|
||||
const history = await WavePaymentService.getPaymentHistory();
|
||||
setRecentTransactions(history.slice(0, 10)); // 10 plus récents
|
||||
|
||||
// Compter les paiements en attente
|
||||
await updatePendingPaymentsCount();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur initialisation contexte Wave:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure l'écoute de la connectivité réseau
|
||||
*/
|
||||
const setupNetworkListener = () => {
|
||||
const unsubscribe = NetInfo.addEventListener(state => {
|
||||
const wasOffline = !isOnline;
|
||||
const isNowOnline = state.isConnected ?? false;
|
||||
|
||||
setIsOnline(isNowOnline);
|
||||
|
||||
// Si on revient en ligne, synchroniser les paiements en attente
|
||||
if (wasOffline && isNowOnline) {
|
||||
console.log('Connexion rétablie - Synchronisation des paiements Wave');
|
||||
syncPendingPayments();
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
};
|
||||
|
||||
/**
|
||||
* Met à jour le nombre de paiements en attente
|
||||
*/
|
||||
const updatePendingPaymentsCount = async () => {
|
||||
try {
|
||||
const pendingPayments = await WavePaymentService.getPendingPayments();
|
||||
setPendingPaymentsCount(pendingPayments.length);
|
||||
} catch (error) {
|
||||
console.error('Erreur mise à jour paiements en attente:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initie un paiement Wave Money
|
||||
*/
|
||||
const initiatePayment = async (request: WavePaymentRequest): Promise<WavePaymentResult> => {
|
||||
setIsLoading(true);
|
||||
setCurrentTransaction(null);
|
||||
|
||||
try {
|
||||
const result = await WavePaymentService.initiatePayment(request);
|
||||
|
||||
setCurrentTransaction(result);
|
||||
|
||||
if (result.success) {
|
||||
// Ajouter à l'historique récent
|
||||
setRecentTransactions(prev => [result, ...prev.slice(0, 9)]);
|
||||
|
||||
// Analytics
|
||||
AnalyticsService.trackEvent('wave_payment_context_success', {
|
||||
type: request.type,
|
||||
amount: request.amount,
|
||||
transactionId: result.transactionId,
|
||||
});
|
||||
} else {
|
||||
// Si échec à cause de la connectivité, mettre à jour le compteur
|
||||
if (!isOnline) {
|
||||
await updatePendingPaymentsCount();
|
||||
}
|
||||
|
||||
AnalyticsService.trackEvent('wave_payment_context_failure', {
|
||||
type: request.type,
|
||||
amount: request.amount,
|
||||
error: result.error,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur contexte paiement Wave:', error);
|
||||
|
||||
const errorResult: WavePaymentResult = {
|
||||
success: false,
|
||||
error: 'Erreur inattendue lors du paiement',
|
||||
};
|
||||
|
||||
setCurrentTransaction(errorResult);
|
||||
return errorResult;
|
||||
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Vérifie le statut d'une transaction
|
||||
*/
|
||||
const checkTransactionStatus = async (transactionId: string): Promise<WaveTransactionStatus> => {
|
||||
try {
|
||||
const status = await WavePaymentService.checkTransactionStatus(transactionId);
|
||||
|
||||
// Mettre à jour la transaction dans l'historique si nécessaire
|
||||
setRecentTransactions(prev =>
|
||||
prev.map(transaction =>
|
||||
transaction.transactionId === transactionId
|
||||
? { ...transaction, status: status.status as any }
|
||||
: transaction
|
||||
)
|
||||
);
|
||||
|
||||
return status;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur vérification statut:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Actualise la liste des transactions
|
||||
*/
|
||||
const refreshTransactions = async () => {
|
||||
try {
|
||||
const history = await WavePaymentService.getPaymentHistory();
|
||||
setRecentTransactions(history.slice(0, 10));
|
||||
await updatePendingPaymentsCount();
|
||||
} catch (error) {
|
||||
console.error('Erreur actualisation transactions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Synchronise les paiements en attente
|
||||
*/
|
||||
const syncPendingPayments = async () => {
|
||||
if (!isOnline) {
|
||||
console.log('Hors ligne - Synchronisation reportée');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await WavePaymentService.syncPendingPayments();
|
||||
await updatePendingPaymentsCount();
|
||||
await refreshTransactions();
|
||||
|
||||
AnalyticsService.trackEvent('wave_payments_synced', {
|
||||
pendingCount: pendingPaymentsCount,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur synchronisation paiements:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Efface la transaction courante
|
||||
*/
|
||||
const clearCurrentTransaction = () => {
|
||||
setCurrentTransaction(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calcule les frais Wave Money
|
||||
*/
|
||||
const calculateFees = async (amount: string) => {
|
||||
try {
|
||||
return await WavePaymentService.calculateFees(amount);
|
||||
} catch (error) {
|
||||
console.error('Erreur calcul frais contexte:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retourne l'historique complet des paiements
|
||||
*/
|
||||
const getPaymentHistory = async (): Promise<WavePaymentResult[]> => {
|
||||
try {
|
||||
return await WavePaymentService.getPaymentHistory();
|
||||
} catch (error) {
|
||||
console.error('Erreur récupération historique:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const contextValue: WavePaymentContextType = {
|
||||
// État
|
||||
isLoading,
|
||||
currentTransaction,
|
||||
recentTransactions,
|
||||
isOnline,
|
||||
pendingPaymentsCount,
|
||||
|
||||
// Actions
|
||||
initiatePayment,
|
||||
checkTransactionStatus,
|
||||
refreshTransactions,
|
||||
syncPendingPayments,
|
||||
clearCurrentTransaction,
|
||||
|
||||
// Utilitaires
|
||||
calculateFees,
|
||||
getPaymentHistory,
|
||||
};
|
||||
|
||||
return (
|
||||
<WavePaymentContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</WavePaymentContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook pour utiliser le contexte Wave Payment
|
||||
*/
|
||||
export const useWavePayment = (): WavePaymentContextType => {
|
||||
const context = useContext(WavePaymentContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('useWavePayment doit être utilisé dans un WavePaymentProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook pour vérifier si Wave Money est disponible
|
||||
*/
|
||||
export const useWaveAvailability = () => {
|
||||
const { isOnline } = useWavePayment();
|
||||
|
||||
return {
|
||||
isWaveAvailable: isOnline, // Simplification - en réalité, vérifier aussi la config
|
||||
reason: isOnline ? null : 'Connexion internet requise',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook pour les statistiques de paiement Wave
|
||||
*/
|
||||
export const useWaveStats = () => {
|
||||
const { recentTransactions, pendingPaymentsCount } = useWavePayment();
|
||||
|
||||
const successfulPayments = recentTransactions.filter(t => t.success).length;
|
||||
const failedPayments = recentTransactions.filter(t => !t.success).length;
|
||||
const totalAmount = recentTransactions
|
||||
.filter(t => t.success)
|
||||
.reduce((sum, t) => {
|
||||
// Extraire le montant de la transaction (simplification)
|
||||
return sum + 0; // À implémenter selon le format des données
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
successfulPayments,
|
||||
failedPayments,
|
||||
pendingPaymentsCount,
|
||||
totalAmount,
|
||||
successRate: recentTransactions.length > 0
|
||||
? (successfulPayments / recentTransactions.length) * 100
|
||||
: 0,
|
||||
};
|
||||
};
|
||||
368
unionflow-mobile-apps/src/navigation/AppNavigator.tsx
Normal file
368
unionflow-mobile-apps/src/navigation/AppNavigator.tsx
Normal file
@@ -0,0 +1,368 @@
|
||||
import React from 'react';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import { createDrawerNavigator } from '@react-navigation/drawer';
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { theme } from '../theme/theme';
|
||||
|
||||
// Écrans d'authentification
|
||||
import LoginScreen from '../screens/auth/LoginScreen';
|
||||
import RegisterScreen from '../screens/auth/RegisterScreen';
|
||||
import ForgotPasswordScreen from '../screens/auth/ForgotPasswordScreen';
|
||||
import BiometricSetupScreen from '../screens/auth/BiometricSetupScreen';
|
||||
|
||||
// Écrans principaux
|
||||
import HomeScreen from '../screens/home/HomeScreen';
|
||||
import CotisationsScreen from '../screens/cotisations/CotisationsScreen';
|
||||
import PaymentScreen from '../screens/payment/PaymentScreen';
|
||||
import WavePaymentScreen from '../screens/payment/WavePaymentScreen';
|
||||
import ProfileScreen from '../screens/profile/ProfileScreen';
|
||||
import AssociationsScreen from '../screens/associations/AssociationsScreen';
|
||||
import MembersScreen from '../screens/members/MembersScreen';
|
||||
import EventsScreen from '../screens/events/EventsScreen';
|
||||
import AideMutuelleScreen from '../screens/aide/AideMutuelleScreen';
|
||||
import NotificationsScreen from '../screens/notifications/NotificationsScreen';
|
||||
import SettingsScreen from '../screens/settings/SettingsScreen';
|
||||
|
||||
// Écrans de workflow
|
||||
import WorkflowScreen from '../screens/workflow/WorkflowScreen';
|
||||
import AdhesionWorkflowScreen from '../screens/workflow/AdhesionWorkflowScreen';
|
||||
|
||||
// Types de navigation
|
||||
export type RootStackParamList = {
|
||||
Auth: undefined;
|
||||
Main: undefined;
|
||||
Payment: { type: 'cotisation' | 'adhesion' | 'aide' | 'evenement'; amount?: string };
|
||||
WavePayment: {
|
||||
type: 'cotisation' | 'adhesion' | 'aide' | 'evenement';
|
||||
amount: string;
|
||||
description: string;
|
||||
metadata?: any;
|
||||
};
|
||||
Workflow: { workflowId: string; instanceId?: string };
|
||||
AdhesionWorkflow: { associationId: string };
|
||||
};
|
||||
|
||||
export type AuthStackParamList = {
|
||||
Login: undefined;
|
||||
Register: undefined;
|
||||
ForgotPassword: undefined;
|
||||
BiometricSetup: undefined;
|
||||
};
|
||||
|
||||
export type MainTabParamList = {
|
||||
Home: undefined;
|
||||
Cotisations: undefined;
|
||||
Associations: undefined;
|
||||
Profile: undefined;
|
||||
More: undefined;
|
||||
};
|
||||
|
||||
export type DrawerParamList = {
|
||||
MainTabs: undefined;
|
||||
Members: undefined;
|
||||
Events: undefined;
|
||||
AideMutuelle: undefined;
|
||||
Notifications: undefined;
|
||||
Settings: undefined;
|
||||
};
|
||||
|
||||
const RootStack = createStackNavigator<RootStackParamList>();
|
||||
const AuthStack = createStackNavigator<AuthStackParamList>();
|
||||
const MainTab = createBottomTabNavigator<MainTabParamList>();
|
||||
const Drawer = createDrawerNavigator<DrawerParamList>();
|
||||
|
||||
/**
|
||||
* Navigateur d'authentification
|
||||
*/
|
||||
const AuthNavigator: React.FC = () => {
|
||||
return (
|
||||
<AuthStack.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
cardStyle: { backgroundColor: theme.colors.background },
|
||||
}}
|
||||
>
|
||||
<AuthStack.Screen name="Login" component={LoginScreen} />
|
||||
<AuthStack.Screen name="Register" component={RegisterScreen} />
|
||||
<AuthStack.Screen name="ForgotPassword" component={ForgotPasswordScreen} />
|
||||
<AuthStack.Screen name="BiometricSetup" component={BiometricSetupScreen} />
|
||||
</AuthStack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigateur à onglets principal
|
||||
*/
|
||||
const MainTabNavigator: React.FC = () => {
|
||||
return (
|
||||
<MainTab.Navigator
|
||||
screenOptions={({ route }) => ({
|
||||
headerShown: false,
|
||||
tabBarIcon: ({ focused, color, size }) => {
|
||||
let iconName: string;
|
||||
|
||||
switch (route.name) {
|
||||
case 'Home':
|
||||
iconName = focused ? 'home' : 'home-outline';
|
||||
break;
|
||||
case 'Cotisations':
|
||||
iconName = focused ? 'credit-card' : 'credit-card-outline';
|
||||
break;
|
||||
case 'Associations':
|
||||
iconName = focused ? 'account-group' : 'account-group-outline';
|
||||
break;
|
||||
case 'Profile':
|
||||
iconName = focused ? 'account' : 'account-outline';
|
||||
break;
|
||||
case 'More':
|
||||
iconName = focused ? 'menu' : 'menu';
|
||||
break;
|
||||
default:
|
||||
iconName = 'circle';
|
||||
}
|
||||
|
||||
return <Icon name={iconName} size={size} color={color} />;
|
||||
},
|
||||
tabBarActiveTintColor: theme.colors.primary,
|
||||
tabBarInactiveTintColor: theme.custom.colors.textSecondary,
|
||||
tabBarStyle: {
|
||||
backgroundColor: theme.colors.surface,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: theme.custom.colors.border,
|
||||
height: Platform.OS === 'ios' ? 83 : 60,
|
||||
paddingBottom: Platform.OS === 'ios' ? 20 : 8,
|
||||
paddingTop: 8,
|
||||
},
|
||||
tabBarLabelStyle: {
|
||||
fontSize: theme.custom.typography.sizes.xs,
|
||||
fontFamily: theme.custom.typography.families.medium,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<MainTab.Screen
|
||||
name="Home"
|
||||
component={HomeScreen}
|
||||
options={{ tabBarLabel: 'Accueil' }}
|
||||
/>
|
||||
<MainTab.Screen
|
||||
name="Cotisations"
|
||||
component={CotisationsScreen}
|
||||
options={{ tabBarLabel: 'Cotisations' }}
|
||||
/>
|
||||
<MainTab.Screen
|
||||
name="Associations"
|
||||
component={AssociationsScreen}
|
||||
options={{ tabBarLabel: 'Associations' }}
|
||||
/>
|
||||
<MainTab.Screen
|
||||
name="Profile"
|
||||
component={ProfileScreen}
|
||||
options={{ tabBarLabel: 'Profil' }}
|
||||
/>
|
||||
<MainTab.Screen
|
||||
name="More"
|
||||
component={MoreScreen}
|
||||
options={{ tabBarLabel: 'Plus' }}
|
||||
/>
|
||||
</MainTab.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Écran "Plus" avec navigation vers les autres sections
|
||||
*/
|
||||
const MoreScreen: React.FC = ({ navigation }: any) => {
|
||||
const menuItems = [
|
||||
{ title: 'Membres', icon: 'account-multiple', screen: 'Members' },
|
||||
{ title: 'Événements', icon: 'calendar', screen: 'Events' },
|
||||
{ title: 'Aide Mutuelle', icon: 'hand-heart', screen: 'AideMutuelle' },
|
||||
{ title: 'Notifications', icon: 'bell', screen: 'Notifications' },
|
||||
{ title: 'Paramètres', icon: 'cog', screen: 'Settings' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1, padding: 16 }}>
|
||||
{menuItems.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
backgroundColor: theme.colors.surface,
|
||||
marginBottom: 8,
|
||||
borderRadius: 12,
|
||||
}}
|
||||
onClick={() => navigation.navigate(item.screen)}
|
||||
>
|
||||
<Icon name={item.icon} size={24} color={theme.colors.primary} />
|
||||
<span style={{ marginLeft: 16, fontSize: 16 }}>{item.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigateur avec tiroir (drawer)
|
||||
*/
|
||||
const DrawerNavigator: React.FC = () => {
|
||||
return (
|
||||
<Drawer.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
drawerStyle: {
|
||||
backgroundColor: theme.colors.background,
|
||||
width: 280,
|
||||
},
|
||||
drawerActiveTintColor: theme.colors.primary,
|
||||
drawerInactiveTintColor: theme.custom.colors.textSecondary,
|
||||
drawerLabelStyle: {
|
||||
fontFamily: theme.custom.typography.families.medium,
|
||||
fontSize: theme.custom.typography.sizes.base,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Drawer.Screen
|
||||
name="MainTabs"
|
||||
component={MainTabNavigator}
|
||||
options={{
|
||||
drawerLabel: 'Accueil',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Icon name="home" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="Members"
|
||||
component={MembersScreen}
|
||||
options={{
|
||||
drawerLabel: 'Membres',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Icon name="account-multiple" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="Events"
|
||||
component={EventsScreen}
|
||||
options={{
|
||||
drawerLabel: 'Événements',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Icon name="calendar" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="AideMutuelle"
|
||||
component={AideMutuelleScreen}
|
||||
options={{
|
||||
drawerLabel: 'Aide Mutuelle',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Icon name="hand-heart" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="Notifications"
|
||||
component={NotificationsScreen}
|
||||
options={{
|
||||
drawerLabel: 'Notifications',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Icon name="bell" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="Settings"
|
||||
component={SettingsScreen}
|
||||
options={{
|
||||
drawerLabel: 'Paramètres',
|
||||
drawerIcon: ({ color, size }) => (
|
||||
<Icon name="cog" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Drawer.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigateur principal de l'application
|
||||
*/
|
||||
const AppNavigator: React.FC = () => {
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
return (
|
||||
<RootStack.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
cardStyle: { backgroundColor: theme.colors.background },
|
||||
}}
|
||||
>
|
||||
{!isAuthenticated ? (
|
||||
<RootStack.Screen name="Auth" component={AuthNavigator} />
|
||||
) : (
|
||||
<>
|
||||
<RootStack.Screen name="Main" component={DrawerNavigator} />
|
||||
<RootStack.Screen
|
||||
name="Payment"
|
||||
component={PaymentScreen}
|
||||
options={{
|
||||
presentation: 'modal',
|
||||
headerShown: true,
|
||||
headerTitle: 'Paiement',
|
||||
headerStyle: {
|
||||
backgroundColor: theme.colors.primary,
|
||||
},
|
||||
headerTintColor: theme.custom.colors.textOnPrimary,
|
||||
}}
|
||||
/>
|
||||
<RootStack.Screen
|
||||
name="WavePayment"
|
||||
component={WavePaymentScreen}
|
||||
options={{
|
||||
presentation: 'modal',
|
||||
headerShown: true,
|
||||
headerTitle: 'Paiement Wave Money',
|
||||
headerStyle: {
|
||||
backgroundColor: theme.custom.colors.wave,
|
||||
},
|
||||
headerTintColor: theme.custom.colors.textOnPrimary,
|
||||
}}
|
||||
/>
|
||||
<RootStack.Screen
|
||||
name="Workflow"
|
||||
component={WorkflowScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
headerTitle: 'Processus',
|
||||
headerStyle: {
|
||||
backgroundColor: theme.colors.primary,
|
||||
},
|
||||
headerTintColor: theme.custom.colors.textOnPrimary,
|
||||
}}
|
||||
/>
|
||||
<RootStack.Screen
|
||||
name="AdhesionWorkflow"
|
||||
component={AdhesionWorkflowScreen}
|
||||
options={{
|
||||
headerShown: true,
|
||||
headerTitle: 'Adhésion',
|
||||
headerStyle: {
|
||||
backgroundColor: theme.colors.primary,
|
||||
},
|
||||
headerTintColor: theme.custom.colors.textOnPrimary,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</RootStack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppNavigator;
|
||||
600
unionflow-mobile-apps/src/screens/payment/WavePaymentScreen.tsx
Normal file
600
unionflow-mobile-apps/src/screens/payment/WavePaymentScreen.tsx
Normal file
@@ -0,0 +1,600 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
Alert,
|
||||
Vibration,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
TextInput,
|
||||
ActivityIndicator,
|
||||
Divider,
|
||||
Chip,
|
||||
} from 'react-native-paper';
|
||||
import { RouteProp } from '@react-navigation/native';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { Formik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withSpring,
|
||||
withSequence,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
import { theme } from '../../theme/theme';
|
||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||
import { useWavePayment } from '../../contexts/WavePaymentContext';
|
||||
import { WavePaymentService } from '../../services/WavePaymentService';
|
||||
import { AnalyticsService } from '../../services/AnalyticsService';
|
||||
import { HapticService } from '../../services/HapticService';
|
||||
|
||||
type WavePaymentScreenRouteProp = RouteProp<RootStackParamList, 'WavePayment'>;
|
||||
type WavePaymentScreenNavigationProp = StackNavigationProp<RootStackParamList, 'WavePayment'>;
|
||||
|
||||
interface Props {
|
||||
route: WavePaymentScreenRouteProp;
|
||||
navigation: WavePaymentScreenNavigationProp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Écran de paiement Wave Money ultra moderne
|
||||
*
|
||||
* Interface intuitive et sécurisée pour les paiements Wave Money en Côte d'Ivoire :
|
||||
* - Design moderne avec animations fluides
|
||||
* - Validation en temps réel des numéros Wave
|
||||
* - Calcul automatique des frais
|
||||
* - Feedback visuel et haptique
|
||||
* - Gestion des erreurs élégante
|
||||
* - Support hors ligne avec synchronisation
|
||||
*
|
||||
* @author Lions Dev Team
|
||||
* @version 2.0.0
|
||||
* @since 2025-01-16
|
||||
*/
|
||||
const WavePaymentScreen: React.FC<Props> = ({ route, navigation }) => {
|
||||
const { type, amount, description, metadata } = route.params;
|
||||
const { initiatePayment, isLoading } = useWavePayment();
|
||||
|
||||
const [fees, setFees] = useState<{ base: string; fees: string; total: string } | null>(null);
|
||||
const [isCalculatingFees, setIsCalculatingFees] = useState(false);
|
||||
const [paymentStatus, setPaymentStatus] = useState<'idle' | 'processing' | 'success' | 'error'>('idle');
|
||||
|
||||
// Animations
|
||||
const cardScale = useSharedValue(1);
|
||||
const waveIconRotation = useSharedValue(0);
|
||||
const successScale = useSharedValue(0);
|
||||
|
||||
useEffect(() => {
|
||||
// Animation d'entrée
|
||||
cardScale.value = withSpring(1, { damping: 15 });
|
||||
|
||||
// Calculer les frais automatiquement
|
||||
if (amount) {
|
||||
calculateFees(amount);
|
||||
}
|
||||
|
||||
// Analytics
|
||||
AnalyticsService.trackEvent('wave_payment_screen_opened', {
|
||||
type,
|
||||
amount,
|
||||
});
|
||||
}, [amount]);
|
||||
|
||||
/**
|
||||
* Schéma de validation Yup pour le formulaire
|
||||
*/
|
||||
const validationSchema = Yup.object().shape({
|
||||
phoneNumber: Yup.string()
|
||||
.required('Le numéro de téléphone est obligatoire')
|
||||
.matches(/^\+225[0-9]{8}$/, 'Format invalide. Utilisez +225XXXXXXXX')
|
||||
.test('wave-number', 'Ce numéro ne semble pas être un numéro Wave valide', (value) => {
|
||||
// Validation basique des numéros Wave CI
|
||||
if (!value) return false;
|
||||
const number = value.replace('+225', '');
|
||||
// Les numéros Wave commencent généralement par 01, 05, 07
|
||||
return /^(01|05|07)[0-9]{6}$/.test(number);
|
||||
}),
|
||||
amount: Yup.string()
|
||||
.required('Le montant est obligatoire')
|
||||
.test('min-amount', 'Montant minimum : 100 FCFA', (value) => {
|
||||
return value ? parseFloat(value) >= 100 : false;
|
||||
})
|
||||
.test('max-amount', 'Montant maximum : 1,000,000 FCFA', (value) => {
|
||||
return value ? parseFloat(value) <= 1000000 : false;
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Calcule les frais Wave Money
|
||||
*/
|
||||
const calculateFees = async (amount: string) => {
|
||||
setIsCalculatingFees(true);
|
||||
try {
|
||||
const result = await WavePaymentService.calculateFees(amount);
|
||||
setFees(result);
|
||||
|
||||
// Animation de l'icône Wave
|
||||
waveIconRotation.value = withSequence(
|
||||
withTiming(360, { duration: 500 }),
|
||||
withTiming(0, { duration: 0 })
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Erreur calcul frais:', error);
|
||||
} finally {
|
||||
setIsCalculatingFees(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Traite le paiement Wave Money
|
||||
*/
|
||||
const handlePayment = async (values: { phoneNumber: string; amount: string }) => {
|
||||
try {
|
||||
setPaymentStatus('processing');
|
||||
HapticService.impact('medium');
|
||||
|
||||
// Animation de traitement
|
||||
cardScale.value = withSpring(0.95);
|
||||
|
||||
const paymentRequest = {
|
||||
type,
|
||||
amount: values.amount,
|
||||
phoneNumber: values.phoneNumber,
|
||||
description,
|
||||
metadata,
|
||||
};
|
||||
|
||||
const result = await initiatePayment(paymentRequest);
|
||||
|
||||
if (result.success) {
|
||||
setPaymentStatus('success');
|
||||
successScale.value = withSpring(1);
|
||||
HapticService.success();
|
||||
|
||||
// Vibration de succès
|
||||
if (Platform.OS === 'android') {
|
||||
Vibration.vibrate([100, 200, 100]);
|
||||
}
|
||||
|
||||
AnalyticsService.trackEvent('wave_payment_success', {
|
||||
type,
|
||||
amount: values.amount,
|
||||
transactionId: result.transactionId,
|
||||
});
|
||||
|
||||
Alert.alert(
|
||||
'Paiement initié',
|
||||
`Votre paiement de ${values.amount} FCFA a été initié avec succès.\n\nTransaction ID: ${result.transactionId}`,
|
||||
[
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => navigation.goBack(),
|
||||
},
|
||||
]
|
||||
);
|
||||
} else {
|
||||
throw new Error(result.error || 'Erreur lors du paiement');
|
||||
}
|
||||
} catch (error) {
|
||||
setPaymentStatus('error');
|
||||
HapticService.error();
|
||||
|
||||
AnalyticsService.trackEvent('wave_payment_error', {
|
||||
type,
|
||||
amount: values.amount,
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
Alert.alert(
|
||||
'Erreur de paiement',
|
||||
error.message || 'Une erreur est survenue lors du paiement. Veuillez réessayer.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
} finally {
|
||||
cardScale.value = withSpring(1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Formate un numéro de téléphone Wave
|
||||
*/
|
||||
const formatPhoneNumber = (text: string) => {
|
||||
// Supprimer tous les caractères non numériques sauf +
|
||||
let cleaned = text.replace(/[^\d+]/g, '');
|
||||
|
||||
// Ajouter +225 si pas présent
|
||||
if (!cleaned.startsWith('+225')) {
|
||||
if (cleaned.startsWith('225')) {
|
||||
cleaned = '+' + cleaned;
|
||||
} else if (cleaned.startsWith('0')) {
|
||||
cleaned = '+225' + cleaned.substring(1);
|
||||
} else if (cleaned.length > 0 && !cleaned.startsWith('+')) {
|
||||
cleaned = '+225' + cleaned;
|
||||
}
|
||||
}
|
||||
|
||||
// Limiter à +225 + 8 chiffres
|
||||
if (cleaned.length > 12) {
|
||||
cleaned = cleaned.substring(0, 12);
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
const cardAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: cardScale.value }],
|
||||
}));
|
||||
|
||||
const waveIconAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ rotate: `${waveIconRotation.value}deg` }],
|
||||
}));
|
||||
|
||||
const successAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: successScale.value }],
|
||||
opacity: successScale.value,
|
||||
}));
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
|
||||
<Animated.View style={[cardAnimatedStyle]}>
|
||||
{/* En-tête Wave Money */}
|
||||
<Card style={styles.waveCard}>
|
||||
<View style={styles.waveHeader}>
|
||||
<Animated.View style={waveIconAnimatedStyle}>
|
||||
<Icon name="wave" size={32} color={theme.custom.colors.textOnPrimary} />
|
||||
</Animated.View>
|
||||
<View style={styles.waveInfo}>
|
||||
<Text style={styles.waveTitle}>Wave Money</Text>
|
||||
<Text style={styles.waveSubtitle}>Paiement mobile sécurisé</Text>
|
||||
</View>
|
||||
<Chip
|
||||
mode="outlined"
|
||||
textStyle={styles.chipText}
|
||||
style={styles.chip}
|
||||
>
|
||||
Côte d'Ivoire
|
||||
</Chip>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* Détails du paiement */}
|
||||
<Card style={styles.detailsCard}>
|
||||
<Text style={styles.sectionTitle}>Détails du paiement</Text>
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.detailLabel}>Type :</Text>
|
||||
<Text style={styles.detailValue}>{getPaymentTypeLabel(type)}</Text>
|
||||
</View>
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.detailLabel}>Description :</Text>
|
||||
<Text style={styles.detailValue}>{description}</Text>
|
||||
</View>
|
||||
<Divider style={styles.divider} />
|
||||
|
||||
{/* Calcul des frais */}
|
||||
{fees && (
|
||||
<View style={styles.feesContainer}>
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.detailLabel}>Montant :</Text>
|
||||
<Text style={styles.detailValue}>{fees.base}</Text>
|
||||
</View>
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.detailLabel}>Frais Wave :</Text>
|
||||
<Text style={styles.feesValue}>{fees.fees}</Text>
|
||||
</View>
|
||||
<Divider style={styles.divider} />
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.totalLabel}>Total à payer :</Text>
|
||||
<Text style={styles.totalValue}>{fees.total}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{isCalculatingFees && (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" color={theme.colors.primary} />
|
||||
<Text style={styles.loadingText}>Calcul des frais...</Text>
|
||||
</View>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Formulaire de paiement */}
|
||||
<Formik
|
||||
initialValues={{
|
||||
phoneNumber: '',
|
||||
amount: amount || '',
|
||||
}}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handlePayment}
|
||||
>
|
||||
{({ handleChange, handleBlur, handleSubmit, values, errors, touched, setFieldValue }) => (
|
||||
<Card style={styles.formCard}>
|
||||
<Text style={styles.sectionTitle}>Informations de paiement</Text>
|
||||
|
||||
<TextInput
|
||||
label="Numéro Wave Money"
|
||||
value={values.phoneNumber}
|
||||
onChangeText={(text) => {
|
||||
const formatted = formatPhoneNumber(text);
|
||||
setFieldValue('phoneNumber', formatted);
|
||||
}}
|
||||
onBlur={handleBlur('phoneNumber')}
|
||||
error={touched.phoneNumber && !!errors.phoneNumber}
|
||||
mode="outlined"
|
||||
style={styles.input}
|
||||
left={<TextInput.Icon icon="phone" />}
|
||||
placeholder="+225XXXXXXXX"
|
||||
keyboardType="phone-pad"
|
||||
maxLength={12}
|
||||
/>
|
||||
{touched.phoneNumber && errors.phoneNumber && (
|
||||
<Text style={styles.errorText}>{errors.phoneNumber}</Text>
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
label="Montant (FCFA)"
|
||||
value={values.amount}
|
||||
onChangeText={(text) => {
|
||||
handleChange('amount')(text);
|
||||
if (text && parseFloat(text) >= 100) {
|
||||
calculateFees(text);
|
||||
}
|
||||
}}
|
||||
onBlur={handleBlur('amount')}
|
||||
error={touched.amount && !!errors.amount}
|
||||
mode="outlined"
|
||||
style={styles.input}
|
||||
left={<TextInput.Icon icon="currency-usd" />}
|
||||
placeholder="0"
|
||||
keyboardType="numeric"
|
||||
editable={!amount} // Désactiver si montant prédéfini
|
||||
/>
|
||||
{touched.amount && errors.amount && (
|
||||
<Text style={styles.errorText}>{errors.amount}</Text>
|
||||
)}
|
||||
|
||||
{/* Bouton de paiement */}
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={handleSubmit}
|
||||
loading={isLoading || paymentStatus === 'processing'}
|
||||
disabled={isLoading || paymentStatus === 'processing'}
|
||||
style={styles.payButton}
|
||||
contentStyle={styles.payButtonContent}
|
||||
labelStyle={styles.payButtonLabel}
|
||||
icon="credit-card"
|
||||
>
|
||||
{paymentStatus === 'processing' ? 'Traitement...' : 'Payer avec Wave'}
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
{/* Indicateur de succès */}
|
||||
{paymentStatus === 'success' && (
|
||||
<Animated.View style={[styles.successContainer, successAnimatedStyle]}>
|
||||
<Icon name="check-circle" size={64} color={theme.custom.colors.success} />
|
||||
<Text style={styles.successText}>Paiement initié avec succès !</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
{/* Informations de sécurité */}
|
||||
<Card style={styles.securityCard}>
|
||||
<View style={styles.securityHeader}>
|
||||
<Icon name="shield-check" size={24} color={theme.custom.colors.success} />
|
||||
<Text style={styles.securityTitle}>Paiement sécurisé</Text>
|
||||
</View>
|
||||
<Text style={styles.securityText}>
|
||||
Vos informations sont protégées par un chiffrement de niveau bancaire.
|
||||
Wave Money est agréé par la BCEAO pour les services de paiement mobile.
|
||||
</Text>
|
||||
</Card>
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retourne le libellé du type de paiement
|
||||
*/
|
||||
const getPaymentTypeLabel = (type: string): string => {
|
||||
switch (type) {
|
||||
case 'cotisation': return 'Cotisation';
|
||||
case 'adhesion': return 'Adhésion';
|
||||
case 'aide': return 'Aide mutuelle';
|
||||
case 'evenement': return 'Événement';
|
||||
default: return 'Paiement';
|
||||
}
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.background,
|
||||
padding: theme.custom.spacing.md,
|
||||
},
|
||||
|
||||
waveCard: {
|
||||
backgroundColor: theme.custom.colors.wave,
|
||||
marginBottom: theme.custom.spacing.md,
|
||||
borderRadius: theme.custom.dimensions.borderRadius.lg,
|
||||
},
|
||||
|
||||
waveHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: theme.custom.spacing.lg,
|
||||
},
|
||||
|
||||
waveInfo: {
|
||||
flex: 1,
|
||||
marginLeft: theme.custom.spacing.md,
|
||||
},
|
||||
|
||||
waveTitle: {
|
||||
fontSize: theme.custom.typography.sizes.xl,
|
||||
fontWeight: theme.custom.typography.weights.bold,
|
||||
color: theme.custom.colors.textOnPrimary,
|
||||
},
|
||||
|
||||
waveSubtitle: {
|
||||
fontSize: theme.custom.typography.sizes.sm,
|
||||
color: theme.custom.colors.textOnPrimary,
|
||||
opacity: 0.8,
|
||||
},
|
||||
|
||||
chip: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
},
|
||||
|
||||
chipText: {
|
||||
color: theme.custom.colors.textOnPrimary,
|
||||
fontSize: theme.custom.typography.sizes.xs,
|
||||
},
|
||||
|
||||
detailsCard: {
|
||||
marginBottom: theme.custom.spacing.md,
|
||||
padding: theme.custom.spacing.lg,
|
||||
},
|
||||
|
||||
formCard: {
|
||||
marginBottom: theme.custom.spacing.md,
|
||||
padding: theme.custom.spacing.lg,
|
||||
},
|
||||
|
||||
sectionTitle: {
|
||||
fontSize: theme.custom.typography.sizes.lg,
|
||||
fontWeight: theme.custom.typography.weights.semibold,
|
||||
color: theme.custom.colors.text,
|
||||
marginBottom: theme.custom.spacing.md,
|
||||
},
|
||||
|
||||
detailRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.custom.spacing.sm,
|
||||
},
|
||||
|
||||
detailLabel: {
|
||||
fontSize: theme.custom.typography.sizes.base,
|
||||
color: theme.custom.colors.textSecondary,
|
||||
},
|
||||
|
||||
detailValue: {
|
||||
fontSize: theme.custom.typography.sizes.base,
|
||||
fontWeight: theme.custom.typography.weights.medium,
|
||||
color: theme.custom.colors.text,
|
||||
},
|
||||
|
||||
feesContainer: {
|
||||
marginTop: theme.custom.spacing.sm,
|
||||
},
|
||||
|
||||
feesValue: {
|
||||
fontSize: theme.custom.typography.sizes.base,
|
||||
fontWeight: theme.custom.typography.weights.medium,
|
||||
color: theme.custom.colors.warning,
|
||||
},
|
||||
|
||||
totalLabel: {
|
||||
fontSize: theme.custom.typography.sizes.lg,
|
||||
fontWeight: theme.custom.typography.weights.semibold,
|
||||
color: theme.custom.colors.text,
|
||||
},
|
||||
|
||||
totalValue: {
|
||||
fontSize: theme.custom.typography.sizes.lg,
|
||||
fontWeight: theme.custom.typography.weights.bold,
|
||||
color: theme.custom.colors.wave,
|
||||
},
|
||||
|
||||
divider: {
|
||||
marginVertical: theme.custom.spacing.sm,
|
||||
},
|
||||
|
||||
loadingContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: theme.custom.spacing.md,
|
||||
},
|
||||
|
||||
loadingText: {
|
||||
marginLeft: theme.custom.spacing.sm,
|
||||
color: theme.custom.colors.textSecondary,
|
||||
},
|
||||
|
||||
input: {
|
||||
marginBottom: theme.custom.spacing.md,
|
||||
},
|
||||
|
||||
errorText: {
|
||||
color: theme.custom.colors.error,
|
||||
fontSize: theme.custom.typography.sizes.sm,
|
||||
marginTop: -theme.custom.spacing.sm,
|
||||
marginBottom: theme.custom.spacing.sm,
|
||||
},
|
||||
|
||||
payButton: {
|
||||
backgroundColor: theme.custom.colors.wave,
|
||||
marginTop: theme.custom.spacing.md,
|
||||
borderRadius: theme.custom.dimensions.borderRadius.md,
|
||||
},
|
||||
|
||||
payButtonContent: {
|
||||
height: theme.custom.dimensions.buttonHeights.lg,
|
||||
},
|
||||
|
||||
payButtonLabel: {
|
||||
fontSize: theme.custom.typography.sizes.lg,
|
||||
fontWeight: theme.custom.typography.weights.semibold,
|
||||
},
|
||||
|
||||
successContainer: {
|
||||
alignItems: 'center',
|
||||
padding: theme.custom.spacing.xl,
|
||||
},
|
||||
|
||||
successText: {
|
||||
fontSize: theme.custom.typography.sizes.lg,
|
||||
fontWeight: theme.custom.typography.weights.semibold,
|
||||
color: theme.custom.colors.success,
|
||||
marginTop: theme.custom.spacing.md,
|
||||
textAlign: 'center',
|
||||
},
|
||||
|
||||
securityCard: {
|
||||
backgroundColor: theme.custom.colors.success + '10',
|
||||
padding: theme.custom.spacing.lg,
|
||||
marginTop: theme.custom.spacing.md,
|
||||
},
|
||||
|
||||
securityHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.custom.spacing.sm,
|
||||
},
|
||||
|
||||
securityTitle: {
|
||||
fontSize: theme.custom.typography.sizes.base,
|
||||
fontWeight: theme.custom.typography.weights.semibold,
|
||||
color: theme.custom.colors.success,
|
||||
marginLeft: theme.custom.spacing.sm,
|
||||
},
|
||||
|
||||
securityText: {
|
||||
fontSize: theme.custom.typography.sizes.sm,
|
||||
color: theme.custom.colors.textSecondary,
|
||||
lineHeight: theme.custom.typography.lineHeights.relaxed * theme.custom.typography.sizes.sm,
|
||||
},
|
||||
});
|
||||
|
||||
export default WavePaymentScreen;
|
||||
433
unionflow-mobile-apps/src/services/WavePaymentService.ts
Normal file
433
unionflow-mobile-apps/src/services/WavePaymentService.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import NetInfo from '@react-native-community/netinfo';
|
||||
import { ApiService } from './ApiService';
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
|
||||
/**
|
||||
* Service de paiement Wave Money pour l'application mobile UnionFlow
|
||||
*
|
||||
* Ce service gère toutes les interactions avec l'API Wave Money :
|
||||
* - Initiation des paiements
|
||||
* - Vérification du statut des transactions
|
||||
* - Calcul des frais
|
||||
* - Gestion hors ligne avec synchronisation
|
||||
* - Cache des données pour performance
|
||||
*
|
||||
* @author Lions Dev Team
|
||||
* @version 2.0.0
|
||||
* @since 2025-01-16
|
||||
*/
|
||||
|
||||
export interface WavePaymentRequest {
|
||||
type: 'cotisation' | 'adhesion' | 'aide' | 'evenement';
|
||||
amount: string;
|
||||
phoneNumber: string;
|
||||
description: string;
|
||||
metadata?: any;
|
||||
}
|
||||
|
||||
export interface WavePaymentResult {
|
||||
success: boolean;
|
||||
transactionId?: string;
|
||||
waveTransactionId?: string;
|
||||
status?: 'SUCCES' | 'EN_ATTENTE' | 'ECHEC';
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface WaveTransactionStatus {
|
||||
transactionId: string;
|
||||
status: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface WaveFees {
|
||||
base: string;
|
||||
fees: string;
|
||||
total: string;
|
||||
}
|
||||
|
||||
class WavePaymentServiceClass {
|
||||
private readonly CACHE_KEY = 'wave_payment_cache';
|
||||
private readonly PENDING_PAYMENTS_KEY = 'pending_wave_payments';
|
||||
private readonly FEES_CACHE_KEY = 'wave_fees_cache';
|
||||
private readonly CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
/**
|
||||
* Initie un paiement Wave Money
|
||||
*/
|
||||
async initiatePayment(request: WavePaymentRequest): Promise<WavePaymentResult> {
|
||||
try {
|
||||
// Vérifier la connectivité
|
||||
const netInfo = await NetInfo.fetch();
|
||||
|
||||
if (!netInfo.isConnected) {
|
||||
// Mode hors ligne - sauvegarder pour synchronisation ultérieure
|
||||
await this.savePendingPayment(request);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Pas de connexion internet. Le paiement sera traité dès que la connexion sera rétablie.',
|
||||
};
|
||||
}
|
||||
|
||||
// Valider la demande
|
||||
this.validatePaymentRequest(request);
|
||||
|
||||
// Préparer les données pour l'API
|
||||
const apiRequest = this.prepareApiRequest(request);
|
||||
|
||||
// Appeler l'API selon le type de paiement
|
||||
let response;
|
||||
switch (request.type) {
|
||||
case 'cotisation':
|
||||
response = await ApiService.post('/api/v1/payments/wave/cotisation', apiRequest);
|
||||
break;
|
||||
case 'adhesion':
|
||||
response = await ApiService.post('/api/v1/payments/wave/adhesion', apiRequest);
|
||||
break;
|
||||
case 'aide':
|
||||
response = await ApiService.post('/api/v1/payments/wave/aide-mutuelle', apiRequest);
|
||||
break;
|
||||
case 'evenement':
|
||||
response = await ApiService.post('/api/v1/payments/wave/evenement', apiRequest);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Type de paiement non supporté');
|
||||
}
|
||||
|
||||
// Traiter la réponse
|
||||
const result = this.processPaymentResponse(response);
|
||||
|
||||
// Sauvegarder en cache pour consultation ultérieure
|
||||
await this.cachePaymentResult(request, result);
|
||||
|
||||
// Analytics
|
||||
AnalyticsService.trackEvent('wave_payment_initiated', {
|
||||
type: request.type,
|
||||
amount: request.amount,
|
||||
success: result.success,
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur initiation paiement Wave:', error);
|
||||
|
||||
AnalyticsService.trackEvent('wave_payment_error', {
|
||||
type: request.type,
|
||||
amount: request.amount,
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: this.getErrorMessage(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie le statut d'une transaction Wave
|
||||
*/
|
||||
async checkTransactionStatus(transactionId: string): Promise<WaveTransactionStatus> {
|
||||
try {
|
||||
const response = await ApiService.get(`/api/v1/payments/wave/status/${transactionId}`);
|
||||
|
||||
return {
|
||||
transactionId: response.transactionId,
|
||||
status: response.status,
|
||||
message: response.message,
|
||||
timestamp: response.timestamp,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur vérification statut:', error);
|
||||
throw new Error('Impossible de vérifier le statut de la transaction');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les frais Wave Money pour un montant donné
|
||||
*/
|
||||
async calculateFees(amount: string): Promise<WaveFees> {
|
||||
try {
|
||||
// Vérifier le cache d'abord
|
||||
const cachedFees = await this.getCachedFees(amount);
|
||||
if (cachedFees) {
|
||||
return cachedFees;
|
||||
}
|
||||
|
||||
const response = await ApiService.get(`/api/v1/payments/wave/fees?montant=${amount}`);
|
||||
|
||||
const fees: WaveFees = {
|
||||
base: response.montantBase,
|
||||
fees: response.frais,
|
||||
total: response.montantTotal,
|
||||
};
|
||||
|
||||
// Mettre en cache
|
||||
await this.cacheFees(amount, fees);
|
||||
|
||||
return fees;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur calcul frais:', error);
|
||||
|
||||
// Calcul local en cas d'erreur API
|
||||
return this.calculateFeesLocally(amount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise les paiements en attente
|
||||
*/
|
||||
async syncPendingPayments(): Promise<void> {
|
||||
try {
|
||||
const pendingPayments = await this.getPendingPayments();
|
||||
|
||||
if (pendingPayments.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Synchronisation de ${pendingPayments.length} paiements en attente`);
|
||||
|
||||
for (const payment of pendingPayments) {
|
||||
try {
|
||||
const result = await this.initiatePayment(payment.request);
|
||||
|
||||
if (result.success) {
|
||||
// Supprimer de la liste des paiements en attente
|
||||
await this.removePendingPayment(payment.id);
|
||||
|
||||
// Notifier l'utilisateur du succès
|
||||
// NotificationService.showLocalNotification({
|
||||
// title: 'Paiement synchronisé',
|
||||
// body: `Votre paiement de ${payment.request.amount} FCFA a été traité avec succès.`,
|
||||
// });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur synchronisation paiement:', error);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur synchronisation générale:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'historique des paiements Wave
|
||||
*/
|
||||
async getPaymentHistory(): Promise<any[]> {
|
||||
try {
|
||||
const cached = await AsyncStorage.getItem(this.CACHE_KEY);
|
||||
if (cached) {
|
||||
const data = JSON.parse(cached);
|
||||
return data.payments || [];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('Erreur récupération historique:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES PRIVÉES ====================
|
||||
|
||||
private validatePaymentRequest(request: WavePaymentRequest): void {
|
||||
if (!request.amount || parseFloat(request.amount) <= 0) {
|
||||
throw new Error('Montant invalide');
|
||||
}
|
||||
|
||||
if (parseFloat(request.amount) < 100) {
|
||||
throw new Error('Montant minimum : 100 FCFA');
|
||||
}
|
||||
|
||||
if (parseFloat(request.amount) > 1000000) {
|
||||
throw new Error('Montant maximum : 1,000,000 FCFA');
|
||||
}
|
||||
|
||||
if (!request.phoneNumber || !this.isValidWaveNumber(request.phoneNumber)) {
|
||||
throw new Error('Numéro Wave invalide');
|
||||
}
|
||||
|
||||
if (!request.description) {
|
||||
throw new Error('Description obligatoire');
|
||||
}
|
||||
}
|
||||
|
||||
private isValidWaveNumber(phoneNumber: string): boolean {
|
||||
// Validation des numéros Wave CI : +225 suivi de 8 chiffres
|
||||
const wavePattern = /^\+225[0-9]{8}$/;
|
||||
return wavePattern.test(phoneNumber);
|
||||
}
|
||||
|
||||
private prepareApiRequest(request: WavePaymentRequest): any {
|
||||
return {
|
||||
montant: request.amount,
|
||||
numeroTelephone: request.phoneNumber,
|
||||
description: request.description,
|
||||
metadata: {
|
||||
...request.metadata,
|
||||
source: 'mobile_app',
|
||||
version: '2.0.0',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private processPaymentResponse(response: any): WavePaymentResult {
|
||||
if (response.transactionId) {
|
||||
return {
|
||||
success: true,
|
||||
transactionId: response.transactionId,
|
||||
waveTransactionId: response.waveTransactionId,
|
||||
status: response.statut,
|
||||
message: response.message,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: response.message || 'Erreur lors du paiement',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async savePendingPayment(request: WavePaymentRequest): Promise<void> {
|
||||
try {
|
||||
const pendingPayments = await this.getPendingPayments();
|
||||
|
||||
const newPayment = {
|
||||
id: Date.now().toString(),
|
||||
request,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
pendingPayments.push(newPayment);
|
||||
|
||||
await AsyncStorage.setItem(
|
||||
this.PENDING_PAYMENTS_KEY,
|
||||
JSON.stringify(pendingPayments)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Erreur sauvegarde paiement en attente:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async getPendingPayments(): Promise<any[]> {
|
||||
try {
|
||||
const stored = await AsyncStorage.getItem(this.PENDING_PAYMENTS_KEY);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
} catch (error) {
|
||||
console.error('Erreur récupération paiements en attente:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async removePendingPayment(paymentId: string): Promise<void> {
|
||||
try {
|
||||
const pendingPayments = await this.getPendingPayments();
|
||||
const filtered = pendingPayments.filter(p => p.id !== paymentId);
|
||||
|
||||
await AsyncStorage.setItem(
|
||||
this.PENDING_PAYMENTS_KEY,
|
||||
JSON.stringify(filtered)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Erreur suppression paiement en attente:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async cachePaymentResult(request: WavePaymentRequest, result: WavePaymentResult): Promise<void> {
|
||||
try {
|
||||
const cached = await AsyncStorage.getItem(this.CACHE_KEY);
|
||||
const data = cached ? JSON.parse(cached) : { payments: [] };
|
||||
|
||||
data.payments.unshift({
|
||||
request,
|
||||
result,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Garder seulement les 50 derniers paiements
|
||||
data.payments = data.payments.slice(0, 50);
|
||||
|
||||
await AsyncStorage.setItem(this.CACHE_KEY, JSON.stringify(data));
|
||||
} catch (error) {
|
||||
console.error('Erreur cache paiement:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async getCachedFees(amount: string): Promise<WaveFees | null> {
|
||||
try {
|
||||
const cached = await AsyncStorage.getItem(this.FEES_CACHE_KEY);
|
||||
if (!cached) return null;
|
||||
|
||||
const data = JSON.parse(cached);
|
||||
const entry = data[amount];
|
||||
|
||||
if (entry && Date.now() - entry.timestamp < this.CACHE_DURATION) {
|
||||
return entry.fees;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Erreur récupération cache frais:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async cacheFees(amount: string, fees: WaveFees): Promise<void> {
|
||||
try {
|
||||
const cached = await AsyncStorage.getItem(this.FEES_CACHE_KEY);
|
||||
const data = cached ? JSON.parse(cached) : {};
|
||||
|
||||
data[amount] = {
|
||||
fees,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
await AsyncStorage.setItem(this.FEES_CACHE_KEY, JSON.stringify(data));
|
||||
} catch (error) {
|
||||
console.error('Erreur cache frais:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private calculateFeesLocally(amount: string): WaveFees {
|
||||
const montant = parseFloat(amount);
|
||||
let frais: number;
|
||||
|
||||
// Barème Wave Money Côte d'Ivoire
|
||||
if (montant <= 1000) {
|
||||
frais = 25;
|
||||
} else if (montant <= 5000) {
|
||||
frais = 50;
|
||||
} else if (montant <= 25000) {
|
||||
frais = montant * 0.01; // 1%
|
||||
} else {
|
||||
frais = 250; // Plafond
|
||||
}
|
||||
|
||||
const total = montant + frais;
|
||||
|
||||
return {
|
||||
base: `${montant.toLocaleString()} FCFA`,
|
||||
fees: `${frais.toLocaleString()} FCFA`,
|
||||
total: `${total.toLocaleString()} FCFA`,
|
||||
};
|
||||
}
|
||||
|
||||
private getErrorMessage(error: any): string {
|
||||
if (error.response?.data?.message) {
|
||||
return error.response.data.message;
|
||||
}
|
||||
|
||||
if (error.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return 'Une erreur inattendue est survenue';
|
||||
}
|
||||
}
|
||||
|
||||
export const WavePaymentService = new WavePaymentServiceClass();
|
||||
358
unionflow-mobile-apps/src/theme/theme.ts
Normal file
358
unionflow-mobile-apps/src/theme/theme.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import { DefaultTheme } from 'react-native-paper';
|
||||
import { Dimensions, Platform } from 'react-native';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* Design System UnionFlow - Thème moderne pour l'application mobile
|
||||
*
|
||||
* Ce thème définit l'identité visuelle complète de l'application avec :
|
||||
* - Palette de couleurs inspirée des couleurs de la Côte d'Ivoire
|
||||
* - Typographie moderne et lisible
|
||||
* - Espacements cohérents
|
||||
* - Composants réutilisables
|
||||
* - Support du mode sombre
|
||||
* - Adaptation aux différentes tailles d'écran
|
||||
*
|
||||
* @author Lions Dev Team
|
||||
* @version 2.0.0
|
||||
* @since 2025-01-16
|
||||
*/
|
||||
|
||||
// ==================== COULEURS ====================
|
||||
|
||||
export const colors = {
|
||||
// Couleurs principales inspirées du drapeau ivoirien
|
||||
primary: '#FF8C00', // Orange vif (drapeau CI)
|
||||
primaryDark: '#E67E00', // Orange foncé
|
||||
primaryLight: '#FFB347', // Orange clair
|
||||
|
||||
secondary: '#228B22', // Vert (drapeau CI)
|
||||
secondaryDark: '#1F7A1F', // Vert foncé
|
||||
secondaryLight: '#90EE90', // Vert clair
|
||||
|
||||
accent: '#FFD700', // Or/Jaune
|
||||
|
||||
// Couleurs Wave Money
|
||||
wave: '#1E3A8A', // Bleu Wave
|
||||
waveDark: '#1E40AF', // Bleu Wave foncé
|
||||
waveLight: '#3B82F6', // Bleu Wave clair
|
||||
|
||||
// Couleurs système
|
||||
background: '#FFFFFF', // Blanc
|
||||
surface: '#F8F9FA', // Gris très clair
|
||||
card: '#FFFFFF', // Blanc pour les cartes
|
||||
|
||||
// Couleurs de texte
|
||||
text: '#1A1A1A', // Noir principal
|
||||
textSecondary: '#6B7280', // Gris moyen
|
||||
textLight: '#9CA3AF', // Gris clair
|
||||
textOnPrimary: '#FFFFFF', // Blanc sur primary
|
||||
|
||||
// Couleurs d'état
|
||||
success: '#10B981', // Vert succès
|
||||
warning: '#F59E0B', // Orange warning
|
||||
error: '#EF4444', // Rouge erreur
|
||||
info: '#3B82F6', // Bleu info
|
||||
|
||||
// Couleurs de bordure
|
||||
border: '#E5E7EB', // Gris bordure
|
||||
borderLight: '#F3F4F6', // Gris bordure clair
|
||||
borderDark: '#D1D5DB', // Gris bordure foncé
|
||||
|
||||
// Couleurs d'ombre
|
||||
shadow: '#000000',
|
||||
shadowLight: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowMedium: 'rgba(0, 0, 0, 0.15)',
|
||||
shadowDark: 'rgba(0, 0, 0, 0.25)',
|
||||
|
||||
// Couleurs transparentes
|
||||
overlay: 'rgba(0, 0, 0, 0.5)',
|
||||
overlayLight: 'rgba(0, 0, 0, 0.3)',
|
||||
|
||||
// Couleurs spécifiques métier
|
||||
cotisation: '#10B981', // Vert pour cotisations
|
||||
adhesion: '#3B82F6', // Bleu pour adhésions
|
||||
aideMutuelle: '#F59E0B', // Orange pour aide mutuelle
|
||||
evenement: '#8B5CF6', // Violet pour événements
|
||||
};
|
||||
|
||||
// ==================== TYPOGRAPHIE ====================
|
||||
|
||||
export const typography = {
|
||||
// Tailles de police
|
||||
sizes: {
|
||||
xs: 12,
|
||||
sm: 14,
|
||||
base: 16,
|
||||
lg: 18,
|
||||
xl: 20,
|
||||
'2xl': 24,
|
||||
'3xl': 30,
|
||||
'4xl': 36,
|
||||
'5xl': 48,
|
||||
},
|
||||
|
||||
// Poids de police
|
||||
weights: {
|
||||
light: '300' as const,
|
||||
normal: '400' as const,
|
||||
medium: '500' as const,
|
||||
semibold: '600' as const,
|
||||
bold: '700' as const,
|
||||
extrabold: '800' as const,
|
||||
},
|
||||
|
||||
// Familles de police
|
||||
families: {
|
||||
regular: Platform.OS === 'ios' ? 'SF Pro Display' : 'Roboto',
|
||||
medium: Platform.OS === 'ios' ? 'SF Pro Display Medium' : 'Roboto Medium',
|
||||
bold: Platform.OS === 'ios' ? 'SF Pro Display Bold' : 'Roboto Bold',
|
||||
},
|
||||
|
||||
// Hauteurs de ligne
|
||||
lineHeights: {
|
||||
tight: 1.2,
|
||||
normal: 1.4,
|
||||
relaxed: 1.6,
|
||||
loose: 1.8,
|
||||
},
|
||||
};
|
||||
|
||||
// ==================== ESPACEMENTS ====================
|
||||
|
||||
export const spacing = {
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 16,
|
||||
lg: 24,
|
||||
xl: 32,
|
||||
'2xl': 48,
|
||||
'3xl': 64,
|
||||
'4xl': 96,
|
||||
};
|
||||
|
||||
// ==================== DIMENSIONS ====================
|
||||
|
||||
export const dimensions = {
|
||||
screen: {
|
||||
width,
|
||||
height,
|
||||
},
|
||||
|
||||
// Tailles d'écran
|
||||
isSmallScreen: width < 375,
|
||||
isMediumScreen: width >= 375 && width < 414,
|
||||
isLargeScreen: width >= 414,
|
||||
|
||||
// Hauteurs communes
|
||||
headerHeight: Platform.OS === 'ios' ? 88 : 56,
|
||||
tabBarHeight: Platform.OS === 'ios' ? 83 : 56,
|
||||
statusBarHeight: Platform.OS === 'ios' ? 44 : 24,
|
||||
|
||||
// Rayons de bordure
|
||||
borderRadius: {
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 12,
|
||||
lg: 16,
|
||||
xl: 24,
|
||||
full: 9999,
|
||||
},
|
||||
|
||||
// Tailles d'icônes
|
||||
iconSizes: {
|
||||
xs: 16,
|
||||
sm: 20,
|
||||
md: 24,
|
||||
lg: 32,
|
||||
xl: 48,
|
||||
},
|
||||
|
||||
// Tailles de boutons
|
||||
buttonHeights: {
|
||||
sm: 36,
|
||||
md: 44,
|
||||
lg: 52,
|
||||
},
|
||||
};
|
||||
|
||||
// ==================== OMBRES ====================
|
||||
|
||||
export const shadows = {
|
||||
none: {
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0,
|
||||
shadowRadius: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
|
||||
sm: {
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.18,
|
||||
shadowRadius: 1.0,
|
||||
elevation: 1,
|
||||
},
|
||||
|
||||
md: {
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.23,
|
||||
shadowRadius: 2.62,
|
||||
elevation: 4,
|
||||
},
|
||||
|
||||
lg: {
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.30,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
},
|
||||
|
||||
xl: {
|
||||
shadowOffset: { width: 0, height: 6 },
|
||||
shadowOpacity: 0.37,
|
||||
shadowRadius: 7.49,
|
||||
elevation: 12,
|
||||
},
|
||||
};
|
||||
|
||||
// ==================== ANIMATIONS ====================
|
||||
|
||||
export const animations = {
|
||||
durations: {
|
||||
fast: 150,
|
||||
normal: 250,
|
||||
slow: 350,
|
||||
},
|
||||
|
||||
easings: {
|
||||
easeInOut: 'ease-in-out',
|
||||
easeIn: 'ease-in',
|
||||
easeOut: 'ease-out',
|
||||
linear: 'linear',
|
||||
},
|
||||
};
|
||||
|
||||
// ==================== THÈME REACT NATIVE PAPER ====================
|
||||
|
||||
export const theme = {
|
||||
...DefaultTheme,
|
||||
|
||||
colors: {
|
||||
...DefaultTheme.colors,
|
||||
primary: colors.primary,
|
||||
accent: colors.accent,
|
||||
background: colors.background,
|
||||
surface: colors.surface,
|
||||
text: colors.text,
|
||||
onSurface: colors.text,
|
||||
disabled: colors.textLight,
|
||||
placeholder: colors.textSecondary,
|
||||
backdrop: colors.overlay,
|
||||
notification: colors.error,
|
||||
},
|
||||
|
||||
fonts: {
|
||||
...DefaultTheme.fonts,
|
||||
regular: {
|
||||
fontFamily: typography.families.regular,
|
||||
fontWeight: typography.weights.normal,
|
||||
},
|
||||
medium: {
|
||||
fontFamily: typography.families.medium,
|
||||
fontWeight: typography.weights.medium,
|
||||
},
|
||||
light: {
|
||||
fontFamily: typography.families.regular,
|
||||
fontWeight: typography.weights.light,
|
||||
},
|
||||
thin: {
|
||||
fontFamily: typography.families.regular,
|
||||
fontWeight: typography.weights.light,
|
||||
},
|
||||
},
|
||||
|
||||
roundness: dimensions.borderRadius.md,
|
||||
|
||||
// Extensions personnalisées
|
||||
custom: {
|
||||
colors,
|
||||
typography,
|
||||
spacing,
|
||||
dimensions,
|
||||
shadows,
|
||||
animations,
|
||||
},
|
||||
};
|
||||
|
||||
// ==================== STYLES COMMUNS ====================
|
||||
|
||||
export const commonStyles = {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
|
||||
centerContent: {
|
||||
justifyContent: 'center' as const,
|
||||
alignItems: 'center' as const,
|
||||
},
|
||||
|
||||
row: {
|
||||
flexDirection: 'row' as const,
|
||||
alignItems: 'center' as const,
|
||||
},
|
||||
|
||||
spaceBetween: {
|
||||
flexDirection: 'row' as const,
|
||||
justifyContent: 'space-between' as const,
|
||||
alignItems: 'center' as const,
|
||||
},
|
||||
|
||||
card: {
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: dimensions.borderRadius.md,
|
||||
padding: spacing.md,
|
||||
...shadows.md,
|
||||
},
|
||||
|
||||
button: {
|
||||
borderRadius: dimensions.borderRadius.md,
|
||||
height: dimensions.buttonHeights.md,
|
||||
justifyContent: 'center' as const,
|
||||
alignItems: 'center' as const,
|
||||
},
|
||||
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: dimensions.borderRadius.sm,
|
||||
padding: spacing.md,
|
||||
fontSize: typography.sizes.base,
|
||||
color: colors.text,
|
||||
},
|
||||
|
||||
shadow: shadows.md,
|
||||
|
||||
// Styles spécifiques Wave Money
|
||||
waveCard: {
|
||||
backgroundColor: colors.wave,
|
||||
borderRadius: dimensions.borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
...shadows.lg,
|
||||
},
|
||||
|
||||
waveButton: {
|
||||
backgroundColor: colors.wave,
|
||||
borderRadius: dimensions.borderRadius.md,
|
||||
height: dimensions.buttonHeights.lg,
|
||||
justifyContent: 'center' as const,
|
||||
alignItems: 'center' as const,
|
||||
},
|
||||
};
|
||||
|
||||
export type Theme = typeof theme;
|
||||
export type Colors = typeof colors;
|
||||
export type Typography = typeof typography;
|
||||
export type Spacing = typeof spacing;
|
||||
export type Dimensions = typeof dimensions;
|
||||
Reference in New Issue
Block a user