Skip to content

Commit facd48d

Browse files
Merge branch 'Expensify:main' into knip-audit-export-1
2 parents 2a8ad71 + 6361f34 commit facd48d

72 files changed

Lines changed: 1033 additions & 1836 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/components/Hoverable/ActiveHoverable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {cloneElement, useCallback, useEffect, useMemo, useRef, useState} from 'react';
22
import {DeviceEventEmitter} from 'react-native';
3+
import getReturnValue from '@libs/getReturnValue';
34
import mergeRefs from '@libs/mergeRefs';
4-
import {getReturnValue} from '@libs/ValueUtils';
55
import CONST from '@src/CONST';
66
import type HoverableProps from './types';
77

src/components/Hoverable/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, {cloneElement} from 'react';
22
import {hasHoverSupport} from '@libs/DeviceCapabilities';
3+
import getReturnValue from '@libs/getReturnValue';
34
import mergeRefs from '@libs/mergeRefs';
4-
import {getReturnValue} from '@libs/ValueUtils';
55
import ActiveHoverable from './ActiveHoverable';
66
import type HoverableProps from './types';
77

src/components/ReportActionItem/TripRoomPreview.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ function TripRoomPreview({
206206
{reservationsData.length > 0 && (
207207
<FlatList
208208
data={reservationsData}
209-
style={[styles.gap4, styles.border, styles.borderRadiusComponentLarge, styles.p4]}
209+
style={[styles.border, styles.borderRadiusComponentLarge, styles.p4, styles.flexGrow0]}
210+
contentContainerStyle={styles.gap4}
210211
renderItem={renderItem}
211212
/>
212213
)}

src/hooks/useBeforeRemove.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@ import type {EventListenerCallback, EventMapCore, NavigationState} from '@react-
33
import {useEffect} from 'react';
44

55
// beforeRemove have some limitations. When the react-navigation is upgraded to 7.x, update this to use usePreventRemove hook.
6-
const useBeforeRemove = (onBeforeRemove: EventListenerCallback<EventMapCore<NavigationState>, 'beforeRemove'>, isEnabled = true) => {
6+
const useBeforeRemove = (onBeforeRemove: EventListenerCallback<EventMapCore<NavigationState>, 'beforeRemove'>) => {
77
const navigation = useNavigation();
88

99
useEffect(() => {
10-
if (!isEnabled) {
11-
return undefined;
12-
}
1310
const unsubscribe = navigation.addListener('beforeRemove', onBeforeRemove);
1411
return unsubscribe;
15-
}, [navigation, onBeforeRemove, isEnabled]);
12+
}, [navigation, onBeforeRemove]);
1613
};
1714

1815
export default useBeforeRemove;

src/hooks/useDiscardChangesConfirmation/index.native.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import type {NavigationAction} from '@react-navigation/native';
2-
import {useIsFocused, usePreventRemove} from '@react-navigation/native';
2+
import {usePreventRemove} from '@react-navigation/native';
33
import {useCallback, useRef, useState} from 'react';
44
import {ModalActions} from '@components/Modal/Global/ModalContext';
55
import useConfirmModal from '@hooks/useConfirmModal';
66
import useLocalize from '@hooks/useLocalize';
7+
import Log from '@libs/Log';
78
import navigationRef from '@libs/Navigation/navigationRef';
89
import type UseDiscardChangesConfirmationOptions from './types';
910

10-
function useDiscardChangesConfirmation({getHasUnsavedChanges, onVisibilityChange, isEnabled = true}: UseDiscardChangesConfirmationOptions) {
11+
function useDiscardChangesConfirmation({getHasUnsavedChanges, onCancel, onVisibilityChange, onConfirm}: UseDiscardChangesConfirmationOptions) {
1112
const {translate} = useLocalize();
12-
const isFocused = useIsFocused();
1313
const {showConfirmModal} = useConfirmModal();
1414
const [shouldAllowNavigation, setShouldAllowNavigation] = useState(false);
1515
const blockedNavigationAction = useRef<NavigationAction | undefined>(undefined);
1616

17-
const shouldPrevent = isEnabled && isFocused && !shouldAllowNavigation;
17+
const shouldPrevent = !shouldAllowNavigation;
1818

1919
usePreventRemove(
2020
shouldPrevent,
@@ -36,18 +36,28 @@ function useDiscardChangesConfirmation({getHasUnsavedChanges, onVisibilityChange
3636
}).then((result) => {
3737
onVisibilityChange?.(false);
3838
if (result.action !== ModalActions.CONFIRM) {
39+
onCancel?.();
3940
return;
4041
}
41-
setShouldAllowNavigation(true);
42-
if (blockedNavigationAction.current) {
43-
navigationRef.current?.dispatch(blockedNavigationAction.current);
44-
blockedNavigationAction.current = undefined;
45-
} else {
46-
navigationRef.current?.goBack();
47-
}
42+
const confirmNavigation = () => {
43+
setShouldAllowNavigation(true);
44+
if (blockedNavigationAction.current) {
45+
navigationRef.current?.dispatch(blockedNavigationAction.current);
46+
blockedNavigationAction.current = undefined;
47+
} else {
48+
navigationRef.current?.goBack();
49+
}
50+
};
51+
Promise.resolve()
52+
.then(() => onConfirm?.())
53+
.then(confirmNavigation)
54+
.catch((error: unknown) => {
55+
Log.warn('[useDiscardChangesConfirmation] Failed to run onConfirm callback', {error});
56+
blockedNavigationAction.current = undefined;
57+
});
4858
});
4959
},
50-
[getHasUnsavedChanges, onVisibilityChange, showConfirmModal, translate],
60+
[getHasUnsavedChanges, onCancel, onVisibilityChange, onConfirm, showConfirmModal, translate],
5161
),
5262
);
5363
}
Lines changed: 24 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
import type {NavigationAction} from '@react-navigation/native';
2-
import {useIsFocused, useNavigation} from '@react-navigation/native';
2+
import {useNavigation} from '@react-navigation/native';
33
import {useCallback, useEffect, useRef} from 'react';
44
import {ModalActions} from '@components/Modal/Global/ModalContext';
55
import useBeforeRemove from '@hooks/useBeforeRemove';
66
import useConfirmModal from '@hooks/useConfirmModal';
77
import useLocalize from '@hooks/useLocalize';
8+
import Log from '@libs/Log';
89
import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNavigationActionToMicrotaskQueue';
910
import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction';
1011
import navigationRef from '@libs/Navigation/navigationRef';
1112
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
1213
import type {RootNavigatorParamList} from '@libs/Navigation/types';
1314
import type UseDiscardChangesConfirmationOptions from './types';
1415

