Skip to content
Merged
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
22 changes: 15 additions & 7 deletions src/pages/iou/request/step/DiscardChangesConfirmation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ 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 navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction';
import navigationRef from '@libs/Navigation/navigationRef';
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {RootNavigatorParamList} from '@libs/Navigation/types';
import type DiscardChangesConfirmationProps from './types';

function DiscardChangesConfirmation({getHasUnsavedChanges}: DiscardChangesConfirmationProps) {
function DiscardChangesConfirmation({getHasUnsavedChanges, onCancel}: DiscardChangesConfirmationProps) {
const navigation = useNavigation<PlatformStackNavigationProp<RootNavigatorParamList>>();
const {translate} = useLocalize();
const [isVisible, setIsVisible] = useState(false);
Expand All @@ -25,7 +26,7 @@ function DiscardChangesConfirmation({getHasUnsavedChanges}: DiscardChangesConfir

e.preventDefault();
blockedNavigationAction.current = e.data.action;
setIsVisible(true);
navigateAfterInteraction(() => setIsVisible((prev) => !prev));
},
[getHasUnsavedChanges],
),
Expand All @@ -39,16 +40,19 @@ function DiscardChangesConfirmation({getHasUnsavedChanges}: DiscardChangesConfir
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', () => {
const unsubscribe = navigation.addListener('transitionStart', ({data: {closing}}) => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (!getHasUnsavedChanges() || blockedNavigationAction.current || shouldNavigateBack.current) {
if (!getHasUnsavedChanges()) {
return;
}
shouldNavigateBack.current = true;
if (closing) {
window.history.go(1);
return;
}

// Navigation.navigate() rerenders the current page and resets its states
window.history.go(1);
setIsVisible(true);
shouldNavigateBack.current = true;
navigateAfterInteraction(() => setIsVisible((prev) => !prev));
});

return unsubscribe;
Expand Down Expand Up @@ -78,6 +82,10 @@ function DiscardChangesConfirmation({getHasUnsavedChanges}: DiscardChangesConfir
blockedNavigationAction.current = undefined;
shouldNavigateBack.current = false;
}}
onModalHide={() => {
shouldNavigateBack.current = false;
onCancel?.();
}}
Comment on lines +85 to +88
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from #75014 BugZero checklist:

This PR caused a regression where users cannot discard unsaved changes in the description field on Android mWeb when using the browser back button.

Root Cause: The onModalHide callback added in this PR creates a race condition with the InteractionManager.runAfterInteractions() in the onConfirm handler. The callback sets shouldNavigateBack.current = false immediately when the modal starts hiding, but the navigation logic is queued to run after animations complete. By the time the navigation logic executes, the flag has already been reset, causing an early return before navigation can occur.

The Issue: When users confirm "Discard changes", the modal closes but they remain on the same page with their unsaved text still in the field.

Current Fix: The fix in #76492 addresses this by:

  1. Adding an isConfirmed ref to track whether user confirmed or cancelled
  2. Only resetting shouldNavigateBack.current when user cancels
  3. Using setNavigationActionToMicrotaskQueue() to ensure proper execution timing

/>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
type DiscardChangesConfirmationProps = {
getHasUnsavedChanges: () => boolean;
onCancel?: () => void;
};

export default DiscardChangesConfirmationProps;
9 changes: 7 additions & 2 deletions src/pages/iou/request/step/IOURequestStepDescription.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import lodashIsEmpty from 'lodash/isEmpty';
import React, {useCallback, useMemo, useRef} from 'react';
import {View} from 'react-native';
import {InteractionManager, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
Expand Down Expand Up @@ -64,7 +64,7 @@ function IOURequestStepDescription({
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false});
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput(true);
const {inputCallbackRef, inputRef} = useAutoFocusInput(true);
const isEditing = action === CONST.IOU.ACTION.EDIT;
// In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value
const isEditingSplit = (iouType === CONST.IOU.TYPE.SPLIT || iouType === CONST.IOU.TYPE.SPLIT_EXPENSE) && isEditing;
Expand Down Expand Up @@ -200,6 +200,11 @@ function IOURequestStepDescription({
</View>
</FormProvider>
<DiscardChangesConfirmation
onCancel={() => {
InteractionManager.runAfterInteractions(() => {
inputRef.current?.focus();
});
}}
getHasUnsavedChanges={() => {
if (isSavedRef.current) {
return false;
Expand Down
9 changes: 7 additions & 2 deletions src/pages/iou/request/step/IOURequestStepMerchant.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useCallback, useRef} from 'react';
import {View} from 'react-native';
import {InteractionManager, View} from 'react-native';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
Expand Down Expand Up @@ -41,7 +41,7 @@ function IOURequestStepMerchant({
const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true});
const styles = useThemeStyles();
const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
const {inputCallbackRef, inputRef} = useAutoFocusInput();
const isEditing = action === CONST.IOU.ACTION.EDIT;

// In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value
Expand Down Expand Up @@ -141,6 +141,11 @@ function IOURequestStepMerchant({
</View>
</FormProvider>
<DiscardChangesConfirmation
onCancel={() => {
InteractionManager.runAfterInteractions(() => {
inputRef.current?.focus();
});
}}
getHasUnsavedChanges={() => {
if (isSavedRef.current) {
return false;
Expand Down
Loading