Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -48,6 +48,7 @@ function TransactionListItemWide<TItem extends ListItem>({
nonPersonalAndWorkspaceCards,
isAttendeesEnabledForMovingPolicy,
currentSearchHash,
chatReport,
}: TransactionListItemWideProps<TItem>) {
const styles = useThemeStyles();
const theme = useTheme();
Expand Down Expand Up @@ -178,6 +179,7 @@ function TransactionListItemWide<TItem extends ListItem>({
<TransactionItemRow
transactionItem={transactionItem}
report={transactionItem.report}
chatReport={chatReport}
policy={transactionItem.policy}
policyCategories={policyCategories}
policyTagLists={policyTagLists}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,17 @@ function TransactionListItemInner<TItem extends ListItem>({
transactionItem,
]);
const currentUserDetails = useCurrentUserPersonalDetails();
const [parentChatReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID)}`);
const [chatReportActions] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? snapshotReport?.parentReportID)}`);
const chatReportID = snapshotReport?.chatReportID ?? snapshotReport?.parentReportID;
const [parentChatReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(chatReportID)}`);
// Fall back to the search snapshot when the chat report isn't in live Onyx yet (e.g. offline or not fetched),
// matching the grouped-report path in ReportListItemHeader so the Pay flow can still resolve the chat report.
const snapshotChatReport = chatReportID ? snapshotData?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] : undefined;
const chatReport = parentChatReport ?? snapshotChatReport;
const [chatReportActions] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(chatReport?.reportID ?? chatReportID)}`);
const {amountOwed, currentUserAccountID, currentUserLogin, introSelected, betas, isSelfTourViewed, activePolicy, nextStep, chatReportPolicy, delegateEmail, delegateAccountID} =
useReportPaymentContext({
reportID: transactionItem.reportID,
chatReportPolicyID: parentChatReport?.policyID,
chatReportPolicyID: chatReport?.policyID,
});
const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});

