import React, { useCallback, useMemo, useState } from 'react'; import { ActivityIndicator, FlatList, Pressable, RefreshControl, ScrollView, StyleSheet, Text, View, } from 'react-native'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import { Icon } from '../../../components/Icon'; import DatePicker from 'react-native-date-picker'; import { AppModal as Modal, Button, Card, EmptyState, Loader, ScreenShell, SegmentedControl, } from '../../../components'; import { useTheme } from '../../../theme'; import { formatCurrency, formatDate, formatTime } from '../../../utils/formatters'; import type { HomeStackParamList } from '../../../navigation/types'; import type { ApiError } from '../../../types/api'; import type { BeneficiaryMode, FundTransferHistoryItem, FundTransferStatus } from '../../../types/cbs'; import { describeError } from '../../../services/api/cbsErrors'; import { toStatementDate } from '../../home/utils/statement'; import { useFundTransferHistory, type HistoryFilters } from '../hooks/useFundTransferHistory'; import { humaniseReason, statusCopy, toneColors } from '../utils/transferStatus'; type Props = NativeStackScreenProps; const STATUS_FILTERS: ReadonlyArray<{ value: FundTransferStatus | 'ALL'; label: string }> = [ { value: 'ALL', label: 'All' }, { value: 'SUCCESS', label: 'Successful' }, { value: 'FAILED', label: 'Failed' }, { value: 'PROCESSING', label: 'In progress' }, { value: 'PENDING_RECONCILIATION', label: 'Pending' }, { value: 'MANUAL_REVIEW', label: 'Under review' }, ]; const MODE_FILTERS: ReadonlyArray<{ value: BeneficiaryMode | 'ALL'; label: string }> = [ { value: 'ALL', label: 'All' }, { value: 'P2A', label: 'Account' }, { value: 'P2P', label: 'Mobile' }, ]; function TransferRow({ item, onPress, }: { item: FundTransferHistoryItem; onPress: (item: FundTransferHistoryItem) => void; }) { const theme = useTheme(); const copy = statusCopy(item.status); const colors = toneColors(copy.tone, theme.colors); // Shown only for failed and review states, and only when there is one. const showReason = Boolean(item.failureReason) && (item.status === 'FAILED' || item.status === 'MANUAL_REVIEW'); return ( onPress(item)} > {item.payeeName || 'Payee'} {item.payeeDisplay ? ( {item.payeeDisplay} ) : null} {`${formatDate(item.createdAt)} · ${formatTime(item.createdAt)}`} {showReason ? ( {humaniseReason(item.failureReason as string)} ) : null} {formatCurrency(item.amount)} {/* See TransferDetailScreen: `simple` stops Android clipping the last word of a label drawn in a custom font. */} {copy.label} ); } export default function TransferHistoryScreen({ navigation }: Props) { const theme = useTheme(); const [filters, setFilters] = useState({ status: 'ALL', mode: 'ALL' }); const [rangeOpen, setRangeOpen] = useState(false); const [pickerOpen, setPickerOpen] = useState<'from' | 'to' | null>(null); const [tempFrom, setTempFrom] = useState(() => { const date = new Date(); date.setDate(date.getDate() - 30); return date; }); const [tempTo, setTempTo] = useState(() => new Date()); const [rangeError, setRangeError] = useState(null); const { transfers, totalElements, hasMore, isLoading, isLoadingMore, isRefreshing, error, loadMore, reload } = useFundTransferHistory(filters); const apiError = error as ApiError | undefined; const hasRange = Boolean(filters.fromDate && filters.toDate); const openDetail = useCallback( (item: FundTransferHistoryItem) => navigation.navigate('TransferDetail', { transferId: item.transferId }), [navigation], ); const applyRange = useCallback(() => { if (tempFrom.getTime() > tempTo.getTime()) { setRangeError('The start date cannot be after the end date.'); return; } setRangeError(null); setRangeOpen(false); setFilters((current) => ({ ...current, fromDate: toStatementDate(tempFrom), toDate: toStatementDate(tempTo), })); }, [tempFrom, tempTo]); const clearRange = useCallback(() => { setRangeOpen(false); setRangeError(null); setFilters((current) => ({ ...current, fromDate: undefined, toDate: undefined })); }, []); const subtitle = useMemo(() => { if (isLoading) return 'Loading your transfers'; return `${totalElements} ${totalElements === 1 ? 'transfer' : 'transfers'}`; }, [isLoading, totalElements]); const listHeader = ( {STATUS_FILTERS.map((option) => { const active = filters.status === option.value; return ( setFilters((current) => ({ ...current, status: option.value }))} style={[ styles.chip, { borderRadius: theme.radius.pill, backgroundColor: active ? theme.colors.primary : theme.colors.surface, borderColor: active ? theme.colors.primary : theme.colors.cardBorder, }, ]} > {option.label} ); })} setFilters((current) => ({ ...current, mode }))} accessibilityLabel="Transfer type" /> setRangeOpen(true)} style={[ styles.rangeButton, { backgroundColor: theme.colors.surface, borderColor: hasRange ? theme.colors.primary : theme.colors.cardBorder, borderRadius: theme.radius.md, }, ]} > {hasRange ? `${formatDate(filters.fromDate as string)} – ${formatDate(filters.toDate as string)}` : 'Any date'} ); const renderEmpty = () => { if (isLoading) { return ; } if (apiError) { const copy = describeError(apiError.code); return ( ); } return ( ); }; return ( item.transferId} renderItem={({ item }) => } ListHeaderComponent={listHeader} ListEmptyComponent={renderEmpty} ListFooterComponent={ isLoadingMore ? ( ) : null } ItemSeparatorComponent={Separator} onEndReached={hasMore ? loadMore : undefined} onEndReachedThreshold={0.4} refreshControl={ } showsVerticalScrollIndicator={false} contentContainerStyle={styles.listContent} /> setRangeOpen(false)} title="Filter by date"> {(['from', 'to'] as const).map((which) => ( {which === 'from' ? 'From' : 'To'} setPickerOpen(which)} style={[ styles.rangeButton, { backgroundColor: theme.colors.surfaceMuted, borderColor: theme.colors.cardBorder, borderRadius: theme.radius.md, }, ]} > {formatDate(toStatementDate(which === 'from' ? tempFrom : tempTo))} ))} {rangeError ? ( {rangeError} ) : null}