From e68fbae1dc18cc60d2db02647e6318f968766979 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Mon, 22 Dec 2025 21:50:54 +0700 Subject: [PATCH 1/3] fix: migrate useBeforeRemove usage in DiscardChangesConfirmation --- .../index.native.tsx | 23 +++++--------- .../step/DiscardChangesConfirmation/index.tsx | 31 ++++++------------- .../step/DiscardChangesConfirmation/types.ts | 2 +- .../step/IOURequestStepDescription.tsx | 29 ++++++++--------- .../request/step/IOURequestStepMerchant.tsx | 31 ++++++++++--------- .../settings/Profile/Avatar/AvatarPage.tsx | 14 ++++----- 6 files changed, 57 insertions(+), 73 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..13ea34591940 100644 --- a/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx +++ b/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx @@ -1,29 +1,22 @@ +import {usePreventRemove} from '@react-navigation/native'; import type {NavigationAction} 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'; -function DiscardChangesConfirmation({getHasUnsavedChanges}: DiscardChangesConfirmationProps) { +function DiscardChangesConfirmation({hasUnsavedChanges}: DiscardChangesConfirmationProps) { const {translate} = useLocalize(); const [isVisible, setIsVisible] = useState(false); const blockedNavigationAction = useRef(undefined); - useBeforeRemove( - useCallback( - (e) => { - if (!getHasUnsavedChanges()) { - return; - } - - e.preventDefault(); - blockedNavigationAction.current = e.data.action; - setIsVisible(true); - }, - [getHasUnsavedChanges], - ), + usePreventRemove( + hasUnsavedChanges, + useCallback((e) => { + blockedNavigationAction.current = e.data.action; + setIsVisible(true); + }, []), ); return ( diff --git a/src/pages/iou/request/step/DiscardChangesConfirmation/index.tsx b/src/pages/iou/request/step/DiscardChangesConfirmation/index.tsx index 9b213b878e81..9ccdd813cf1b 100644 --- a/src/pages/iou/request/step/DiscardChangesConfirmation/index.tsx +++ b/src/pages/iou/request/step/DiscardChangesConfirmation/index.tsx @@ -1,8 +1,7 @@ import type {NavigationAction} from '@react-navigation/native'; -import {useNavigation} from '@react-navigation/native'; +import {useNavigation, usePreventRemove} from '@react-navigation/native'; import React, {memo, useCallback, useEffect, useRef, useState} from 'react'; import ConfirmModal from '@components/ConfirmModal'; -import useBeforeRemove from '@hooks/useBeforeRemove'; import useLocalize from '@hooks/useLocalize'; import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNavigationActionToMicrotaskQueue'; import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; @@ -11,7 +10,7 @@ import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNa import type {RootNavigatorParamList} from '@libs/Navigation/types'; import type DiscardChangesConfirmationProps from './types'; -function DiscardChangesConfirmation({getHasUnsavedChanges, onCancel}: DiscardChangesConfirmationProps) { +function DiscardChangesConfirmation({hasUnsavedChanges, onCancel}: DiscardChangesConfirmationProps) { const navigation = useNavigation>(); const {translate} = useLocalize(); const [isVisible, setIsVisible] = useState(false); @@ -19,19 +18,12 @@ function DiscardChangesConfirmation({getHasUnsavedChanges, onCancel}: DiscardCha const shouldNavigateBack = useRef(false); const isConfirmed = useRef(false); - useBeforeRemove( - useCallback( - (e) => { - if (!getHasUnsavedChanges() || shouldNavigateBack.current) { - return; - } - - e.preventDefault(); - blockedNavigationAction.current = e.data.action; - navigateAfterInteraction(() => setIsVisible((prev) => !prev)); - }, - [getHasUnsavedChanges], - ), + usePreventRemove( + hasUnsavedChanges || shouldNavigateBack.current, + useCallback((e) => { + blockedNavigationAction.current = e.data.action; + navigateAfterInteraction(() => setIsVisible((prev) => !prev)); + }, []), ); /** @@ -40,11 +32,8 @@ function DiscardChangesConfirmation({getHasUnsavedChanges, onCancel}: DiscardCha * So we need to go forward to get back to the current page */ useEffect(() => { - // transitionStart is triggered before the previous page is fully loaded so RHP sliding animation - // could be less "glitchy" when going back and forth between the previous and current pages const unsubscribe = navigation.addListener('transitionStart', ({data: {closing}}) => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (!getHasUnsavedChanges()) { + if (!hasUnsavedChanges) { return; } shouldNavigateBack.current = true; @@ -58,7 +47,7 @@ function DiscardChangesConfirmation({getHasUnsavedChanges, onCancel}: DiscardCha }); return unsubscribe; - }, [navigation, getHasUnsavedChanges]); + }, [hasUnsavedChanges, navigation]); const navigateBack = useCallback(() => { if (blockedNavigationAction.current) { diff --git a/src/pages/iou/request/step/DiscardChangesConfirmation/types.ts b/src/pages/iou/request/step/DiscardChangesConfirmation/types.ts index 2fdd9d24b5f2..04f007f916a6 100644 --- a/src/pages/iou/request/step/DiscardChangesConfirmation/types.ts +++ b/src/pages/iou/request/step/DiscardChangesConfirmation/types.ts @@ -1,6 +1,6 @@ type DiscardChangesConfirmationProps = { - getHasUnsavedChanges: () => boolean; onCancel?: () => void; + hasUnsavedChanges: boolean; }; export default DiscardChangesConfirmationProps; diff --git a/src/pages/iou/request/step/IOURequestStepDescription.tsx b/src/pages/iou/request/step/IOURequestStepDescription.tsx index 204191ea1c26..48619af47f99 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.tsx +++ b/src/pages/iou/request/step/IOURequestStepDescription.tsx @@ -1,5 +1,5 @@ import lodashIsEmpty from 'lodash/isEmpty'; -import React, {useCallback, useMemo, useRef} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -69,8 +69,7 @@ function IOURequestStepDescription({ return isEditingSplit && !lodashIsEmpty(splitDraftTransaction) ? (splitDraftTransaction?.comment?.comment ?? '') : (transaction?.comment?.comment ?? ''); }, [isTransactionDraft, iouType, isEditingSplit, splitDraftTransaction, transaction?.comment?.comment]); - const descriptionRef = useRef(currentDescriptionInMarkdown); - const isSavedRef = useRef(false); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); useRestartOnReceiptFailure(transaction, reportID, iouType, action); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const currentUserAccountIDParam = currentUserPersonalDetails.accountID; @@ -120,16 +119,23 @@ function IOURequestStepDescription({ Navigation.goBack(backTo); }; - const updateDescriptionRef = (value: string) => { - descriptionRef.current = value; - }; + const updateHasUnsavedChanges = useCallback( + (value: string) => { + if (value === currentDescriptionInMarkdown) { + setHasUnsavedChanges(false); + return; + } + setHasUnsavedChanges(true); + }, + [currentDescriptionInMarkdown], + ); const updateComment = (value: FormOnyxValues) => { if (!transaction?.transactionID) { return; } - isSavedRef.current = true; + setHasUnsavedChanges(false); const newComment = value.moneyRequestComment.trim(); // Only update comment if it has changed @@ -196,7 +202,7 @@ function IOURequestStepDescription({ inputID={INPUT_IDS.MONEY_REQUEST_COMMENT} name={INPUT_IDS.MONEY_REQUEST_COMMENT} defaultValue={currentDescriptionInMarkdown} - onValueChange={updateDescriptionRef} + onValueChange={updateHasUnsavedChanges} label={translate('moneyRequestConfirmationList.whatsItFor')} accessibilityLabel={translate('moneyRequestConfirmationList.whatsItFor')} role={CONST.ROLE.PRESENTATION} @@ -217,12 +223,7 @@ function IOURequestStepDescription({ inputRef.current?.focus(); }); }} - getHasUnsavedChanges={() => { - if (isSavedRef.current) { - return false; - } - return descriptionRef.current !== currentDescriptionInMarkdown; - }} + hasUnsavedChanges={hasUnsavedChanges} /> ); diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index 430b9e7a6bb7..dfbe0b739987 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, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -56,13 +56,23 @@ 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 currentUserPersonalDetails = useCurrentUserPersonalDetails(); const currentUserAccountIDParam = currentUserPersonalDetails.accountID; const currentUserEmailParam = currentUserPersonalDetails.login ?? ''; const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + + const updateHasUnsavedChanges = useCallback( + (value: string) => { + if (value === initialMerchant) { + setHasUnsavedChanges(false); + return; + } + setHasUnsavedChanges(true); + }, + [initialMerchant], + ); const isMerchantRequired = isPolicyExpenseChat(report) || isExpenseRequest(report) || transaction?.participants?.some((participant) => !!participant.isPolicyExpenseChat); @@ -91,12 +101,8 @@ function IOURequestStepMerchant({ [isMerchantRequired, translate], ); - const updateMerchantRef = (value: string) => { - merchantRef.current = value; - }; - const updateMerchant = (value: FormOnyxValues) => { - isSavedRef.current = true; + setHasUnsavedChanges(false); const newMerchant = value.moneyRequestMerchant?.trim(); // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value @@ -155,7 +161,7 @@ function IOURequestStepMerchant({ inputID={INPUT_IDS.MONEY_REQUEST_MERCHANT} name={INPUT_IDS.MONEY_REQUEST_MERCHANT} defaultValue={initialMerchant} - onValueChange={updateMerchantRef} + onValueChange={updateHasUnsavedChanges} label={translate('common.merchant')} accessibilityLabel={translate('common.merchant')} role={CONST.ROLE.PRESENTATION} @@ -170,12 +176,7 @@ function IOURequestStepMerchant({ inputRef.current?.focus(); }); }} - getHasUnsavedChanges={() => { - if (isSavedRef.current) { - return false; - } - return merchantRef.current !== initialMerchant; - }} + hasUnsavedChanges={hasUnsavedChanges} /> ); diff --git a/src/pages/settings/Profile/Avatar/AvatarPage.tsx b/src/pages/settings/Profile/Avatar/AvatarPage.tsx index 5e0023900638..f1714fb824a3 100644 --- a/src/pages/settings/Profile/Avatar/AvatarPage.tsx +++ b/src/pages/settings/Profile/Avatar/AvatarPage.tsx @@ -51,7 +51,7 @@ function ProfileAvatar() { const [selected, setSelected] = useState(); const avatarCaptureRef = useRef(null); - const isSavingRef = useRef(false); + const [isSaving, setIsSaving] = useState(false); const icons = useMemoizedLazyExpensifyIcons(['Upload']); const styles = useThemeStyles(); @@ -149,7 +149,7 @@ function ProfileAvatar() { }); const onPress = useCallback(() => { - isSavingRef.current = true; + setIsSaving(true); if (imageData.file) { updateAvatar(imageData.file, { @@ -159,7 +159,7 @@ function ProfileAvatar() { }); setImageData({...EMPTY_FILE}); Navigation.dismissModal(); - isSavingRef.current = false; + setIsSaving(false); return; } @@ -178,11 +178,11 @@ function ProfileAvatar() { ); setSelected(undefined); Navigation.dismissModal(); - isSavingRef.current = false; + setIsSaving(false); return; } if (!selected || !avatarCaptureRef.current) { - isSavingRef.current = false; + setIsSaving(false); return; } // User selected a letter avatar @@ -195,7 +195,7 @@ function ProfileAvatar() { setSelected(undefined); setImageData({...EMPTY_FILE}); Navigation.dismissModal(); - isSavingRef.current = false; + setIsSaving(false); }); }, [currentUserPersonalDetails?.accountID, currentUserPersonalDetails?.avatar, currentUserPersonalDetails?.avatarThumbnail, imageData.file, selected]); @@ -313,7 +313,7 @@ function ProfileAvatar() { imageType={cropImageData.type} buttonLabel={translate('avatarPage.upload')} /> - !isSavingRef.current && isDirty} /> + ); } From ec90dab076f5b910c47a3f9a3531720e69e1912f Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Tue, 6 Jan 2026 15:48:27 +0700 Subject: [PATCH 2/3] refactor deprecated --- src/pages/iou/request/step/IOURequestStepDescription.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepDescription.tsx b/src/pages/iou/request/step/IOURequestStepDescription.tsx index adb435af771d..e14852606317 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.tsx +++ b/src/pages/iou/request/step/IOURequestStepDescription.tsx @@ -22,7 +22,6 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {shouldUseTransactionDraft} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; -import {getPersonalPolicy} from '@libs/PolicyUtils'; import variables from '@styles/variables'; import {setDraftSplitTransaction, setMoneyRequestDescription, updateMoneyRequestDescription} from '@userActions/IOU'; import CONST from '@src/CONST'; @@ -163,7 +162,8 @@ function IOURequestStepDescription({ // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useShowNotFoundPageInIOUStep(action, iouType, reportActionID, report, transaction); - const isReportInGroupPolicy = !!report?.policyID && report.policyID !== CONST.POLICY.ID_FAKE && getPersonalPolicy()?.id !== report.policyID; + const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true}); + const isReportInGroupPolicy = !!report?.policyID && report.policyID !== CONST.POLICY.ID_FAKE && personalPolicyID !== report.policyID; const getDescriptionHint = () => { return transaction?.category && policyCategories ? (policyCategories[transaction?.category]?.commentHint ?? '') : ''; }; From 7cdf4c5d199c491a290ad0fe74a56543de074268 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Tue, 3 Mar 2026 12:31:25 +0700 Subject: [PATCH 3/3] fix: navigation back issue --- .../request/step/DiscardChangesConfirmation/index.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pages/iou/request/step/DiscardChangesConfirmation/index.tsx b/src/pages/iou/request/step/DiscardChangesConfirmation/index.tsx index 2881a48d302a..25243e67f430 100644 --- a/src/pages/iou/request/step/DiscardChangesConfirmation/index.tsx +++ b/src/pages/iou/request/step/DiscardChangesConfirmation/index.tsx @@ -17,12 +17,13 @@ function DiscardChangesConfirmation({hasUnsavedChanges, onCancel}: DiscardChange const blockedNavigationAction = useRef(undefined); const [shouldNavigateBack, setShouldNavigateBack] = useState(false); const isConfirmed = useRef(false); + const [discardConfirmed, setDiscardConfirmed] = useState(false); usePreventRemove( - hasUnsavedChanges || shouldNavigateBack, + (hasUnsavedChanges || shouldNavigateBack) && !discardConfirmed, useCallback((e) => { blockedNavigationAction.current = e.data.action; - navigateAfterInteraction(() => setIsVisible((prev) => !prev)); + navigateAfterInteraction(() => setIsVisible(true)); }, []), ); @@ -33,7 +34,7 @@ function DiscardChangesConfirmation({hasUnsavedChanges, onCancel}: DiscardChange */ useEffect(() => { const unsubscribe = navigation.addListener('transitionStart', ({data: {closing}}) => { - if (!hasUnsavedChanges) { + if (!hasUnsavedChanges || isConfirmed.current) { return; } setShouldNavigateBack(true); @@ -43,7 +44,7 @@ function DiscardChangesConfirmation({hasUnsavedChanges, onCancel}: DiscardChange } // Navigation.navigate() rerenders the current page and resets its states window.history.go(1); - navigateAfterInteraction(() => setIsVisible((prev) => !prev)); + navigateAfterInteraction(() => setIsVisible(true)); }); return unsubscribe; @@ -70,6 +71,7 @@ function DiscardChangesConfirmation({hasUnsavedChanges, onCancel}: DiscardChange cancelText={translate('common.cancel')} onConfirm={() => { isConfirmed.current = true; + setDiscardConfirmed(true); setIsVisible(false); }} onCancel={() => { @@ -79,7 +81,6 @@ function DiscardChangesConfirmation({hasUnsavedChanges, onCancel}: DiscardChange }} onModalHide={() => { if (isConfirmed.current) { - isConfirmed.current = false; setNavigationActionToMicrotaskQueue(navigateBack); } else { setShouldNavigateBack(false);