App icon indicating copy to clipboard operation
App copied to clipboard

[$250] Add confirmation dialog on unsaved changes

Open dubielzyk-expensify opened this issue 11 months ago β€’ 15 comments
trafficstars

Problem

When users fill in details in the app (e.g. description on an expense) and tap back or the scrim/overlay by mistake, the user loses everything they filled it. This can be particularly annoying if you submit an expense and fill in multiple details only to misclick and have to start over. We saw this multiple times during our usability study.

Solution

To remedy this, let's add a confirmation dialog on these sort of interactions by throwing up a confirmation dialog for unsaved changes. Here's a video to demonstrate:

https://github.com/user-attachments/assets/36948029-5b52-43fb-aa14-bea2a3011f70

Notes:

  • If the value isn't changed, we should show the warning.
  • Clicking Discard continues the action of the user (if back, go back. If scrim/overlay, closer the right-hand-pane) without saving the value.
  • Any movement away from the screen should trigger the confirmation dialog (given the value has changed)

Questions:

  • Can this be applied systematically or do we have to go through all flows? If not, we should probably start with Submit expense to try it out and continue from there.

cc @Expensify/design @JmillsExpensify @trjExpensify

Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~021864985847950506437
  • Upwork Job ID: 1864985847950506437
  • Last Price Increase: 2024-12-06
Issue OwnerCurrent Issue Owner: @ahmedGaber93

dubielzyk-expensify avatar Dec 06 '24 05:12 dubielzyk-expensify

Edited by proposal-police: This proposal was edited at 2024-12-06 05:23:17 UTC.

Proposal

Please re-state the problem that we are trying to solve in this issue.

Add confirmation dialog on unsaved changes

What is the root cause of that problem?

New feature

What changes do you think we should make in order to solve the problem?

  1. Create a new component DiscardChangeModal based on ConfirmModal
  2. Create a new state like isShowDiscardChangeModal
  3. Will set isShowDiscardChangeModal as true when moving away from the screen without saving the value.

What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future?

NA

What alternative solutions did you explore? (Optional)

Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.

nkdengineer avatar Dec 06 '24 05:12 nkdengineer

Triggered auto assignment to @abekkala (NewFeature), see https://stackoverflowteams.com/c/expensify/questions/14418#:~:text=BugZero%20process%20steps%20for%20feature%20requests for more details. Please add this Feature request to a GH project, as outlined in the SO.

melvin-bot[bot] avatar Dec 06 '24 10:12 melvin-bot[bot]

:warning: It looks like this issue is labelled as a New Feature but not tied to any GitHub Project. Keep in mind that all new features should be tied to GitHub Projects in order to properly track external CAP software time :warning:

melvin-bot[bot] avatar Dec 06 '24 10:12 melvin-bot[bot]

@dubielzyk-expensify make sure you add Bug or New feature labels to issues like this, to get it assigned out to a BZ member to manage and progress.

trjExpensify avatar Dec 06 '24 10:12 trjExpensify

Triggered auto assignment to Design team member for new feature review - @dubielzyk-expensify (NewFeature)

melvin-bot[bot] avatar Dec 06 '24 10:12 melvin-bot[bot]

Job added to Upwork: https://www.upwork.com/jobs/~021864985847950506437

melvin-bot[bot] avatar Dec 06 '24 10:12 melvin-bot[bot]

Triggered auto assignment to Contributor-plus team member for initial proposal review - @ahmedGaber93 (External)

melvin-bot[bot] avatar Dec 06 '24 10:12 melvin-bot[bot]

Proposal

Please re-state the problem that we are trying to solve in this issue.

Add confirmation dialog on unsaved changes

What is the root cause of that problem?

New Feature

What changes do you think we should make in order to solve the problem?

If we start from IOURequestStepDescription page, https://github.com/Expensify/App/blob/a9bf599522215bc8c844d587374fd4f04d6b860b/src/pages/iou/request/step/IOURequestStepDescription.tsx#L57

  1. we can use the already existing ConfirmModal
  2. modify the function used for onBackButtonPress

https://github.com/Expensify/App/blob/a9bf599522215bc8c844d587374fd4f04d6b860b/src/pages/iou/request/step/IOURequestStepDescription.tsx#L139

to use a modified function as:

    const handleNavigation = () => {
        if (newDescription !== currentDescription) {
            setIsConfirmationModalVisible(true);
        } else {
            Navigation.goBack(backTo);
        }
    };
  1. add this:
  const [isConfirmationModalVisible, setIsConfirmationModalVisible] = useState(false);

       <ConfirmModal
                  title={'Discard Changes?'}
                  prompt='Are you sure you want to discard the changes you made?'
                  isVisible={isConfirmationModalVisible}
                  onConfirm={()=>{
                      setIsConfirmationModalVisible(false)
                      Navigation.goBack(backTo);

                  }}
                  onCancel={()=>{
                      setIsConfirmationModalVisible(false)
                  }}
                  confirmText={'Discard Changed'}
                  cancelText={'Cancel'}
                  danger
              />

