757 lines
28 KiB
TypeScript
757 lines
28 KiB
TypeScript
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;
|
||
|
||
console.log("payment res",res)
|
||
|
||
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;
|
||
|
||
console.log("payment payload ",payload)
|
||
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={theme.colors.reverseText} style={{ marginRight: 8 }} />
|
||
<Text style={[styles.confirmText, { color: theme.colors.black }]}>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: {
|
||
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,
|
||
},
|
||
});
|