Skip to content

Commit 7224b83

Browse files
authored
Merge pull request Expensify#93161 from callstack-internal/refactor/extract-getIsP2PForAmount
refactor:extract getIsP2PForAmount into AmountSubmission helper
2 parents 452d18d + 99215d5 commit 7224b83

3 files changed

Lines changed: 182 additions & 2 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
2+
import Onyx from 'react-native-onyx';
3+
import {isParticipantP2P} from '@libs/IOUUtils';
4+
import {getMoneyRequestParticipantsFromReport} from '@userActions/IOU/MoneyRequest';
5+
import ONYXKEYS from '@src/ONYXKEYS';
6+
import type * as OnyxTypes from '@src/types/onyx';
7+
8+
// `allReports` and `allReportDrafts` are only consumed by submit-time helpers in this module,
9+
// never during render. Onyx.connectWithoutView is appropriate. If React components need these
10+
// values, use useOnyx instead.
11+
12+
let allReports: OnyxCollection<OnyxTypes.Report>;
13+
Onyx.connectWithoutView({
14+
key: ONYXKEYS.COLLECTION.REPORT,
15+
waitForCollectionCallback: true,
16+
callback: (value) => (allReports = value),
17+
});
18+
19+
let allReportDrafts: OnyxCollection<OnyxTypes.Report>;
20+
Onyx.connectWithoutView({
21+
key: ONYXKEYS.COLLECTION.REPORT_DRAFT,
22+
waitForCollectionCallback: true,
23+
callback: (value) => (allReportDrafts = value),
24+
});
25+
26+
/**
27+
* Look up a report by ID across the cached `COLLECTION.REPORT` and `COLLECTION.REPORT_DRAFT`
28+
* collections. Returns the report-draft entry when no concrete report exists for the ID.
29+
*
30+
* Intended for submit-time call sites (e.g. inside `navigateToNextPage`) where the caches are
31+
* guaranteed to be hydrated. For render-time reads where a stale cache would silently misreport
32+
* a value, use `useReportOrReportDraft` instead so the screen re-renders when the report arrives.
33+
*/
34+
function getReportOrReportDraftForAmount(reportID: string | undefined): OnyxEntry<OnyxTypes.Report> {
35+
if (!reportID) {
36+
return undefined;
37+
}
38+
return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? allReportDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`];
39+
}
40+
41+
type GetIsP2PForAmountArgs = {
42+
chatReportForP2P: OnyxEntry<OnyxTypes.Report>;
43+
currentUserAccountID: number | undefined;
44+
};
45+
46+
/**
47+
* Determines whether the first participant of `chatReportForP2P` is a P2P participant.
48+
* The caller is responsible for resolving the correct chat report (e.g. via reactive
49+
* `useReportOrReportDraft` hooks for editing flows where the transaction thread must be
50+
* traversed to the actual chat report).
51+
*/
52+
function getIsP2PForAmount({chatReportForP2P, currentUserAccountID}: GetIsP2PForAmountArgs): boolean {
53+
const firstParticipant = getMoneyRequestParticipantsFromReport(chatReportForP2P, currentUserAccountID).at(0);
54+
return isParticipantP2P(firstParticipant);
55+
}
56+
57+
export {getIsP2PForAmount, getReportOrReportDraftForAmount};

src/pages/iou/request/step/IOURequestStepAmount.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import type {SelectedTabRequest} from '@src/types/onyx';
7575
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
7676
import type Transaction from '@src/types/onyx/Transaction';
7777
import {isEmptyObject} from '@src/types/utils/EmptyObject';
78+
import {getIsP2PForAmount, getReportOrReportDraftForAmount} from './AmountSubmission';
7879
import IOURequestStepCurrencyModal from './IOURequestStepCurrencyModal';
7980
import StepScreenWrapper from './StepScreenWrapper';
8081
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
@@ -123,7 +124,6 @@ function IOURequestStepAmount({
123124
const iouOrExpenseReport = useReportOrReportDraft(report?.chatReportID);
124125
const actualChatReportID = iouOrExpenseReport && isMoneyRequestReport(iouOrExpenseReport) ? iouOrExpenseReport.chatReportID : undefined;
125126
const actualChatReport = useReportOrReportDraft(actualChatReportID);
126-
const transactionAssociatedReport = useReportOrReportDraft(transaction?.reportID);
127127
const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.parentReportID)}`);
128128
const [parentReportNextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${getNonEmptyStringOnyxID(report?.parentReportID)}`);
129129
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
@@ -449,6 +449,7 @@ function IOURequestStepAmount({
449449

450450
// Preserve user's participant selection to avoid forcing them back to default workspace.
451451
const iouReportID = transaction?.reportID;
452+
const transactionAssociatedReport = getReportOrReportDraftForAmount(transaction?.reportID);
452453
const selectedReport = iouReportID === CONST.REPORT.UNREPORTED_REPORT_ID ? selfDMReport : transactionAssociatedReport;
453454
const navigationIOUType = isSelfDM(selectedReport) ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT;
454455
const chatReportID = selectedReport?.chatReportID ?? selectedReport?.reportID;
@@ -575,7 +576,7 @@ function IOURequestStepAmount({
575576
allowFlippingAmount={!isSplitBill && allowNegative}
576577
selectedTab={iouRequestType as SelectedTabRequest}
577578
chatReportID={reportID}
578-
isP2P={isParticipantP2P(getMoneyRequestParticipantsFromReport(chatReportForP2PCheck, currentUserPersonalDetails.accountID).at(0))}
579+
isP2P={getIsP2PForAmount({chatReportForP2P: chatReportForP2PCheck, currentUserAccountID: currentUserPersonalDetails.accountID})}
579580
isCurrencyPressable={!isUnreportedDistanceExpense}
580581
/>
581582
</StepScreenWrapper>

tests/unit/AmountSubmissionTest.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import Onyx from 'react-native-onyx';
2+
import {getIsP2PForAmount, getReportOrReportDraftForAmount} from '@pages/iou/request/step/AmountSubmission';
3+
import CONST from '@src/CONST';
4+
import ONYXKEYS from '@src/ONYXKEYS';
5+
import type {Report} from '@src/types/onyx';
6+
import {createRandomReport} from '../utils/collections/reports';
7+
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
8+
9+
const CURRENT_USER_ACCOUNT_ID = 5;
10+
const OTHER_USER_ACCOUNT_ID = 10;
11+
12+
jest.mock('@src/libs/Navigation/Navigation', () => ({
13+
navigate: jest.fn(),
14+
goBack: jest.fn(),
15+
navigationRef: {
16+
getCurrentRoute: jest.fn(() => undefined),
17+
},
18+
}));
19+
20+
describe('AmountSubmission', () => {
21+
beforeAll(() => {
22+
Onyx.init({keys: ONYXKEYS});
23+
return waitForBatchedUpdates();
24+
});
25+
26+
beforeEach(async () => {
27+
await Onyx.clear();
28+
await waitForBatchedUpdates();
29+
});
30+
31+
describe('getReportOrReportDraftForAmount', () => {
32+
it('returns undefined when reportID is undefined', () => {
33+
expect(getReportOrReportDraftForAmount(undefined)).toBeUndefined();
34+
});
35+
36+
it('returns undefined when reportID is an empty string', () => {
37+
expect(getReportOrReportDraftForAmount('')).toBeUndefined();
38+
});
39+
40+
it('returns the report from COLLECTION.REPORT when it exists', async () => {
41+
const reportID = 'report-1';
42+
const testReport: Report = {...createRandomReport(1, undefined), reportID};
43+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, testReport);
44+
await waitForBatchedUpdates();
45+
46+
const result = getReportOrReportDraftForAmount(reportID);
47+
expect(result?.reportID).toBe(reportID);
48+
});
49+
50+
it('falls back to COLLECTION.REPORT_DRAFT when not in REPORT', async () => {
51+
const reportID = 'draft-1';
52+
const draftReport: Report = {...createRandomReport(2, undefined), reportID};
53+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`, draftReport);
54+
await waitForBatchedUpdates();
55+
56+
const result = getReportOrReportDraftForAmount(reportID);
57+
expect(result?.reportID).toBe(reportID);
58+
});
59+
60+
it('prefers COLLECTION.REPORT over COLLECTION.REPORT_DRAFT when both have the reportID', async () => {
61+
const reportID = 'both-1';
62+
const realReport: Report = {...createRandomReport(3, undefined), reportID, reportName: 'real'};
63+
const draftReport: Report = {...createRandomReport(4, undefined), reportID, reportName: 'draft'};
64+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, realReport);
65+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`, draftReport);
66+
await waitForBatchedUpdates();
67+
68+
const result = getReportOrReportDraftForAmount(reportID);
69+
expect(result?.reportName).toBe('real');
70+
});
71+
72+
it('returns undefined when neither collection has the reportID', () => {
73+
expect(getReportOrReportDraftForAmount('nonexistent')).toBeUndefined();
74+
});
75+
});
76+
77+
describe('getIsP2PForAmount', () => {
78+
it('returns true for a P2P chat with another participant', () => {
79+
const p2pChat: Report = {
80+
...createRandomReport(11, undefined),
81+
participants: {
82+
[CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS},
83+
[OTHER_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS},
84+
},
85+
};
86+
87+
const result = getIsP2PForAmount({chatReportForP2P: p2pChat, currentUserAccountID: CURRENT_USER_ACCOUNT_ID});
88+
expect(result).toBe(true);
89+
});
90+
91+
it('returns false for a self-DM', () => {
92+
const selfDMChat: Report = createRandomReport(12, CONST.REPORT.CHAT_TYPE.SELF_DM);
93+
94+
const result = getIsP2PForAmount({chatReportForP2P: selfDMChat, currentUserAccountID: CURRENT_USER_ACCOUNT_ID});
95+
expect(result).toBe(false);
96+
});
97+
98+
it('returns false for a policy expense chat', () => {
99+
const policyExpenseChat: Report = createRandomReport(13, CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT);
100+
101+
const result = getIsP2PForAmount({chatReportForP2P: policyExpenseChat, currentUserAccountID: CURRENT_USER_ACCOUNT_ID});
102+
expect(result).toBe(false);
103+
});
104+
105+
it('returns false when chatReportForP2P is undefined', () => {
106+
const result = getIsP2PForAmount({chatReportForP2P: undefined, currentUserAccountID: CURRENT_USER_ACCOUNT_ID});
107+
expect(result).toBe(false);
108+
});
109+
110+
it('returns false when the chat report only contains the current user as participant', () => {
111+
const soloReport: Report = {
112+
...createRandomReport(14, undefined),
113+
participants: {
114+
[CURRENT_USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS},
115+
},
116+
};
117+
118+
const result = getIsP2PForAmount({chatReportForP2P: soloReport, currentUserAccountID: CURRENT_USER_ACCOUNT_ID});
119+
expect(result).toBe(false);
120+
});
121+
});
122+
});

0 commit comments

Comments
 (0)