we should create translation for the above used texts. We can add the same ConfirmModal in all the pages needed.

https://github.com/user-attachments/assets/e350c6f6-93b1-4052-a0e0-9069de091b63

Shahidullah-Muffakir avatar Dec 06 '24 11:12 Shahidullah-Muffakir

Edited by proposal-police: This proposal was edited at 2024-12-10 07:13:19 UTC.

Proposal

Please re-state the problem that we are trying to solve in this issue.

We want to show a discard changes confirmation when closing the page if there is an unsaved changes.

What is the root cause of that problem?

New feature.

What changes do you think we should make in order to solve the problem?

Since we want to apply this to many pages, we can create a new component called DiscardChangesConfirmation. This component will make use of beforeRemove event and prevent default if there is an unsaved changes. If the user press cancel, then we simply close the confirm modal, else, we continue with the navigation action that is previously prevented.

const DiscardChangesConfirmation = ({getHasUnsavedChanges}) => {
    const navigation = useNavigation();
    const [isVisible, setIsVisible] = useState(false);
    const preventedNavigationAction = useRef();

    const handlePreventRemove = useCallback((e) => {
        if (!getHasUnsavedChanges()) {
            return;
        }

        e.preventDefault();
        blockedNavigationAction.current = e.data.action;
        setIsVisible(true);
    }, [getHasUnsavedChanges]);

    useEffect(() => {
        navigation.addListener('beforeRemove', handlePreventRemove);
        return () => navigation.removeListener('beforeRemove', handlePreventRemove);
    }, [handlePreventRemove]);

    return (
        <ConfirmModal
            isVisible={isVisible}
            title='Discard changes?'
            prompt='Are you sure?'
            danger
            confirmText='Discard changes'
            cancelText='Cancel'
            onConfirm={() => {
                setIsVisible(false);
                navigation.dispatch(preventedNavigationAction.current)
            }}
            onCancel={() => setIsVisible(false)}
        />
    )
}

Then, to use this in the description page for example, we need to update the input first to be a controlled input. https://github.com/Expensify/App/blob/7d42188f6070d11c3964397b60246ba9d300d8d7/src/pages/iou/request/step/IOURequestStepDescription.tsx#L153-L158

const [description, setDescription] = useState(currentDescription);

<InputWrapper
    valueType="string"
    value={description}
    onValueChange={setDescription}

Then, add a isSavedRef to indicate that the user saves the value so we don't show discard confirm modal when the user saves the changes.

const isSavedRef = useRef(false);

And set it to true here. https://github.com/Expensify/App/blob/8981081bed8fb8a07f8797d49af2797332c9259d/src/pages/iou/request/step/IOURequestStepDescription.tsx#L101-L102

And use the new component like this:

<DiscardChangesConfirmation
    getHasUnsavedChanges={() => {
        if (isSavedRef.current) {
            return false;
        }
        return description !== currentDescription
    }}
/>

What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future?

N/A

bernhardoj avatar Dec 06 '24 14:12 bernhardoj

πŸ“£ @bjmmtin! πŸ“£ Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork. Please follow these steps:

  1. Make sure you've read and understood the contributing guidelines.
  2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
  3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
  4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details. Screen Shot 2022-11-16 at 4 42 54 PM Format:
Contributor details
Your Expensify account email: <REPLACE EMAIL HERE>
Upwork Profile Link: <REPLACE LINK HERE>

melvin-bot[bot] avatar Dec 06 '24 15:12 melvin-bot[bot]

react-hook-form simplifies form management by providing easy-to-use hooks for registering inputs, handling submissions, and tracking form state. useBlocker effectively prevents users from accidentally losing unsaved changes by prompting them with a modal alert before navigating away from the form.

import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useBlocker } from 'react-router-dom';

