Files
unionflow-server-impl-quarkus/unionflow-mobile-apps/src/screens/payment/WavePaymentScreen.tsx
2025-08-20 21:00:35 +00:00

601 lines
18 KiB
TypeScript

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;