From 33e2b5ea5c8c42fd8165661f5eea7b6b13d20946 Mon Sep 17 00:00:00 2001 From: Nodebrute Date: Mon, 15 Dec 2025 17:30:47 +0500 Subject: [PATCH 1/4] fix merchant field not working --- .../index.native.tsx | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx b/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx index b5ecb1e0a2bf..57330d523926 100644 --- a/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx +++ b/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx @@ -1,7 +1,6 @@ -import type {NavigationAction} from '@react-navigation/native'; +import {usePreventRemove} from '@react-navigation/native'; import React, {memo, useCallback, useRef, useState} from 'react'; import ConfirmModal from '@components/ConfirmModal'; -import useBeforeRemove from '@hooks/useBeforeRemove'; import useLocalize from '@hooks/useLocalize'; import navigationRef from '@libs/Navigation/navigationRef'; import type DiscardChangesConfirmationProps from './types'; @@ -9,21 +8,15 @@ import type DiscardChangesConfirmationProps from './types'; function DiscardChangesConfirmation({getHasUnsavedChanges}: DiscardChangesConfirmationProps) { const {translate} = useLocalize(); const [isVisible, setIsVisible] = useState(false); - const blockedNavigationAction = useRef(undefined); + const shouldAllowNavigation = useRef(false); - useBeforeRemove( - useCallback( - (e) => { - if (!getHasUnsavedChanges()) { - return; - } + const hasUnsavedChanges = getHasUnsavedChanges(); - e.preventDefault(); - blockedNavigationAction.current = e.data.action; - setIsVisible(true); - }, - [getHasUnsavedChanges], - ), + usePreventRemove( + hasUnsavedChanges && !shouldAllowNavigation.current, + useCallback(() => { + setIsVisible(true); + }, []), ); return ( @@ -36,9 +29,8 @@ function DiscardChangesConfirmation({getHasUnsavedChanges}: DiscardChangesConfir cancelText={translate('common.cancel')} onConfirm={() => { setIsVisible(false); - if (blockedNavigationAction.current) { - navigationRef.current?.dispatch(blockedNavigationAction.current); - } + shouldAllowNavigation.current = true; + navigationRef.current?.goBack(); }} onCancel={() => setIsVisible(false)} shouldHandleNavigationBack From a78add70aaa4601d76b1e14f87e34b82a6dc1c84 Mon Sep 17 00:00:00 2001 From: Nodebrute Date: Mon, 15 Dec 2025 22:20:34 +0500 Subject: [PATCH 2/4] show discard modal on swipe back --- .../index.native.tsx | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx b/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx index 57330d523926..f07849dc7865 100644 --- a/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx +++ b/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx @@ -1,6 +1,8 @@ +import type {NavigationAction} from '@react-navigation/native'; import {usePreventRemove} from '@react-navigation/native'; import React, {memo, useCallback, useRef, useState} from 'react'; import ConfirmModal from '@components/ConfirmModal'; +import useBeforeRemove from '@hooks/useBeforeRemove'; import useLocalize from '@hooks/useLocalize'; import navigationRef from '@libs/Navigation/navigationRef'; import type DiscardChangesConfirmationProps from './types'; @@ -9,16 +11,37 @@ function DiscardChangesConfirmation({getHasUnsavedChanges}: DiscardChangesConfir const {translate} = useLocalize(); const [isVisible, setIsVisible] = useState(false); const shouldAllowNavigation = useRef(false); + const blockedNavigationAction = useRef(undefined); const hasUnsavedChanges = getHasUnsavedChanges(); + const shouldPrevent = hasUnsavedChanges && !shouldAllowNavigation.current; + // usePreventRemove prevents navigation at native level to avoid state sync error + // This is critical for swipe gestures on iOS to prevent native/JS state mismatch + // Its callback fires when navigation is prevented and shows the modal usePreventRemove( - hasUnsavedChanges && !shouldAllowNavigation.current, - useCallback(() => { + shouldPrevent, + useCallback(({data}) => { + blockedNavigationAction.current = data.action; setIsVisible(true); }, []), ); + useBeforeRemove( + useCallback( + (e) => { + if (!getHasUnsavedChanges() || shouldAllowNavigation.current || isVisible) { + return; + } + + e.preventDefault(); + blockedNavigationAction.current = e.data.action; + setIsVisible(true); + }, + [getHasUnsavedChanges, isVisible], + ), + ); + return ( { setIsVisible(false); shouldAllowNavigation.current = true; - navigationRef.current?.goBack(); + if (blockedNavigationAction.current) { + navigationRef.current?.dispatch(blockedNavigationAction.current); + blockedNavigationAction.current = undefined; + } else { + navigationRef.current?.goBack(); + } + }} + onCancel={() => { + setIsVisible(false); + blockedNavigationAction.current = undefined; }} - onCancel={() => setIsVisible(false)} shouldHandleNavigationBack /> ); From d30d4c6b8a2f6d564f0ff56a62572516dfff8911 Mon Sep 17 00:00:00 2001 From: Nodebrute Date: Thu, 18 Dec 2025 04:12:43 +0500 Subject: [PATCH 3/4] convert refs to state for usepreventremove reactivity --- .../index.native.tsx | 19 ---------- .../step/IOURequestStepDescription.tsx | 36 +++++++++++------- .../request/step/IOURequestStepMerchant.tsx | 38 +++++++++++-------- 3 files changed, 44 insertions(+), 49 deletions(-) diff --git a/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx b/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx index f07849dc7865..31cae7433fd7 100644 --- a/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx +++ b/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx @@ -2,7 +2,6 @@ import type {NavigationAction} from '@react-navigation/native'; import {usePreventRemove} from '@react-navigation/native'; import React, {memo, useCallback, useRef, useState} from 'react'; import ConfirmModal from '@components/ConfirmModal'; -import useBeforeRemove from '@hooks/useBeforeRemove'; import useLocalize from '@hooks/useLocalize'; import navigationRef from '@libs/Navigation/navigationRef'; import type DiscardChangesConfirmationProps from './types'; @@ -16,9 +15,6 @@ function DiscardChangesConfirmation({getHasUnsavedChanges}: DiscardChangesConfir const hasUnsavedChanges = getHasUnsavedChanges(); const shouldPrevent = hasUnsavedChanges && !shouldAllowNavigation.current; - // usePreventRemove prevents navigation at native level to avoid state sync error - // This is critical for swipe gestures on iOS to prevent native/JS state mismatch - // Its callback fires when navigation is prevented and shows the modal usePreventRemove( shouldPrevent, useCallback(({data}) => { @@ -27,21 +23,6 @@ function DiscardChangesConfirmation({getHasUnsavedChanges}: DiscardChangesConfir }, []), ); - useBeforeRemove( - useCallback( - (e) => { - if (!getHasUnsavedChanges() || shouldAllowNavigation.current || isVisible) { - return; - } - - e.preventDefault(); - blockedNavigationAction.current = e.data.action; - setIsVisible(true); - }, - [getHasUnsavedChanges, isVisible], - ), - ); - return ( { + const navigateBack = useCallback(() => { Navigation.goBack(backTo); - }; + }, [backTo]); + + useEffect(() => { + if (isSaved && shouldNavigateAfterSaveRef.current) { + shouldNavigateAfterSaveRef.current = false; + navigateBack(); + } + }, [isSaved, navigateBack]); const updateDescriptionRef = (value: string) => { - descriptionRef.current = value; + setCurrentDescription(value); }; const updateComment = (value: FormOnyxValues) => { @@ -129,19 +137,18 @@ function IOURequestStepDescription({ return; } - isSavedRef.current = true; const newComment = value.moneyRequestComment.trim(); - // Only update comment if it has changed if (newComment === currentDescriptionInMarkdown) { - navigateBack(); + setIsSaved(true); + shouldNavigateAfterSaveRef.current = true; return; } - // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value if (isEditingSplit) { setDraftSplitTransaction(transaction?.transactionID, splitDraftTransaction, {comment: newComment}); - navigateBack(); + setIsSaved(true); + shouldNavigateAfterSaveRef.current = true; return; } @@ -161,7 +168,8 @@ function IOURequestStepDescription({ ); } - navigateBack(); + setIsSaved(true); + shouldNavigateAfterSaveRef.current = true; }; // eslint-disable-next-line rulesdir/no-negated-variables @@ -218,10 +226,10 @@ function IOURequestStepDescription({ }); }} getHasUnsavedChanges={() => { - if (isSavedRef.current) { + if (isSaved) { return false; } - return descriptionRef.current !== currentDescriptionInMarkdown; + return currentDescription !== currentDescriptionInMarkdown; }} /> diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index 430b9e7a6bb7..3fe729ba2541 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useRef} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -56,8 +56,9 @@ function IOURequestStepMerchant({ const merchant = getTransactionDetails(isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction)?.merchant; const isEmptyMerchant = merchant === '' || merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const initialMerchant = isEmptyMerchant ? '' : merchant; - const merchantRef = useRef(initialMerchant); - const isSavedRef = useRef(false); + const [currentMerchant, setCurrentMerchant] = useState(initialMerchant); + const [isSaved, setIsSaved] = useState(false); + const shouldNavigateAfterSaveRef = useRef(false); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const currentUserAccountIDParam = currentUserPersonalDetails.accountID; const currentUserEmailParam = currentUserPersonalDetails.login ?? ''; @@ -66,9 +67,16 @@ function IOURequestStepMerchant({ const isMerchantRequired = isPolicyExpenseChat(report) || isExpenseRequest(report) || transaction?.participants?.some((participant) => !!participant.isPolicyExpenseChat); - const navigateBack = () => { + const navigateBack = useCallback(() => { Navigation.goBack(backTo); - }; + }, [backTo]); + + useEffect(() => { + if (isSaved && shouldNavigateAfterSaveRef.current) { + shouldNavigateAfterSaveRef.current = false; + navigateBack(); + } + }, [isSaved, navigateBack]); const validate = useCallback( (value: FormOnyxValues) => { @@ -92,27 +100,24 @@ function IOURequestStepMerchant({ ); const updateMerchantRef = (value: string) => { - merchantRef.current = value; + setCurrentMerchant(value); }; const updateMerchant = (value: FormOnyxValues) => { - isSavedRef.current = true; const newMerchant = value.moneyRequestMerchant?.trim(); - // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value if (isEditingSplitBill) { setDraftSplitTransaction(transactionID, splitDraftTransaction, {merchant: newMerchant}); - navigateBack(); + setIsSaved(true); + shouldNavigateAfterSaveRef.current = true; return; } - // In case the merchant hasn't been changed, do not make the API request. - // In case the merchant has been set to empty string while current merchant is partial, do nothing too. if (newMerchant === merchant || (newMerchant === '' && merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT)) { - navigateBack(); + setIsSaved(true); + shouldNavigateAfterSaveRef.current = true; return; } - // When creating/editing an expense, newMerchant can be blank so we fall back on PARTIAL_TRANSACTION_MERCHANT setMoneyRequestMerchant(transactionID, newMerchant || CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, !isEditing); if (isEditing) { updateMoneyRequestMerchant( @@ -127,7 +132,8 @@ function IOURequestStepMerchant({ isASAPSubmitBetaEnabled, ); } - navigateBack(); + setIsSaved(true); + shouldNavigateAfterSaveRef.current = true; }; return ( @@ -171,10 +177,10 @@ function IOURequestStepMerchant({ }); }} getHasUnsavedChanges={() => { - if (isSavedRef.current) { + if (isSaved) { return false; } - return merchantRef.current !== initialMerchant; + return currentMerchant !== initialMerchant; }} /> From 993663b8b09b0d05b64b512f01c9c7e9856b61a3 Mon Sep 17 00:00:00 2001 From: Nodebrute Date: Thu, 18 Dec 2025 04:35:00 +0500 Subject: [PATCH 4/4] fix lint errors --- src/pages/iou/request/step/IOURequestStepDescription.tsx | 7 ++++--- src/pages/iou/request/step/IOURequestStepMerchant.tsx | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepDescription.tsx b/src/pages/iou/request/step/IOURequestStepDescription.tsx index 5d48676df32b..c9502d730949 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.tsx +++ b/src/pages/iou/request/step/IOURequestStepDescription.tsx @@ -122,10 +122,11 @@ function IOURequestStepDescription({ }, [backTo]); useEffect(() => { - if (isSaved && shouldNavigateAfterSaveRef.current) { - shouldNavigateAfterSaveRef.current = false; - navigateBack(); + if (!isSaved || !shouldNavigateAfterSaveRef.current) { + return; } + shouldNavigateAfterSaveRef.current = false; + navigateBack(); }, [isSaved, navigateBack]); const updateDescriptionRef = (value: string) => { diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index 3fe729ba2541..fe960e38e1d2 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -72,10 +72,11 @@ function IOURequestStepMerchant({ }, [backTo]); useEffect(() => { - if (isSaved && shouldNavigateAfterSaveRef.current) { - shouldNavigateAfterSaveRef.current = false; - navigateBack(); + if (!isSaved || !shouldNavigateAfterSaveRef.current) { + return; } + shouldNavigateAfterSaveRef.current = false; + navigateBack(); }, [isSaved, navigateBack]); const validate = useCallback(