15-
function useDiscardChangesConfirmation({getHasUnsavedChanges, onCancel, onVisibilityChange, isEnabled = true}: UseDiscardChangesConfirmationOptions) {
16+
function useDiscardChangesConfirmation({getHasUnsavedChanges, onCancel, onVisibilityChange, onConfirm}: UseDiscardChangesConfirmationOptions) {
1617
const navigation = useNavigation<PlatformStackNavigationProp<RootNavigatorParamList>>();
17-
const isFocused = useIsFocused();
1818
const {translate} = useLocalize();
19-
const {showConfirmModal, closeModal} = useConfirmModal();
19+
const {showConfirmModal} = useConfirmModal();
2020
const blockedNavigationAction = useRef<NavigationAction>(undefined);
2121
const shouldNavigateBack = useRef(false);
22-
const isDiscardModalOpenRef = useRef(false);
2322

2423
const navigateBack = useCallback(() => {
2524
if (blockedNavigationAction.current) {
@@ -34,7 +33,6 @@ function useDiscardChangesConfirmation({getHasUnsavedChanges, onCancel, onVisibi
3433

3534
const showDiscardModal = useCallback(() => {
3635
onVisibilityChange?.(true);
37-
isDiscardModalOpenRef.current = true;
3836
showConfirmModal({
3937
title: translate('discardChangesConfirmation.title'),
4038
prompt: translate('discardChangesConfirmation.body'),
@@ -43,43 +41,41 @@ function useDiscardChangesConfirmation({getHasUnsavedChanges, onCancel, onVisibi
4341
cancelText: translate('common.cancel'),
4442
shouldIgnoreBackHandlerDuringTransition: true,
4543
}).then((result) => {
46-
isDiscardModalOpenRef.current = false;
4744
onVisibilityChange?.(false);
4845
if (result.action === ModalActions.CONFIRM) {
49-
setNavigationActionToMicrotaskQueue(navigateBack);
46+
Promise.resolve()
47+
.then(() => onConfirm?.())
48+
.then(() => {
49+
setNavigationActionToMicrotaskQueue(navigateBack);
50+
})
51+
.catch((error: unknown) => {
52+
Log.warn('[useDiscardChangesConfirmation] Failed to run onConfirm callback', {error});
53+
blockedNavigationAction.current = undefined;
54+
shouldNavigateBack.current = false;
55+
});
5056
} else {
5157
blockedNavigationAction.current = undefined;
5258
shouldNavigateBack.current = false;
5359
onCancel?.();
5460
}
5561
});
56-
}, [showConfirmModal, translate, navigateBack, onCancel, onVisibilityChange]);
62+
}, [showConfirmModal, translate, navigateBack, onCancel, onConfirm, onVisibilityChange]);
5763

58-
useBeforeRemove(
59-
useCallback(
60-
(e) => {
61-
if (!isEnabled || !isFocused || !getHasUnsavedChanges() || shouldNavigateBack.current) {
62-
return;
63-
}
64-
65-
e.preventDefault();
66-
blockedNavigationAction.current = e.data.action;
67-
navigateAfterInteraction(showDiscardModal);
68-
},
69-
[getHasUnsavedChanges, isFocused, isEnabled, showDiscardModal],
70-
),
71-
isEnabled && isFocused,
72-
);
64+
useBeforeRemove((e) => {
65+
if (!getHasUnsavedChanges() || shouldNavigateBack.current) {
66+
return;
67+
}
68+
e.preventDefault();
69+
blockedNavigationAction.current = e.data.action;
70+
navigateAfterInteraction(showDiscardModal);
71+
});
7372

7473
/**
7574
* We cannot programmatically stop the browser's back navigation like react-navigation's beforeRemove.
7675
* Events like popstate and transitionStart are triggered AFTER the back navigation has already completed.
7776
* So we need to go forward to get back to the current page.
7877
*/
7978
useEffect(() => {
80-
if (!isEnabled || !isFocused) {
81-
return undefined;
82-
}
8379
const unsubscribe = navigation.addListener('transitionStart', ({data: {closing}}) => {
8480
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
8581
if (!getHasUnsavedChanges()) {
@@ -95,20 +91,7 @@ function useDiscardChangesConfirmation({getHasUnsavedChanges, onCancel, onVisibi
9591
});
9692

9793
return unsubscribe;
98-
}, [navigation, getHasUnsavedChanges, isFocused, isEnabled, showDiscardModal]);
99-
100-
/**
101-
* When the screen loses focus (or is disabled) while the discard modal is open,
102-
* close the modal and reset refs so we don't leave the modal visible or stale state.
103-
*/
104-
useEffect(() => {
105-
if ((isFocused && isEnabled) || !isDiscardModalOpenRef.current) {
106-
return;
107-
}
108-
closeModal();
109-
blockedNavigationAction.current = undefined;
110-
shouldNavigateBack.current = false;
111-
}, [isFocused, isEnabled, closeModal]);
94+
}, [navigation, getHasUnsavedChanges, showDiscardModal]);
11295
}
11396

11497
export default useDiscardChangesConfirmation;

src/hooks/useDiscardChangesConfirmation/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ type UseDiscardChangesConfirmationOptions = {
22
getHasUnsavedChanges: () => boolean;
33
onCancel?: () => void;
44
onVisibilityChange?: (visible: boolean) => void;
5-
isEnabled?: boolean;
5+
onConfirm?: () => void | Promise<void>;
66
};
77

88
export default UseDiscardChangesConfirmationOptions;

src/hooks/useResetIOUType.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import {useFocusEffect} from '@react-navigation/native';
2+
import {hasOnlyPersonalPoliciesSelector} from '@selectors/Policy';
3+
import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft';
4+
import {useRef} from 'react';
5+
import {Keyboard} from 'react-native';
6+
import type {OnyxEntry} from 'react-native-onyx';
7+
import type {IOURequestType} from '@userActions/IOU';
8+
import {initMoneyRequest} from '@userActions/IOU';
9+
import CONST from '@src/CONST';
10+
import ONYXKEYS from '@src/ONYXKEYS';
11+
import type {Policy, Report, Transaction} from '@src/types/onyx';
12+
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
13+
import useOnyx from './useOnyx';
14+
import usePersonalPolicy from './usePersonalPolicy';
15+
import usePrevious from './usePrevious';
16+
17+
type UseResetIOUTypeParams = {
18+
/** The report ID from the route params */
19+
reportID: string;
20+
21+
/** The current report object */
22+
report: OnyxEntry<Report>;
23+
24+
/** The current draft transaction */
25+
transaction: OnyxEntry<Transaction>;
26+
27+
/** Whether the transaction data is still loading */
28+
isLoadingTransaction?: boolean;
29+
30+
/** Whether the selected tab data is still loading */
31+
isLoadingSelectedTab?: boolean;
32+
33+
/** The current transaction request type derived from tab/transaction state */
34+
transactionRequestType: IOURequestType | undefined;
35+
36+
/** The policy resolved for this transaction */
37+
policy?: OnyxEntry<Policy>;
38+
39+
/** Whether this is a track distance expense */
40+
isTrackDistanceExpense?: boolean;
41+
42+
/** Whether to skip keyboard dismiss for per diem tab */
43+
skipKeyboardDismissForPerDiem?: boolean;
44+
};
45+
46+
/**
47+
* Shared hook that encapsulates the tab-reset logic duplicated between
48+
* `IOURequestStartPage` and `DistanceRequestStartPage`.
49+
*/
50+
function useResetIOUType({
51+
reportID,
52+
report,
53+
transaction,
54+
isLoadingTransaction = false,
55+
isLoadingSelectedTab = false,
56+
transactionRequestType,
57+
policy,
58+
isTrackDistanceExpense = false,
59+
skipKeyboardDismissForPerDiem = false,
60+
}: UseResetIOUTypeParams): (newIOUType: IOURequestType) => void {
61+
const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`);
62+
const [hasOnlyPersonalPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: hasOnlyPersonalPoliciesSelector});
63+
const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES);
64+
const [currentDate] = useOnyx(ONYXKEYS.CURRENT_DATE);
65+
const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector});
66+
67+
const personalPolicy = usePersonalPolicy();
68+
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
69+
70+
const resetIOUTypeIfChanged = (newIOUType: IOURequestType) => {
71+
if (!(skipKeyboardDismissForPerDiem && newIOUType === CONST.IOU.REQUEST_TYPE.PER_DIEM)) {
72+
Keyboard.dismiss();
73+
}
74+
75+
if (transaction?.iouRequestType === newIOUType) {
76+
return;
77+
}
78+
79+
const isFromGlobalCreate = !report?.reportID;
80+
81+
initMoneyRequest({
82+
reportID,
83+
policy,
84+
personalPolicy,
85+
isFromGlobalCreate,
86+
isTrackDistanceExpense,
87+
isFromFloatingActionButton: transaction?.isFromFloatingActionButton ?? transaction?.isFromGlobalCreate ?? isFromGlobalCreate,
88+
currentIouRequestType: transaction?.iouRequestType,
89+
newIouRequestType: newIOUType,
90+
report,
91+
parentReport,
92+
currentDate,
93+
lastSelectedDistanceRates,
94+
currentUserPersonalDetails,
95+
hasOnlyPersonalPolicies: hasOnlyPersonalPolicies ?? true,
96+
draftTransactionIDs,
97+
});
98+
};
99+
100+
const tabSelectedTypeRef = useRef<IOURequestType | null>(null);
101+
102+
const onTabSelected = (newIouType: IOURequestType) => {
103+
tabSelectedTypeRef.current = newIouType;
104+
resetIOUTypeIfChanged(newIouType);
105+
};
106+
107+
const prevTransactionReportID = usePrevious(transaction?.reportID);
108+
const personalPolicyID = personalPolicy?.id;
109+
110+
// Clear out the temporary expense if the reportID in the URL has changed from the transaction's reportID.
111+
useFocusEffect(() => {
112+
// Skip until transactionRequestType catches up with the tab onTabSelected already set.
113+
if (tabSelectedTypeRef.current && transactionRequestType !== tabSelectedTypeRef.current) {
114+
return;
115+
}
116+
tabSelectedTypeRef.current = null;
117+
118+
// The test transaction can change the reportID of the transaction on the flow so we should prevent the reportID from being reverted again.
119+
if (
120+
transaction?.reportID === reportID ||
121+
isLoadingTransaction ||
122+
isLoadingSelectedTab ||
123+
!transactionRequestType ||
124+
prevTransactionReportID !== transaction?.reportID ||
125+
!personalPolicyID
126+
) {
127+
return;
128+
}
129+
resetIOUTypeIfChanged(transactionRequestType);
130+
});
131+
132+
return onTabSelected;
133+
}
134+
135+
export default useResetIOUType;

0 commit comments

Comments
 (0)