import React, { useEffect } from 'react'; import { RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native'; import type { CompositeScreenProps } from '@react-navigation/native'; import type { BottomTabScreenProps } from '@react-navigation/bottom-tabs'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import { Icon } from '../../../components/Icon'; import { Button, Card, EmptyState, InfoBanner, Loader, ScreenShell } from '../../../components'; import { useTheme } from '../../../theme'; import { formatCurrency, formatDate, formatTime } from '../../../utils/formatters'; import type { HomeStackParamList, MainTabParamList } from '../../../navigation/types'; import type { ApiError } from '../../../types/api'; import { describeError } from '../../../services/api/cbsErrors'; import { useApiErrorToast } from '../../../hooks/useApiErrorToast'; import { useGetFundTransferDetailQuery } from '../../payments/services/paymentsApi'; import { MODE_LABEL, humaniseReason, statusCopy, toneColors } from '../utils/transferStatus'; type Props = CompositeScreenProps< NativeStackScreenProps, BottomTabScreenProps >; const when = (iso: string | null) => (iso ? `${formatDate(iso)}, ${formatTime(iso)}` : null); export default function TransferDetailScreen({ navigation, route }: Props) { const theme = useTheme(); const showError = useApiErrorToast(); const { transferId } = route.params; // Always fetched fresh on open: reopening the screen is one of the two ways // the guide says a pending transfer should re-check its state. const { data, isLoading, isFetching, error, refetch } = useGetFundTransferDetailQuery(transferId, { refetchOnMountOrArgChange: true, }); const apiError = error as ApiError | undefined; // A transfer that is not on this profile: close the detail and say why. useEffect(() => { if (apiError?.code === 'TRANSACTION_NOT_FOUND') { showError(apiError); navigation.goBack(); } }, [apiError, navigation, showError]); if (isLoading) { return ( ); } if (!data) { const copy = describeError(apiError?.code); return ( ); } const status = statusCopy(data.status); const colors = toneColors(status.tone, theme.colors); const showReason = Boolean(data.failureReason) && (data.status === 'FAILED' || data.status === 'MANUAL_REVIEW'); const rows: Array<[string, string | null | undefined]> = [ ['Paid to', data.payeeName], [data.mode === 'P2P' ? 'Mobile' : 'Account', data.payeeDisplay], ['IFSC', data.beneficiaryIfsc], ['MMID', data.beneficiaryMmid], ['Type', `IMPS · ${MODE_LABEL[data.mode] ?? data.mode}`], ['Remark', data.remark], ['Initiated', when(data.createdAt)], ['Last updated', when(data.updatedAt)], ]; return ( } > {/* ── the outcome ───────────────────────────────────────────────── */} {formatCurrency(data.amount)} {/* `simple` break strategy: Android's default `highQuality` measures a custom font as fitting on one line, then wraps it on draw, so the last word ("pending") was clipped outside the pill. */} {status.label} {showReason ? ( {humaniseReason(data.failureReason as string)} ) : null} {data.status === 'PENDING_RECONCILIATION' || data.status === 'PROCESSING' ? ( ) : null} {data.status === 'PENDING_OTP' ? ( ) : null} {data.status === 'MANUAL_REVIEW' ? ( ) : null} {/* ── the reference the customer quotes ─────────────────────────── */} Reference {data.referenceId} Press and hold to copy {rows .filter(([, value]) => Boolean(value)) .map(([label, value]) => ( {label} {value} ))} {status.refreshable ? (