Expand Down Expand Up @@ -227,7 +232,7 @@ function TransactionListItemInner<TItem extends ListItem>({
betas,
isSelfTourViewed,
activePolicy,
chatReport: parentChatReport,
chatReport,
chatReportPolicy,
iouReportCurrentNextStepDeprecated: nextStep,
searchData: currentSearchResults?.data,
Expand Down Expand Up @@ -262,6 +267,7 @@ function TransactionListItemInner<TItem extends ListItem>({
policyTagLists,
nonPersonalAndWorkspaceCards,
isAttendeesEnabledForMovingPolicy,
chatReport,
};

if (!isLargeScreenWidth) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {ListItem} from '@components/SelectionList/types';
import type {TransactionPreviewData} from '@libs/actions/Search';
import type {ModifiedMouseEvent} from '@libs/Navigation/helpers/openInternalRouteInNewTab';

import type {CardList, PolicyCategories, PolicyTagLists, ReportAction, TransactionViolation} from '@src/types/onyx';
import type {CardList, PolicyCategories, PolicyTagLists, Report, ReportAction, TransactionViolation} from '@src/types/onyx';

type TransactionListItemSharedProps<TItem extends ListItem> = {
item: TItem;
Expand All @@ -32,6 +32,7 @@ type TransactionListItemSharedProps<TItem extends ListItem> = {
policyTagLists?: PolicyTagLists;
nonPersonalAndWorkspaceCards?: CardList;
isAttendeesEnabledForMovingPolicy?: boolean;
chatReport?: Report;
};

type TransactionListItemWideProps<TItem extends ListItem> = TransactionListItemSharedProps<TItem> & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type TransactionItemRowWideProps = Omit<TransactionItemRowProps, 'shouldUseNarro
function TransactionItemRowWide({
transactionItem,
report,
chatReport,
policy,
policyCategories,
policyTagLists,
Expand Down Expand Up @@ -328,6 +329,7 @@ function TransactionItemRowWide({
policyID={report?.policyID}
hash={transactionItem?.hash}
amount={getReimbursableTotal(report)}
chatReport={chatReport}
shouldDisablePointerEvents={isDisabled || shouldDisableActionPointerEvents}
isMarkAsDone={isMarkAsDone}
/>
Expand Down
2 changes: 2 additions & 0 deletions src/components/TransactionItemRow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const EMPTY_ACTIVE_STYLE: StyleProp<ViewStyle> = [];
function TransactionItemRow({
transactionItem,
report,
chatReport,
policy,
policyCategories,
policyTagLists,
Expand Down Expand Up @@ -182,6 +183,7 @@ function TransactionItemRow({
const wideForwardedProps = {
transactionItem,
report,
chatReport,
policy,
policyCategories,
policyTagLists,
Expand Down
1 change: 1 addition & 0 deletions src/components/TransactionItemRow/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type TransactionWithOptionalSearchFields = TransactionWithOptionalHighlight & {
type TransactionItemRowProps = {
transactionItem: TransactionWithOptionalSearchFields;
report?: Report;
chatReport?: Report;
policy?: Policy;
policyCategories?: PolicyCategories;
policyTagLists?: PolicyTagLists;
Expand Down
174 changes: 174 additions & 0 deletions tests/ui/components/PayActionCellTest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {act, render} from '@testing-library/react-native';

import PayActionCell from '@components/Search/SearchList/ListItem/ActionCell/PayActionCell';
import type {PaymentActionParams} from '@components/SettlementButton/types';

import useOnyx from '@hooks/useOnyx';
import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations';

import {payInvoice} from '@userActions/IOU/PayMoneyRequest';

import CONST from '@src/CONST';
import type {Report} from '@src/types/onyx';

import type {UseOnyxResult} from 'react-native-onyx';

import React from 'react';

const TEST_INVOICE_REPORT_ID = '1001';
const TEST_CHAT_REPORT_ID = '2002';
const TEST_HASH = 12345;

const invoiceReport = {
reportID: TEST_INVOICE_REPORT_ID,
chatReportID: TEST_CHAT_REPORT_ID,
type: CONST.REPORT.TYPE.INVOICE,
currency: CONST.CURRENCY.USD,
policyID: 'policy1',
total: -5000,
} as Report;

const chatReport = {
reportID: TEST_CHAT_REPORT_ID,
type: CONST.REPORT.TYPE.CHAT,
} as Report;

function createOnyxResult<T>(value: NonNullable<T> | undefined): UseOnyxResult<T> {
return [value, {status: 'loaded'}];
}

// Capture the onPress (confirmPayment) handler PayActionCell passes to the settlement button so the payment can be
// confirmed directly, mirroring a user picking "Pay as an individual > Mark as paid".
const mockOnPressHolder: {current?: (params: PaymentActionParams) => void} = {current: undefined};
jest.mock('@components/SettlementButton', () => ({
__esModule: true,
default: (props: {onPress?: (params: PaymentActionParams) => void}) => {
mockOnPressHolder.current = props.onPress;
return null;
},
}));

jest.mock('@userActions/IOU/PayMoneyRequest', () => ({
__esModule: true,
payInvoice: jest.fn(),
payMoneyRequest: jest.fn(),
}));

jest.mock('@userActions/IOU/ReportWorkflow', () => ({
__esModule: true,
canIOUBePaid: jest.fn(() => true),
}));

jest.mock('@libs/actions/Search', () => ({
__esModule: true,
getSearchPayOnyxData: jest.fn(() => ({optimisticData: [], successData: [], failureData: []})),
}));

jest.mock('@libs/ReportUtils', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const actual = jest.requireActual('@libs/ReportUtils');
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
...actual,
__esModule: true,
isInvoiceReport: jest.fn(() => true),
isIndividualInvoiceRoom: jest.fn(() => false),
getReimbursableTotal: jest.fn(() => 5000),
};
});

jest.mock('@hooks/useReportWithTransactionsAndViolations', () => ({__esModule: true, default: jest.fn()}));
jest.mock('@hooks/useNetwork', () => ({__esModule: true, default: jest.fn(() => ({isOffline: false}))}));
jest.mock('@hooks/usePolicy', () => ({__esModule: true, default: jest.fn(() => undefined)}));
jest.mock('@hooks/useCurrencyList', () => ({__esModule: true, useCurrencyListActions: jest.fn(() => ({convertToDisplayString: () => '$50.00'}))}));
jest.mock('@hooks/useParticipantsInvoiceReport', () => ({__esModule: true, getParticipantsInvoiceReport: jest.fn(() => undefined)}));
jest.mock('@hooks/usePaymentContext', () => ({
__esModule: true,
useReportPaymentContext: jest.fn(() => ({
currentUserLogin: 'payer@test.com',
currentUserAccountID: 1,
email: 'payer@test.com',
localCurrencyCode: 'USD',
introSelected: undefined,
betas: [],
isSelfTourViewed: false,
userBillingGracePeriodEnds: undefined,
amountOwed: undefined,
ownerBillingGracePeriodEnd: undefined,
activePolicyID: undefined,
activePolicy: undefined,
defaultWorkspaceName: '',
nextStep: undefined,
chatReportPolicy: undefined,
})),
}));
jest.mock('@hooks/useOnyx', () => jest.fn());

jest.mock('@components/DelegateNoAccessModalProvider', () => ({
__esModule: true,
useDelegateNoAccessState: jest.fn(() => ({isDelegateAccessRestricted: false})),
useDelegateNoAccessActions: jest.fn(() => ({showDelegateNoAccessModal: jest.fn()})),
}));

const mockedUseOnyx = jest.mocked(useOnyx);
const mockedUseReportWithTransactionsAndViolations = jest.mocked(useReportWithTransactionsAndViolations);
const mockedPayInvoice = jest.mocked(payInvoice);

describe('PayActionCell', () => {
beforeEach(() => {
jest.clearAllMocks();
mockOnPressHolder.current = undefined;
mockedUseReportWithTransactionsAndViolations.mockReturnValue([invoiceReport, [], undefined]);
mockedUseOnyx.mockImplementation(() => createOnyxResult(undefined));
});

it('calls payInvoice with the chatReport supplied as a prop (the flat `type:invoice columns:...` transaction row now resolves and passes it)', () => {
render(
<PayActionCell
isLoading={false}
policyID="policy1"
reportID={TEST_INVOICE_REPORT_ID}
hash={TEST_HASH}
amount={5000}
chatReport={chatReport}
/>,
);

act(() => {
mockOnPressHolder.current?.({
paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE,
payAsBusiness: false,
});
});

expect(mockedPayInvoice).toHaveBeenCalledWith(
expect.objectContaining({
paymentMethodType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE,
chatReport,
invoiceReport,
}),
);
});

it('does not call payInvoice when no chatReport prop is supplied', () => {
render(
<PayActionCell
isLoading={false}
policyID="policy1"
reportID={TEST_INVOICE_REPORT_ID}
hash={TEST_HASH}
amount={5000}
chatReport={undefined}
/>,
);

act(() => {
mockOnPressHolder.current?.({
paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE,
payAsBusiness: false,
});
});

expect(mockedPayInvoice).not.toHaveBeenCalled();
});
});
Loading