theme update latest project
This commit is contained in:
@@ -0,0 +1,754 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
Image,
|
||||
ToastAndroid,
|
||||
Modal,
|
||||
Easing,
|
||||
} from 'react-native';
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import { responsiveFontSize } from 'react-native-responsive-dimensions';
|
||||
import LucideIcon from '../../components/common/LucideIcon';
|
||||
import AppView from '../../components/AppView';
|
||||
import { useTheme } from '../../hooks/ui/useTheme';
|
||||
import { apiCall } from '../../utils/ApiHelper';
|
||||
import { BASE_URL, CONTRIBUTE, PAYMENT_MODES_URL } from '../../utils/ApiUrls';
|
||||
import FontsFamily from '../../constents/FontsFamily';
|
||||
import Header from '../../components/common/Header';
|
||||
import { clampRGBA } from 'react-native-reanimated/lib/typescript/Colors';
|
||||
|
||||
interface PaymentMode {
|
||||
id: number;
|
||||
modeCode: string;
|
||||
modeName: string;
|
||||
provider: string;
|
||||
countryCode: string;
|
||||
currencyCode: string;
|
||||
minAmount: number;
|
||||
maxAmount: number;
|
||||
isActive: boolean;
|
||||
isMaintenance: boolean;
|
||||
displayOrder: number;
|
||||
iconUrl: string;
|
||||
}
|
||||
|
||||
// --- Extracted ModeCard Component (hooks must be in a component, not a render function) ---
|
||||
const getBrandColors = (modeCode: string): { bg: string; text: string } => {
|
||||
switch (modeCode) {
|
||||
case 'PAISAPAL': return { bg: '#2BAF2B', text: '#fff' }; // Pesapal green
|
||||
case 'MTN': return { bg: '#FFCC00', text: '#003087' }; // MTN yellow + blue
|
||||
case 'AIRTEL': return { bg: '#ED1C24', text: '#fff' }; // Airtel red
|
||||
default: return { bg: '#6C63FF', text: '#fff' };
|
||||
}
|
||||
};
|
||||
|
||||
interface ModeCardProps {
|
||||
item: PaymentMode;
|
||||
index: number;
|
||||
isSelected: boolean;
|
||||
isDisabled: boolean;
|
||||
onSelect: (id: number) => void;
|
||||
}
|
||||
|
||||
const ModeCard: React.FC<ModeCardProps> = ({ item, index, isSelected, isDisabled, onSelect }) => {
|
||||
const theme = useTheme();
|
||||
const cardAnim = useRef(new Animated.Value(0)).current;
|
||||
|
||||
useEffect(() => {
|
||||
Animated.timing(cardAnim, {
|
||||
toValue: 1,
|
||||
duration: 400,
|
||||
delay: index * 100,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, []);
|
||||
|
||||
const brandColors = getBrandColors(item.modeCode);
|
||||
|
||||
return (
|
||||
<Animated.View style={{ opacity: cardAnim, transform: [{ translateY: cardAnim.interpolate({ inputRange: [0, 1], outputRange: [20, 0] }) }] }}>
|
||||
<TouchableOpacity
|
||||
activeOpacity={isDisabled ? 1 : 0.7}
|
||||
onPress={() => { if (!isDisabled) onSelect(item.id); }}
|
||||
style={[
|
||||
styles.modeCard,
|
||||
{
|
||||
backgroundColor: isSelected ? theme.colors.primary + '12' : theme.colors.card,
|
||||
borderColor: isSelected ? theme.colors.primary : theme.colors.border,
|
||||
opacity: isDisabled ? 0.5 : 1,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* Selection Indicator */}
|
||||
<View style={[styles.radioOuter, { borderColor: isSelected ? theme.colors.primary : theme.colors.textMuted }]}>
|
||||
{isSelected && <View style={[styles.radioInner, { backgroundColor: theme.colors.primary }]} />}
|
||||
</View>
|
||||
|
||||
{/* Provider Brand Icon */}
|
||||
<View style={[styles.modeIconContainer, { backgroundColor: brandColors.bg }]}>
|
||||
<Text style={{ fontSize: responsiveFontSize(2.2), fontFamily: FontsFamily.nunitoExtraBold, color: brandColors.text }}>
|
||||
{item.provider.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Info */}
|
||||
<View style={styles.modeInfo}>
|
||||
<Text style={[styles.modeName, { color: theme.colors.textPrimary }]}>{item.modeName}</Text>
|
||||
<Text style={[styles.modeProvider, { color: theme.colors.textSecondary }]}>
|
||||
{item.provider} • {item.currencyCode}
|
||||
</Text>
|
||||
<Text style={[styles.modeLimits, { color: theme.colors.textMuted }]}>
|
||||
{item.currencyCode} {item.minAmount.toLocaleString()} – {item.maxAmount.toLocaleString()}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Badges */}
|
||||
<View style={styles.badgeContainer}>
|
||||
{item.isMaintenance && (
|
||||
<View style={[styles.badge, { backgroundColor: theme.colors.warning + '22' }]}>
|
||||
<Text style={[styles.badgeText, { color: theme.colors.warning }]}>Maintenance</Text>
|
||||
</View>
|
||||
)}
|
||||
{!item.isActive && !item.isMaintenance && (
|
||||
<View style={[styles.badge, { backgroundColor: theme.colors.error + '22' }]}>
|
||||
<Text style={[styles.badgeText, { color: theme.colors.error }]}>Unavailable</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
// --- Main Screen ---
|
||||
const PaymentModeScreen: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
const navigation = useNavigation();
|
||||
const route = useRoute<any>();
|
||||
const { groupId, routeFrom, routeData = null } = route.params || {};
|
||||
const user = useSelector((state: any) => state.user);
|
||||
|
||||
const [modes, setModes] = useState<PaymentMode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedMode, setSelectedMode] = useState<number | null>(null);
|
||||
const [paying, setPaying] = useState(false);
|
||||
const [payable, setPayable] = useState<any>();
|
||||
// --- Payment processing modal state ---
|
||||
const [processingVisible, setProcessingVisible] = useState(false);
|
||||
const [processingStatus, setProcessingStatus] = useState<'polling' | 'success' | 'failed' | 'timeout'>('polling');
|
||||
const [processingMessage, setProcessingMessage] = useState('Payment request initiated. Awaiting approval...');
|
||||
const [timeRemaining, setTimeRemaining] = useState(360); // 6 minutes in seconds
|
||||
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const countdownRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const spinAnim = useRef(new Animated.Value(0)).current;
|
||||
const spinLoop = useRef<Animated.CompositeAnimation | null>(null);
|
||||
|
||||
// Animations
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
const slideAnim = useRef(new Animated.Value(30)).current;
|
||||
|
||||
useEffect(() => {
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }),
|
||||
Animated.timing(slideAnim, { toValue: 0, duration: 500, useNativeDriver: true }),
|
||||
]).start();
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
{ routeFrom === 'GroupDetails' && handleContribution(); }
|
||||
console.log("data", payable);
|
||||
}, []);
|
||||
const handleContribution = async () => {
|
||||
try {
|
||||
const res = await apiCall(`${CONTRIBUTE}${groupId || route?.params?.groupId}/calculate-contribution`, 'GET') as any;
|
||||
setPayable(res.data);
|
||||
} catch (error) {
|
||||
console.log("Error in handleContribution: ", error);
|
||||
}
|
||||
};
|
||||
// Spin animation for processing modal
|
||||
const startSpinAnimation = () => {
|
||||
spinAnim.setValue(0);
|
||||
spinLoop.current = Animated.loop(
|
||||
Animated.timing(spinAnim, {
|
||||
toValue: 1,
|
||||
duration: 1500,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
);
|
||||
spinLoop.current.start();
|
||||
};
|
||||
|
||||
const stopSpinAnimation = () => {
|
||||
spinLoop.current?.stop();
|
||||
};
|
||||
|
||||
const cleanupPolling = () => {
|
||||
if (pollingRef.current) {
|
||||
clearInterval(pollingRef.current);
|
||||
pollingRef.current = null;
|
||||
}
|
||||
if (countdownRef.current) {
|
||||
clearInterval(countdownRef.current);
|
||||
countdownRef.current = null;
|
||||
}
|
||||
stopSpinAnimation();
|
||||
};
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => cleanupPolling();
|
||||
}, []);
|
||||
|
||||
const pollTransactionStatus = (pgOrderNo: string) => {
|
||||
setProcessingVisible(true);
|
||||
setProcessingStatus('polling');
|
||||
setProcessingMessage('Payment request initiated. Awaiting approval...');
|
||||
setTimeRemaining(360);
|
||||
startSpinAnimation();
|
||||
|
||||
const startTime = Date.now();
|
||||
const MAX_DURATION = 6 * 60 * 1000; // 6 minutes
|
||||
const POLL_INTERVAL = 5000; // 5 seconds
|
||||
|
||||
// Countdown timer
|
||||
countdownRef.current = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const remaining = Math.max(0, Math.ceil((MAX_DURATION - elapsed) / 1000));
|
||||
setTimeRemaining(remaining);
|
||||
}, 1000);
|
||||
|
||||
// Polling interval
|
||||
pollingRef.current = setInterval(async () => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
// Timeout after 6 minutes
|
||||
if (elapsed >= MAX_DURATION) {
|
||||
cleanupPolling();
|
||||
setProcessingStatus('timeout');
|
||||
setProcessingMessage('Payment verification timed out. Please check your transaction history.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const statusRes = await apiCall(
|
||||
`${BASE_URL}payments/pg/check-status/${pgOrderNo}`,
|
||||
'GET'
|
||||
) as any;
|
||||
|
||||
console.log('Poll status response:', JSON.stringify(statusRes));
|
||||
|
||||
const txnStatus = statusRes?.data?.txnStatus;
|
||||
|
||||
if (txnStatus === 'SUCCESS') {
|
||||
cleanupPolling();
|
||||
setProcessingStatus('success');
|
||||
setProcessingMessage('Payment completed successfully!');
|
||||
setTimeout(() => {
|
||||
setProcessingVisible(false);
|
||||
setPaying(false);
|
||||
ToastAndroid.show('Payment successful!', ToastAndroid.SHORT);
|
||||
navigation.goBack();
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (txnStatus === 'FAILED' || txnStatus === 'REJECTED' || txnStatus === 'CANCELLED') {
|
||||
cleanupPolling();
|
||||
setProcessingStatus('failed');
|
||||
setProcessingMessage(statusRes?.data?.message || 'Payment failed. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Still pending — update message
|
||||
setProcessingMessage(statusRes?.message || 'Waiting for payment confirmation...');
|
||||
} catch (err) {
|
||||
console.log('Poll error:', err);
|
||||
// Don't stop polling on network errors, keep trying
|
||||
}
|
||||
}, POLL_INTERVAL);
|
||||
};
|
||||
|
||||
const handleCloseProcessingModal = () => {
|
||||
cleanupPolling();
|
||||
setProcessingVisible(false);
|
||||
setPaying(false);
|
||||
};
|
||||
|
||||
const fetchPaymentModes = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await apiCall(PAYMENT_MODES_URL, 'GET') as any;
|
||||
if (res?.status === 'success' && Array.isArray(res.data)) {
|
||||
const sorted = res.data.sort((a: PaymentMode, b: PaymentMode) => a.displayOrder - b.displayOrder);
|
||||
setModes(sorted);
|
||||
} else {
|
||||
ToastAndroid.show('Failed to load payment methods', ToastAndroid.SHORT);
|
||||
}
|
||||
} catch (error) {
|
||||
ToastAndroid.show('Error loading payment methods', ToastAndroid.SHORT);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPaymentModes();
|
||||
}, [fetchPaymentModes]);
|
||||
const handlePaymentForRouteFormGroupScreen = async () => {
|
||||
try {
|
||||
|
||||
setPaying(true);
|
||||
const payload = {
|
||||
groupId: groupId || 0,
|
||||
paymentId: selectedMode || 0,
|
||||
mobileNumber: '',
|
||||
};
|
||||
console.log('payload', JSON.stringify(payload));
|
||||
|
||||
const res = await apiCall(
|
||||
`${BASE_URL}payments/pg/entry-fee/${groupId}?groupId=${groupId}&paymentId=${selectedMode}&mobileNumber=`,
|
||||
'POST',
|
||||
payload
|
||||
) as any;
|
||||
|
||||
if (res?.status === 'success' || res?.statusCode === 200) {
|
||||
const pgOrderNo = res?.data?.pgOrderNo;
|
||||
if (pgOrderNo) {
|
||||
// Start polling for transaction status
|
||||
pollTransactionStatus(pgOrderNo);
|
||||
} else {
|
||||
ToastAndroid.show('Payment initiated but no order number received', ToastAndroid.SHORT);
|
||||
setPaying(false);
|
||||
}
|
||||
} else {
|
||||
ToastAndroid.show(res?.data?.message || res?.message || 'Payment failed', ToastAndroid.SHORT);
|
||||
setPaying(false);
|
||||
}
|
||||
} catch (error) {
|
||||
ToastAndroid.show('An error occurred during payment', ToastAndroid.SHORT);
|
||||
setPaying(false);
|
||||
}
|
||||
}
|
||||
const handlePaymentForRouteFormGroupDetails = async () => {
|
||||
try {
|
||||
|
||||
setPaying(true);
|
||||
const payload = {
|
||||
groupId: groupId || 0,
|
||||
amount: payable.totalPayable || 0,
|
||||
paymentId: selectedMode || 0,
|
||||
mobileNumber: '',
|
||||
};
|
||||
console.log('payload', JSON.stringify(payload));
|
||||
|
||||
const res = await apiCall(
|
||||
`${BASE_URL}payments/contribute`,
|
||||
'POST',
|
||||
payload
|
||||
) as any;
|
||||
|
||||
if (res?.status === 'success' || res?.statusCode === 200) {
|
||||
const pgOrderNo = res?.data?.pgOrderNo;
|
||||
if (pgOrderNo) {
|
||||
// Start polling for transaction status
|
||||
pollTransactionStatus(pgOrderNo);
|
||||
} else {
|
||||
ToastAndroid.show('Payment initiated but no order number received', ToastAndroid.SHORT);
|
||||
setPaying(false);
|
||||
}
|
||||
} else {
|
||||
ToastAndroid.show(res?.data?.message || res?.message || 'Payment failed', ToastAndroid.SHORT);
|
||||
setPaying(false);
|
||||
}
|
||||
} catch (error) {
|
||||
ToastAndroid.show('An error occurred during payment', ToastAndroid.SHORT);
|
||||
setPaying(false);
|
||||
}
|
||||
}
|
||||
const handleConfirmPayment = async () => {
|
||||
if (!selectedMode) {
|
||||
ToastAndroid.show('Please select a payment method', ToastAndroid.SHORT);
|
||||
return;
|
||||
}
|
||||
switch (routeFrom) {
|
||||
case 'GroupDetails':
|
||||
handlePaymentForRouteFormGroupDetails();
|
||||
break;
|
||||
case 'GroupScreen':
|
||||
handlePaymentForRouteFormGroupScreen();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<AppView style={{ flex: 1, backgroundColor: theme.colors.background }}>
|
||||
<Header title="Select Payment Method" />
|
||||
|
||||
<Animated.View style={[styles.container, { opacity: fadeAnim, transform: [{ translateY: slideAnim }] }]}>
|
||||
{/* Amount Summary */}
|
||||
{routeFrom === 'GroupDetails' && <View style={[styles.amountCard, { backgroundColor: theme.colors.card, borderColor: theme.colors.border }]}>
|
||||
<View style={styles.amountRow}>
|
||||
<LucideIcon name="wallet-outline" size={20} color={theme.colors.primary} />
|
||||
<Text style={[styles.amountLabel, { color: theme.colors.textSecondary }]}>Total Payable</Text>
|
||||
</View>
|
||||
<Text style={[styles.amountValue, { color: theme.colors.textPrimary }]}>
|
||||
{payable?.currencyCode || 'UGX'} {Number(payable?.totalPayable || 0).toLocaleString()}
|
||||
</Text>
|
||||
</View>}
|
||||
{routeFrom === 'GroupScreen' && <View style={[styles.amountCard, { backgroundColor: theme.colors.card, borderColor: theme.colors.border }]}>
|
||||
<View style={styles.amountRow}>
|
||||
<LucideIcon name="wallet-outline" size={20} color={theme.colors.primary} />
|
||||
<Text style={[styles.amountLabel, { color: theme.colors.textSecondary }]}>Entry Fee</Text>
|
||||
</View>
|
||||
<Text style={[styles.amountValue, { color: theme.colors.textPrimary }]}>
|
||||
{payable?.currencyCode || 'UGX'} {Number(routeData?.entryFee || 0).toLocaleString()}
|
||||
</Text>
|
||||
</View>}
|
||||
|
||||
{/* Payment Methods */}
|
||||
{loading ? (
|
||||
<View style={styles.loaderContainer}>
|
||||
<ActivityIndicator size="large" color={theme.colors.primary} />
|
||||
<Text style={[styles.loaderText, { color: theme.colors.textMuted }]}>Loading payment methods...</Text>
|
||||
</View>
|
||||
) : modes.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<LucideIcon name="alert-circle-outline" size={48} color={theme.colors.textMuted} />
|
||||
<Text style={[styles.emptyText, { color: theme.colors.textMuted }]}>No payment methods available</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={modes}
|
||||
keyExtractor={(item) => String(item.id)}
|
||||
renderItem={({ item, index }) => (
|
||||
<ModeCard
|
||||
item={item}
|
||||
index={index}
|
||||
isSelected={selectedMode === item.id}
|
||||
isDisabled={!item.isActive || item.isMaintenance}
|
||||
onSelect={setSelectedMode}
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
{!loading && modes.length > 0 && (
|
||||
<View style={[styles.bottomBar, { backgroundColor: theme.colors.background, borderTopColor: theme.colors.border }]}>
|
||||
<TouchableOpacity
|
||||
onPress={handleConfirmPayment}
|
||||
disabled={!selectedMode || paying}
|
||||
activeOpacity={0.8}
|
||||
style={[
|
||||
styles.confirmButton,
|
||||
{
|
||||
backgroundColor: selectedMode ? theme.colors.primary : theme.colors.textMuted,
|
||||
opacity: paying ? 0.7 : 1,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{paying ? (
|
||||
<ActivityIndicator color="#fff" size="small" />
|
||||
) : (
|
||||
<>
|
||||
<LucideIcon name="shield-checkmark-outline" size={20} color="#fff" style={{ marginRight: 8 }} />
|
||||
<Text style={styles.confirmText}>Confirm & Pay</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Processing Modal — non-dismissable while polling */}
|
||||
<Modal
|
||||
visible={processingVisible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => {
|
||||
// Only allow closing if not actively polling
|
||||
if (processingStatus !== 'polling') handleCloseProcessingModal();
|
||||
}}
|
||||
>
|
||||
<View style={modalStyles.overlay}>
|
||||
<View style={[modalStyles.card, { backgroundColor: theme.colors.card }]}>
|
||||
{/* Icon / Spinner */}
|
||||
{processingStatus === 'polling' ? (
|
||||
<Animated.View style={{
|
||||
transform: [{
|
||||
rotate: spinAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['0deg', '360deg'],
|
||||
}),
|
||||
}],
|
||||
}}>
|
||||
<LucideIcon name="sync-outline" size={52} color={theme.colors.primary} />
|
||||
</Animated.View>
|
||||
) : processingStatus === 'success' ? (
|
||||
<View style={[modalStyles.iconCircle, { backgroundColor: '#2BAF2B22' }]}>
|
||||
<LucideIcon name="checkmark-circle-outline" size={52} color="#2BAF2B" />
|
||||
</View>
|
||||
) : (
|
||||
<View style={[modalStyles.iconCircle, { backgroundColor: theme.colors.error + '22' }]}>
|
||||
<LucideIcon name="close-circle-outline" size={52} color={theme.colors.error} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<Text style={[modalStyles.title, { color: theme.colors.textPrimary }]}>
|
||||
{processingStatus === 'polling' ? 'Processing Payment' :
|
||||
processingStatus === 'success' ? 'Payment Successful' :
|
||||
processingStatus === 'timeout' ? 'Verification Timed Out' : 'Payment Failed'}
|
||||
</Text>
|
||||
|
||||
{/* Message */}
|
||||
<Text style={[modalStyles.message, { color: theme.colors.textSecondary }]}>
|
||||
{processingMessage}
|
||||
</Text>
|
||||
|
||||
{/* Countdown timer while polling */}
|
||||
{processingStatus === 'polling' && (
|
||||
<View style={modalStyles.timerContainer}>
|
||||
<LucideIcon name="time-outline" size={16} color={theme.colors.textMuted} />
|
||||
<Text style={[modalStyles.timerText, { color: theme.colors.textMuted }]}>
|
||||
{Math.floor(timeRemaining / 60)}:{String(timeRemaining % 60).padStart(2, '0')} remaining
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Close button — only visible when NOT polling */}
|
||||
{processingStatus !== 'polling' && (
|
||||
<TouchableOpacity
|
||||
onPress={handleCloseProcessingModal}
|
||||
style={[
|
||||
modalStyles.closeButton,
|
||||
{
|
||||
backgroundColor: processingStatus === 'success'
|
||||
? '#2BAF2B'
|
||||
: theme.colors.primary,
|
||||
},
|
||||
]}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={modalStyles.closeButtonText}>
|
||||
{processingStatus === 'success' ? 'Done' : 'Close'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</AppView>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentModeScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
amountCard: {
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
marginBottom: 20,
|
||||
},
|
||||
amountRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 6,
|
||||
},
|
||||
amountLabel: {
|
||||
fontSize: responsiveFontSize(1.4),
|
||||
fontFamily: FontsFamily.nunitoSemiBold,
|
||||
},
|
||||
amountValue: {
|
||||
fontSize: responsiveFontSize(3),
|
||||
fontFamily: FontsFamily.nunitoExtraBold,
|
||||
},
|
||||
listContent: {
|
||||
paddingBottom: 100,
|
||||
gap: 10,
|
||||
},
|
||||
modeCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
radioOuter: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
borderWidth: 2,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
radioInner: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 6,
|
||||
},
|
||||
modeIconContainer: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
modeIconImage: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 6,
|
||||
},
|
||||
modeInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
modeName: {
|
||||
fontSize: responsiveFontSize(1.7),
|
||||
fontFamily: FontsFamily.nunitoBold,
|
||||
},
|
||||
modeProvider: {
|
||||
fontSize: responsiveFontSize(1.3),
|
||||
fontFamily: FontsFamily.nunitoSemiBold,
|
||||
marginTop: 2,
|
||||
},
|
||||
modeLimits: {
|
||||
fontSize: responsiveFontSize(1.1),
|
||||
fontFamily: FontsFamily.nunitoRegular,
|
||||
marginTop: 2,
|
||||
},
|
||||
badgeContainer: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
badge: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 6,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: responsiveFontSize(1),
|
||||
fontFamily: FontsFamily.nunitoBold,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
loaderContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
loaderText: {
|
||||
fontSize: responsiveFontSize(1.4),
|
||||
fontFamily: FontsFamily.nunitoSemiBold,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: responsiveFontSize(1.6),
|
||||
fontFamily: FontsFamily.nunitoSemiBold,
|
||||
},
|
||||
bottomBar: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
confirmButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 16,
|
||||
borderRadius: 14,
|
||||
},
|
||||
confirmText: {
|
||||
color: '#fff',
|
||||
fontSize: responsiveFontSize(1.8),
|
||||
fontFamily: FontsFamily.nunitoBold,
|
||||
},
|
||||
});
|
||||
|
||||
const modalStyles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
card: {
|
||||
width: '100%',
|
||||
borderRadius: 20,
|
||||
paddingVertical: 36,
|
||||
paddingHorizontal: 24,
|
||||
alignItems: 'center',
|
||||
},
|
||||
iconCircle: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: responsiveFontSize(2.2),
|
||||
fontFamily: FontsFamily.nunitoExtraBold,
|
||||
marginTop: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
message: {
|
||||
fontSize: responsiveFontSize(1.4),
|
||||
fontFamily: FontsFamily.nunitoSemiBold,
|
||||
marginTop: 10,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
},
|
||||
timerContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(128,128,128,0.1)',
|
||||
},
|
||||
timerText: {
|
||||
fontSize: responsiveFontSize(1.2),
|
||||
fontFamily: FontsFamily.nunitoSemiBold,
|
||||
},
|
||||
closeButton: {
|
||||
marginTop: 24,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 40,
|
||||
borderRadius: 12,
|
||||
},
|
||||
closeButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: responsiveFontSize(1.6),
|
||||
fontFamily: FontsFamily.nunitoBold,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user