const MyForm = () => {
  const { register, handleSubmit, formState: { isDirty } } = useForm({
    defaultValues: { name: "", email: "" }
  });
  
  const [alertModal, setAlertModal] = useState({ show: false, title: "", content: "" });
  const [proceed, setProceed] = useState(false);
  const [navigateTo, setNavigateTo] = useState(null);

  const onSubmit = (data) => {
    console.log(data);
    // Reset the form or perform any other action after submission
  };

  const handleNavigation = ({ nextLocation }) => {
    if (isDirty) {
      setProceed(true);
      setNavigateTo(nextLocation.pathname);
      setAlertModal({
        show: true,
        title: "You have unsaved changes.",
        content: "All unsaved changes will be lost.",
      });
      return false; // Prevent navigation
    }
    return true; // Allow navigation
  };

  useBlocker(handleNavigation);

  const handleAlertConfirm = () => {
    setProceed(false);
    setAlertModal({ show: false, title: "", content: "" });
    // Navigate to the next location if proceed is true
    if (navigateTo) {
      // Use your navigation logic here, e.g., using useNavigate from react-router-dom
      // navigate(navigateTo);
    }
  };

  return (
    <div>
      {alertModal.show && (
        <div className="alert-modal">
          <h2>{alertModal.title}</h2>
          <p>{alertModal.content}</p>
          <button onClick={handleAlertConfirm}>Proceed</button>
          <button onClick={() => setAlertModal({ show: false })}>Cancel</button>
        </div>
      )}
      <form onSubmit={handleSubmit(onSubmit)}>
        <input {...register('name')} placeholder="Name" />
        <input {...register('email')} placeholder="Email" />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

export default MyForm;

cyberloop001 avatar Dec 06 '24 16:12 cyberloop001

Reviewing today

ahmedGaber93 avatar Dec 09 '24 06:12 ahmedGaber93

Proposal

Please re-state the problem that we are trying to solve in this issue.

Add confirmation dialog on unsaved changes

What is the root cause of that problem?

New feature

What changes do you think we should make in order to solve the problem?

  • Test branch: https://github.com/mkzie2/App/tree/mkzie2-issue/53679
  1. Create a new DiscardChangesModal that uses ConfirmModal. To detect when the screen is removed, we can use the beforeRemove event. If we click on the confirm button, continue the navigate action. If we click on the cancel button, close this modal.

  2. To apply this pattern to all pages, we can add this in FormWrapper then all pages that use a form always has this behavior.

  • To do that firstly we need to detect whenever the value is changed. Define the initInputValuesRef to store the form's initial value. That will be the first value that is not an empty object of inputValues. Introduce a new prop isValueChanged in FormWrapper here and pass it here isValueChanged={!deepEqual(inputValues, initInputValuesRef.current)}.
const initInputValuesRef = useRef<Form>();

useEffect(() => {
    if (!!initInputValuesRef.current || isEmptyObject(inputValues)) {
        return;
    }

    initInputValuesRef.current = inputValues;
}, [inputValues]);

  • Then we can add DiscardChangesModal here with hasUnsavedChanges={!isValueChanged}
<DiscardChangesModal hasUnsavedChanges={!isValueChanged} />

https://github.com/Expensify/App/blob/37105a1b4c60ba6416b3ae3415c2265a53c55257/src/components/Form/FormWrapper.tsx#L125

Optional: If we don't want to apply for all forms of input, we can introduce a new prop in FormProvider shouldUseDiscardChangesModal and will only render DiscardChangesModal if shouldUseDiscardChangesModal is true. Then we will pass shouldUseDiscardChangesModal as true to FormProvider in all places that we want to use this feature.

What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future?

What alternative solutions did you explore? (Optional)

Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.

mkzie2 avatar Dec 09 '24 10:12 mkzie2

@bernhardoj I love your idea using beforeRemove, And I see you used it before here.

But I have some question:

  1. How we will prevent the beforeRemove that will trigger after click save?
  2. What is about the limitation with @react-navigation/native-stack (back gesture doesn't trigger beforeRemove event with native-stack)? This limitation has been fixed in version 7.x, but we use version 6.9

ahmedGaber93 avatar Dec 09 '24 22:12 ahmedGaber93

now we are using the high version react-router-dom, It's impossible to use the beforeRemove, it can't prevent the navigation and won't trigger after click save. we can implement it only by using useBlocker.

cyberloop001 avatar Dec 10 '24 01:12 cyberloop001

How we will prevent the beforeRemove that will trigger after click save?

Updated to handle this case

What is about the limitation with @react-navigation/native-stack (back gesture doesn't trigger beforeRemove event with native-stack)? This limitation has been fixed in version 7.x, but we use version 6.9

I think the only option we have is to disable the swipe gesture or update the navigation to 7.0 and use usePreventRemove hook.

bernhardoj avatar Dec 10 '24 07:12 bernhardoj

@ahmedGaber93 What do you think about my proposal? This can apply to all places where we use a FormProvider.

mkzie2 avatar Dec 10 '24 07:12 mkzie2

@bernhardoj, can we know how many places/forms are use @react-navigation/native-stack and will be affected with this limitation, I believe RHP will not be affected

ahmedGaber93 avatar Dec 10 '24 08:12 ahmedGaber93

What do you think about my proposal?

@mkzie2 I think it has the same idea from bernhardoj proposal https://github.com/Expensify/App/issues/53679#issuecomment-2523352545

ahmedGaber93 avatar Dec 10 '24 09:12 ahmedGaber93

can we know how many places/forms are use @react-navigation/native-stack and will be affected with this limitation, I believe RHP will not be affected

I think most of the screens are using native stack already (except web). RHP uses createPlatformStackNavigatorComponent which uses the native stack. From my testing, the discard modal won't show if we use the swipe gesture to close the screen on iOS.

bernhardoj avatar Dec 10 '24 11:12 bernhardoj

@bernhardoj's proposal using beforeRemove listener can do the job with some limitations, I think the important limitation is it will not support back gesture in iOS util we upgrade @react-navigation/native-stack to v7.x to use usePreventRemove (now we use v6.9).

I think we can move forward @bernhardoj's proposal, and open a tracker issue to support back gesture in iOS when @react-navigation/native-stack get upgrade.

πŸŽ€ πŸ‘€ πŸŽ€ C+ reviewed

ahmedGaber93 avatar Dec 11 '24 17:12 ahmedGaber93

Triggered auto assignment to @stitesExpensify, see https://stackoverflow.com/c/expensify/questions/7972 for more details.

melvin-bot[bot] avatar Dec 11 '24 17:12 melvin-bot[bot]

πŸ“£ @ahmedGaber93 πŸŽ‰ An offer has been automatically sent to your Upwork account for the Reviewer role πŸŽ‰ Thanks for contributing to the Expensify app!

Offer link Upwork job

melvin-bot[bot] avatar Dec 12 '24 15:12 melvin-bot[bot]

PR is ready

cc: @ahmedGaber93

bernhardoj avatar Dec 16 '24 04:12 bernhardoj

This issue has not been updated in over 15 days. @abekkala, @ahmedGaber93, @stitesExpensify, @bernhardoj, @dubielzyk-expensify eroding to Monthly issue.

P.S. Is everyone reading this sure this is really a near-term priority? Be brave: if you disagree, go ahead and close it out. If someone disagrees, they'll reopen it, and if they don't: one less thing to do!

melvin-bot[bot] avatar Jan 08 '25 09:01 melvin-bot[bot]

Deployed to staging

ahmedGaber93 avatar Jan 08 '25 10:01 ahmedGaber93

Reviewing label has been removed, please complete the "BugZero Checklist".

melvin-bot[bot] avatar Jan 10 '25 03:01 melvin-bot[bot]

The solution for this issue has been :rocket: deployed to production :rocket: in version 9.0.82-12 and is now subject to a 7-day regression period :calendar:. Here is the list of pull requests that resolve this issue:

  • https://github.com/Expensify/App/pull/54176

If no regressions arise, payment will be issued on 2025-01-17. :confetti_ball:

For reference, here are some details about the assignees on this issue:

  • @ahmedGaber93 requires payment automatic offer (Reviewer)
  • @bernhardoj requires payment through NewDot Manual Requests

melvin-bot[bot] avatar Jan 10 '25 03:01 melvin-bot[bot]

BugZero Checklist: The PR adding this new feature has been merged! The following checklist (instructions) will need to be completed before the issue can be closed:

  • [ ] [@ahmedGaber93] Please propose regression test steps to ensure the new feature will work correctly on production in further releases.
  • [ ] [@abekkala] Link the GH issue for creating/updating the regression test once above steps have been agreed upon.

melvin-bot[bot] avatar Jan 10 '25 03:01 melvin-bot[bot]

BugZero Checklist: The PR adding this new feature has been merged! The following checklist (instructions) will need to be completed before the issue can be closed:

  • [x] [@ahmedGaber93] Please propose regression test steps to ensure the new feature will work correctly on production in further releases.

Regression Test Proposal

  1. Open submit expense flow page
  2. Enter amount and proceed to the confirmation page
  3. Open the Description form and keep the input empty
  4. Go back
  5. Verify the page closes
  6. Open the page again
  7. Type anything
  8. Go back
  9. Verify there is a discard confirmation modal
  10. Press Cancel
  11. Verify the modal closes, but the page doesn't close
  12. Repeat step 8-9
  13. Press Discard changes
  14. Verify the modal and page closes
  15. Repeat step 6-7
  16. Save the changes
  17. Verify the page closes and the change is saved
  18. Repeat the same test (step 3-17) for Merchant field and Verify it has the same behavior in steps 5, 9, 11, 14, 17.

ahmedGaber93 avatar Jan 15 '25 08:01 ahmedGaber93