Files
CBS-Mobile-App/src/modules/history/hooks/useFundTransferHistory.ts
T
Abin ToppoandClaude Opus 5.5 f1def81eb7 Add TPIN creation, TPIN-authorised transfers, MPIN reset and transfer history
- Create TPIN: current MPIN -> OTP -> encrypted TPIN via /tpin/create/*,
  from Services and from the transfer screen on TPIN_NOT_SET
- IMPS initiate now sends encryptedTpin instead of encryptedMpin; handles
  INVALID_TPIN and TPIN_NOT_SET
- Share one RSA-OAEP encryption and key-retry path for MPIN and TPIN
- Forgot MPIN on sign-in and Reset MPIN in the Security Center
- Fund transfer history list and detail, reachable from Home
- Brand logo on sign-in and palette updates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:54:43 +05:30

138 lines
4.1 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useGetFundTransferHistoryQuery } from '../../payments/services/paymentsApi';
import type {
BeneficiaryMode,
FundTransferHistoryItem,
FundTransferStatus,
} from '../../../types/cbs';
export const HISTORY_PAGE_SIZE = 10;
export interface HistoryFilters {
status: FundTransferStatus | 'ALL';
mode: BeneficiaryMode | 'ALL';
/** Inclusive, `yyyy-MM-dd`, sent only as a pair. */
fromDate?: string;
toDate?: string;
}
/**
* The transfer history, accumulated page by page — the same shape as
* `useStatement`, for the same reason: RTK Query caches one entry per page, and
* the list needs every page walked so far.
*
* Any change of filter starts again from page 0, as the guide requires; page 2
* of the old filter has no meaning under the new one. The next page is asked
* for only while the server says `last` is false.
*/
export const useFundTransferHistory = (filters: HistoryFilters) => {
const queryKey = `${filters.status}|${filters.mode}|${filters.fromDate ?? ''}|${filters.toDate ?? ''}`;
const [page, setPage] = useState(0);
const [pages, setPages] = useState<Record<number, FundTransferHistoryItem[]>>({});
const loadedKey = useRef(queryKey);
const { data, isLoading, isFetching, error, refetch } = useGetFundTransferHistoryQuery(
{ ...filters, page, size: HISTORY_PAGE_SIZE },
{ refetchOnMountOrArgChange: true },
);
// Reset before the new filter's first response lands, so the list is never
// briefly the old filter's rows under the new filter's chips.
if (loadedKey.current !== queryKey) {
loadedKey.current = queryKey;
if (page !== 0) {
setPage(0);
}
if (Object.keys(pages).length > 0) {
setPages({});
}
}
useEffect(() => {
if (!data) {
return;
}
setPages((current) =>
current[data.number] === data.content ? current : { ...current, [data.number]: data.content },
);
}, [data]);
/**
* Flattened in page order with repeats dropped. The server caches a page for
* about 30 seconds, so a row can shift across a page boundary mid-browse;
* `transferId` is the stable key that stops it appearing twice.
*/
const transfers = useMemo(() => {
const seen = new Set<string>();
const ordered: FundTransferHistoryItem[] = [];
Object.keys(pages)
.map(Number)
.sort((left, right) => left - right)
.forEach((index) => {
(pages[index] ?? []).forEach((item) => {
if (!seen.has(item.transferId)) {
seen.add(item.transferId);
ordered.push(item);
}
});
});
return ordered;
}, [pages]);
const hasMore = data ? !data.last : false;
const loadMore = useCallback(() => {
if (!hasMore || isFetching) {
return;
}
setPage((current) => current + 1);
}, [hasMore, isFetching]);
/**
* Pull-to-refresh: start again from page 0.
*
* The fresh page is written straight from the refetch's own result, not left
* to the `[data]` effect above. The server caches a page for ~30 seconds, so a
* refresh usually returns exactly what it returned before — and RTK Query's
* structural sharing then hands back the SAME `data` object, the effect never
* fires, and a list cleared before the request stayed empty. The current rows
* also stay on screen until the fresh page lands, rather than blanking.
*/
const reload = useCallback(() => {
if (page !== 0) {
// A different cache entry, so `data` genuinely changes and the effect
// refills the list.
setPages({});
setPage(0);
return;
}
refetch()
.unwrap()
.then((fresh) => setPages({ [fresh.number]: fresh.content }))
.catch(() => {
// Surfaced through `error`; the rows already on screen are kept.
});
}, [page, refetch]);
return {
transfers,
totalElements: data?.totalElements ?? 0,
hasMore,
isLoading: isLoading || (isFetching && transfers.length === 0),
isLoadingMore: isFetching && page > 0,
isRefreshing: isFetching && page === 0 && transfers.length > 0,
error,
loadMore,
reload,
};
};