Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ce28060
update duplicate RHP screen to match other flows
nabi-ebrahimi May 5, 2026
ae840e5
Add Spanish translation for keep selected action
nabi-ebrahimi May 5, 2026
3783ec5
enable duplicate list scrolling
nabi-ebrahimi May 5, 2026
e318724
clean up review feedback
nabi-ebrahimi May 5, 2026
d94d6a2
Fix duplicate review lint
nabi-ebrahimi May 5, 2026
edc6c0c
Add missing duplicate review translations
nabi-ebrahimi May 6, 2026
8ca37fa
Merge branch 'main' into feature/88342-update-duplicate-rhp-screen
nabi-ebrahimi May 8, 2026
964cd2e
Merge branch 'main' into feature/88342-update-duplicate-rhp-screen
nabi-ebrahimi May 12, 2026
0dc44c4
align duplicate review scrolling with merge flow
nabi-ebrahimi May 12, 2026
f4e251b
fix duplicate review typecheck
nabi-ebrahimi May 12, 2026
060d758
Merge branch 'main' into feature/88342-update-duplicate-rhp-screen
nabi-ebrahimi May 12, 2026
445a06c
fix duplicate review thread typecheck
nabi-ebrahimi May 12, 2026
fbe7170
Merge branch 'main' into feature/88342-update-duplicate-rhp-screen
nabi-ebrahimi May 18, 2026
5962060
tighten duplicate review header spacing
nabi-ebrahimi May 18, 2026
cee26f5
Merge branch 'main' into feature/88342-update-duplicate-rhp-screen
nabi-ebrahimi May 19, 2026
b7a3d9f
increase duplicate review radio hit area
nabi-ebrahimi May 19, 2026
09298af
Merge branch 'main' into feature/88342-update-duplicate-rhp-screen
nabi-ebrahimi May 20, 2026
a9f6c3e
fix: increase duplicate review radio hit area
nabi-ebrahimi May 20, 2026
b708de4
Merge branch 'main' into feature/88342-update-duplicate-rhp-screen
nabi-ebrahimi May 21, 2026
82f92f5
adjust duplicate review radio spacing
nabi-ebrahimi May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ type TransactionItemRowNarrowProps = Pick<
| 'isInSingleTransactionReport'
| 'shouldShowRadioButton'
| 'onRadioButtonPress'
| 'radioButtonContainerStyle'
| 'radioButtonWrapperStyle'
| 'shouldShowErrors'
| 'isDisabled'
| 'violations'
Expand All @@ -51,6 +53,8 @@ function TransactionItemRowNarrow({
isInSingleTransactionReport = false,
shouldShowRadioButton = false,
onRadioButtonPress = () => {},
radioButtonContainerStyle,
radioButtonWrapperStyle,
shouldShowErrors = true,
isDisabled = false,
violations,
Expand Down Expand Up @@ -149,12 +153,13 @@ function TransactionItemRowNarrow({
</View>
)}
{shouldShowRadioButton && (
<View style={[styles.ml3, styles.justifyContentCenter]}>
<View style={[styles.ml3, styles.justifyContentCenter, radioButtonContainerStyle]}>
<RadioButton
isChecked={isSelected}
disabled={isDisabled}
onPress={() => onRadioButtonPress?.(transactionItem.transactionID)}
accessibilityLabel={CONST.ROLE.RADIO}
style={radioButtonWrapperStyle}
/>
</View>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function TransactionItemRowWide({
isInSingleTransactionReport = false,
shouldShowRadioButton = false,
onRadioButtonPress = () => {},
radioButtonContainerStyle,
shouldShowErrors = true,
isDisabled = false,
violations,
Expand Down Expand Up @@ -592,7 +593,7 @@ function TransactionItemRowWide({
)}
{columns?.map(renderColumn)}
{shouldShowRadioButton && (
<View style={[styles.ml1, styles.justifyContentCenter]}>
<View style={[styles.ml1, styles.justifyContentCenter, radioButtonContainerStyle]}>
<RadioButton
isChecked={isSelected}
disabled={isDisabled}
Expand Down
5 changes: 5 additions & 0 deletions src/components/TransactionItemRow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ function TransactionItemRow({
isInSingleTransactionReport = false,
shouldShowRadioButton = false,
onRadioButtonPress = () => {},
radioButtonContainerStyle,
radioButtonWrapperStyle,
shouldShowErrors = true,
shouldHighlightItemWhenSelected = true,
isDisabled = false,
Expand Down Expand Up @@ -145,6 +147,8 @@ function TransactionItemRow({
isInSingleTransactionReport,
shouldShowRadioButton,
onRadioButtonPress,
radioButtonContainerStyle,
radioButtonWrapperStyle,
shouldShowErrors,
isDisabled,
violations,
Expand Down Expand Up @@ -191,6 +195,7 @@ function TransactionItemRow({
isInSingleTransactionReport,
shouldShowRadioButton,
onRadioButtonPress,
radioButtonContainerStyle,
shouldShowErrors,
shouldHighlightItemWhenSelected,
isDisabled,
Expand Down
2 changes: 2 additions & 0 deletions src/components/TransactionItemRow/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ type TransactionItemRowProps = {
isInSingleTransactionReport?: boolean;
shouldShowRadioButton?: boolean;
onRadioButtonPress?: (transactionID: string) => void;
radioButtonContainerStyle?: StyleProp<ViewStyle>;
radioButtonWrapperStyle?: StyleProp<ViewStyle>;
shouldShowErrors?: boolean;
shouldHighlightItemWhenSelected?: boolean;
isDisabled?: boolean;
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1515,6 +1515,7 @@ const translations: TranslationDeepObject<typeof en> = {
someDuplicatesArePaid: 'Einige dieser Duplikate wurden bereits genehmigt oder bezahlt.',
reviewDuplicates: 'Duplikate prüfen',
keepAll: 'Alle behalten',
keepSelected: 'Auswahl behalten',
noDuplicatesTitle: 'Alles erledigt!',
noDuplicatesDescription: 'Es gibt hier keine doppelten Transaktionen zur Überprüfung.',
confirmApprove: 'Genehmigungsbetrag bestätigen',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,7 @@ const translations = {
someDuplicatesArePaid: 'Some of these duplicates have been approved or paid already.',
reviewDuplicates: 'Review duplicates',
keepAll: 'Keep all',
keepSelected: 'Keep selected',
noDuplicatesTitle: 'All set!',
noDuplicatesDescription: 'There are no duplicate transactions for review here.',
confirmApprove: 'Confirm approval amount',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,7 @@ const translations: TranslationDeepObject<typeof en> = {
someDuplicatesArePaid: 'Algunos de estos duplicados ya han sido aprobados o pagados.',
reviewDuplicates: 'Revisar duplicados',
keepAll: 'Mantener todos',
keepSelected: 'Mantener seleccionado',
noDuplicatesTitle: '¡Todo listo!',
noDuplicatesDescription: 'No hay transacciones duplicadas para revisar aquí.',
confirmApprove: 'Confirmar importe a aprobar',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1518,6 +1518,7 @@ const translations: TranslationDeepObject<typeof en> = {
someDuplicatesArePaid: 'Certains de ces doublons ont déjà été approuvés ou payés.',
reviewDuplicates: 'Examiner les doublons',
keepAll: 'Tout garder',
keepSelected: 'Garder la sélection',
noDuplicatesTitle: 'Tout est en ordre !',
noDuplicatesDescription: "Il n'y a aucune transaction en double à vérifier ici.",
confirmApprove: 'Confirmer le montant approuvé',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1514,6 +1514,7 @@ const translations: TranslationDeepObject<typeof en> = {
someDuplicatesArePaid: 'Alcuni di questi duplicati sono già stati approvati o pagati.',
reviewDuplicates: 'Controlla duplicati',
keepAll: 'Mantieni tutto',
keepSelected: 'Mantieni selezionati',
noDuplicatesTitle: 'Tutto a posto!',
noDuplicatesDescription: 'Non ci sono transazioni duplicate da verificare qui.',
confirmApprove: 'Conferma l’importo approvato',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1495,6 +1495,7 @@ const translations: TranslationDeepObject<typeof en> = {
someDuplicatesArePaid: 'これらの重複の一部は、すでに承認または支払い済みです。',
reviewDuplicates: '重複を確認',
keepAll: 'すべて保持',
keepSelected: '選択したものを保持',
noDuplicatesTitle: '準備完了!',
noDuplicatesDescription: '確認が必要な重複取引はありません。',
confirmApprove: '承認金額を確認',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,7 @@ const translations: TranslationDeepObject<typeof en> = {
someDuplicatesArePaid: 'Sommige van deze duplicaten zijn al goedgekeurd of betaald.',
reviewDuplicates: 'Dubbele items controleren',
keepAll: 'Alles behouden',
keepSelected: 'Selectie behouden',
noDuplicatesTitle: 'Alles in orde!',
noDuplicatesDescription: 'Er zijn hier geen dubbele transacties om te beoordelen.',
confirmApprove: 'Bevestig goedkeuringsbedrag',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1509,6 +1509,7 @@ const translations: TranslationDeepObject<typeof en> = {
someDuplicatesArePaid: 'Niektóre z tych duplikatów zostały już zatwierdzone lub opłacone.',
reviewDuplicates: 'Przejrzyj duplikaty',
keepAll: 'Zachowaj wszystko',
keepSelected: 'Zachowaj wybrane',
noDuplicatesTitle: 'Wszystko gotowe!',
noDuplicatesDescription: 'Nie ma tutaj zduplikowanych transakcji do sprawdzenia.',
confirmApprove: 'Potwierdź kwotę zatwierdzenia',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1508,6 +1508,7 @@ const translations: TranslationDeepObject<typeof en> = {
someDuplicatesArePaid: 'Alguns desses duplicados já foram aprovados ou pagos.',
reviewDuplicates: 'Revisar duplicados',
keepAll: 'Manter tudo',
keepSelected: 'Manter selecionados',
noDuplicatesTitle: 'Tudo pronto!',
noDuplicatesDescription: 'Não há transações duplicadas para revisar aqui.',
confirmApprove: 'Confirmar valor da aprovação',
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,7 @@ const translations: TranslationDeepObject<typeof en> = {
someDuplicatesArePaid: '其中一些重复项已被批准或支付。',
reviewDuplicates: '审核重复项',
keepAll: '全部保留',
keepSelected: '保留所选项',
noDuplicatesTitle: '全部完成!',
noDuplicatesDescription: '这里没有需要审核的重复交易。',
confirmApprove: '确认批准金额',
Expand Down
127 changes: 73 additions & 54 deletions src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
Original file line number Diff line number Diff line change
@@ -1,84 +1,103 @@
import React, {useMemo} from 'react';
import {View} from 'react-native';
import React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {usePersonalDetails} from '@components/OnyxListItemProvider';
import {getButtonRole} from '@components/Button/utils';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {PressableWithFeedback} from '@components/Pressable';
import type {TransactionListItemType} from '@components/Search/SearchList/ListItem/types';
import TransactionItemRow from '@components/TransactionItemRow';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getOriginalReportID} from '@libs/ReportUtils';
import ReportActionItem from '@pages/inbox/report/ReportActionItem';
import {ReportActionItemActionsContext, ReportActionItemStateContext} from '@pages/inbox/report/ReportActionItemContext';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import variables from '@styles/variables';
import {createTransactionThreadReport} from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Transaction} from '@src/types/onyx';

type DuplicateTransactionItemProps = {
transaction: OnyxEntry<Transaction>;
index: number;
isLastItem: boolean;
isSelected: boolean;
onSelectTransaction: (transactionID: string) => void;
onPreviewPressed: (reportID: string) => void;
};

const linkedTransactionRouteErrorSelector = (transaction: OnyxEntry<Transaction>) => transaction?.errorFields?.route ?? null;

function DuplicateTransactionItem({transaction, index, onPreviewPressed}: DuplicateTransactionItemProps) {
function DuplicateTransactionItem({transaction, isLastItem, isSelected, onSelectTransaction, onPreviewPressed}: DuplicateTransactionItemProps) {
const styles = useThemeStyles();
const personalDetails = usePersonalDetails();

const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID);
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`);
const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`);
const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT);
const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed;
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`);
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
const [betas] = useOnyx(ONYXKEYS.BETAS);

const action = Object.values(reportActions ?? {})?.find((reportAction) => {
const IOUTransactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : CONST.DEFAULT_NUMBER_ID;
return IOUTransactionID === transaction?.transactionID;
const iouTransactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : CONST.DEFAULT_NUMBER_ID;
return iouTransactionID === transaction?.transactionID;
});

const originalReportID = getOriginalReportID(report?.reportID, action, reportActions);
const handlePreviewPress = () => {
if (!action || !report) {
return;
}

const [draftMessage] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`);
if (action.childReportID) {
onPreviewPressed(action.childReportID);
return;
}

const [linkedTransactionRouteError] = useOnyx(
`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(isMoneyRequestAction(action) ? getOriginalMessage(action)?.IOUTransactionID : undefined)}`,
{
selector: linkedTransactionRouteErrorSelector,
},
);
const transactionThreadReport = createTransactionThreadReport({
introSelected,
currentUserLogin: currentUserPersonalDetails.login ?? '',
currentUserAccountID: currentUserPersonalDetails.accountID,
betas,
iouReport: report,
iouReportAction: action,
transaction,
});
if (!transactionThreadReport?.reportID) {
return;
}

const stateValue = useMemo(() => ({shouldOpenReportInRHP: true}), []);
const actionsValue = useMemo(() => ({onPreviewPressed}), [onPreviewPressed]);
onPreviewPressed(transactionThreadReport.reportID);
};

if (!action || !report) {
if (!action || !report || !transaction) {
return null;
}

const reportDraftMessage = draftMessage?.[action.reportActionID];
const matchingDraftMessage = reportDraftMessage?.message;

return (
<View style={styles.pb2}>
<ReportActionItemStateContext.Provider value={stateValue}>
<ReportActionItemActionsContext.Provider value={actionsValue}>
<ReportActionItem
action={action}
report={report}
parentReportAction={getReportAction(report?.parentReportID, report?.parentReportActionID)}
index={index}
displayAsGroup={false}
shouldDisplayNewMarker={false}
isFirstVisibleReportAction={false}
shouldDisplayContextMenu={false}
personalDetails={personalDetails}
draftMessage={matchingDraftMessage}
linkedTransactionRouteError={linkedTransactionRouteError}
userBillingFundID={userBillingFundID}
isTryNewDotNVPDismissed={isTryNewDotNVPDismissed}
/>
</ReportActionItemActionsContext.Provider>
</ReportActionItemStateContext.Provider>
</View>
<OfflineWithFeedback pendingAction={transaction.pendingAction}>
<PressableWithFeedback
sentryLabel={CONST.SENTRY_LABEL.SEARCH.TRANSACTION_LIST_ITEM}
onPress={handlePreviewPress}
accessibilityLabel={transaction.comment?.comment ?? ''}
role={getButtonRole(true)}
isNested
hoverStyle={styles.hoveredComponentBG}
style={!isLastItem && styles.borderBottom}
>
<TransactionItemRow
transactionItem={transaction as TransactionListItemType}
report={report}
policy={policy}
shouldUseNarrowLayout
isSelected={isSelected}
shouldShowTooltip={false}
dateColumnSize={CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL}
amountColumnSize={CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL}
taxAmountColumnSize={CONST.SEARCH.TABLE_COLUMN_SIZES.NORMAL}
shouldHighlightItemWhenSelected={false}
shouldShowErrors={false}
style={[styles.p4, styles.pr0]}
shouldShowRadioButton
radioButtonContainerStyle={styles.ml0}
radioButtonWrapperStyle={[styles.justifyContentCenter, styles.pr3half, {paddingLeft: 10, height: variables.w44}]}
onRadioButtonPress={() => onSelectTransaction(transaction.transactionID)}
/>
</PressableWithFeedback>
</OfflineWithFeedback>
);
}

Expand Down
47 changes: 19 additions & 28 deletions src/pages/TransactionDuplicate/DuplicateTransactionsList.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,35 @@
import React, {useCallback} from 'react';
import type {FlatListProps, ListRenderItemInfo, ScrollViewProps} from 'react-native';
import React from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import FlatList from '@components/FlatList/FlatList';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import type {Transaction} from '@src/types/onyx';
import DuplicateTransactionItem from './DuplicateTransactionItem';

type DuplicateTransactionsListProps = {
transactions: Array<OnyxEntry<Transaction>>;
selectedTransactionID?: string;
onSelectTransaction: (transactionID: string) => void;
onPreviewPressed: (reportID: string) => void;
};

const keyExtractor: FlatListProps<OnyxEntry<Transaction>>['keyExtractor'] = (item, index) => `${item?.transactionID}+${index}`;

const maintainVisibleContentPosition: ScrollViewProps['maintainVisibleContentPosition'] = {
minIndexForVisible: 1,
};

function DuplicateTransactionsList({transactions, onPreviewPressed}: DuplicateTransactionsListProps) {
function DuplicateTransactionsList({transactions, selectedTransactionID, onSelectTransaction, onPreviewPressed}: DuplicateTransactionsListProps) {
const styles = useThemeStyles();

const renderItem = useCallback(
({item, index}: ListRenderItemInfo<OnyxEntry<Transaction>>) => (
<DuplicateTransactionItem
transaction={item}
index={index}
onPreviewPressed={onPreviewPressed}
/>
),
[onPreviewPressed],
);
const theme = useTheme();

return (
<FlatList
data={transactions}
renderItem={renderItem}
keyExtractor={keyExtractor}
maintainVisibleContentPosition={maintainVisibleContentPosition}
contentContainerStyle={styles.pt5}
/>
<View style={[styles.expenseWidgetRadius, styles.overflowHidden, {backgroundColor: theme.cardBG}]}>
{transactions.map((transaction, index) => (
<DuplicateTransactionItem
key={transaction?.transactionID ?? transaction?.created ?? 'duplicate-transaction'}
transaction={transaction}
isLastItem={index === transactions.length - 1}
isSelected={transaction?.transactionID === selectedTransactionID}
onSelectTransaction={onSelectTransaction}
onPreviewPressed={onPreviewPressed}
/>
))}
</View>
);
}

Expand Down
Loading
Loading