first commit

This commit is contained in:
DahoudG
2025-08-20 21:00:35 +00:00
commit b2a23bdf89
583 changed files with 243074 additions and 0 deletions

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

View 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,
};
};

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

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

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

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