Refactor IOUModal to MoneyRequestModal#16679
Conversation
marcaaron
left a comment
There was a problem hiding this comment.
Looking pretty 👌. Left a few notes on the hooks stuff.
| const isDoneCreatingIOUTransaction = prevCreatingIOUTransactionStatusRef.current && !lodashGet(props, 'iou.creatingIOUTransaction'); | ||
|
|
||
| // User came back online, so we can try to create the IOU transaction again | ||
| if (prevNetworkStatusRef.current && !props.network.isOffline) { |
There was a problem hiding this comment.
NAB / maybe for a follow up. But I would consider refactoring this logic into a custom hook called useNetworkReconnect() that does something like this:
exports the context object from here:
App/src/components/OnyxProvider.js
Lines 9 to 10 in 4efde84
function useNetworkReconnect(callback) {
const {isOffline} = useContext(NetworkContext);
const prevIsOfflineRef = useRef(isOffline);
useEffect(() => {
if (prevIsOfflineRef.current && !isOffline) {
callback();
}
prevIsOfflineRef.current = isOffline;
}, [isOffline]);
}and would be used like this
useNetworkReconnect(PersonalDetails.openMoneyRequestModalPage);There was a problem hiding this comment.
I agree it would be nice to have a seperate hook for this, I'll create another issue that will go in and just refactor all instances of it
| const selectedCurrencyCode = lodashGet(props, 'iou.selectedCurrencyCode'); | ||
| if (prevSelectedCurrencyCodeRef.current !== selectedCurrencyCode) { | ||
| IOU.setIOUSelectedCurrency(selectedCurrencyCode); | ||
| } |
There was a problem hiding this comment.
NAB, but feel like I don't understand it or maybe it's an anti-pattern. Why is the currency changing? Can we just update this whenever the currency changes (e.g. by moving IOU.setIOUSelectedCurrency() closer to the logic that updates the prop instead of applying a side effect)?
There was a problem hiding this comment.
Yeah I also agree this is an anti-pattern, I would imagine we call this where the logic happens of the user changing their currency code
There was a problem hiding this comment.
... I went to the spot where the currency is selected and it already does this so I am going to 🔪 it from here and see what breaks
| </View> | ||
| </View> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Pretty much all of these functions should use useCallback() if they have dependencies.
There was a problem hiding this comment.
And if they don't we can move them to the top of the file. I looked briefly and I think most do have dependencies.
There was a problem hiding this comment.
What's the guideline for when to cache methods? I didn't think it was necessary here because none of these functions perform any heavy computational tasks that need to be cached
|
This is ready to be reviewed, I still need to add screenshots but we can do a code review |
There was a problem hiding this comment.
Each time we render this component the inline functions with dependencies inside it will get re-created. Based on this conversation we have decided to just wrap those with useCallback().
Full context here: https://expensify.slack.com/archives/C01GTK53T8Q/p1680096510744619
| * A modal used for requesting money, splitting bills or sending money. | ||
| */ | ||
| const propTypes = { | ||
| /** Whether the IOU is for a single request or a group bill split */ |
There was a problem hiding this comment.
nab, should we change some of these IOU to Request?
There was a problem hiding this comment.
Updated some of them to Request - didn't update all of them because they are still IOU reports and used like that throughout the code so didn't want it to cause more confusion
| useEffect(() => { | ||
| PersonalDetails.openMoneyRequestModalPage(); | ||
| IOU.setIOUSelectedCurrency(props.currentUserPersonalDetails.localCurrencyCode); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps -- props.currentUserPersonalDetails will always exist from Onyx |
There was a problem hiding this comment.
it's not needed because we get it from onyx
And because we do not want this effect to run again.
I think it's kind of an anti-pattern though (that we can clean up later probably). If we always want the "iou selected currency" to be the "local currency code" when this component mounts then we should update that value in Onyx whenever we update the local currency code. We can hook into the initial render like this - but it's not obvious if we should...
|
|
||
| useEffect(() => { | ||
| // We only want to check if we just finished creating an IOU transaction | ||
| // We check it within this effect because we're sending the request optimistically but if an error occurs from the API, we will update the iou state with the error later |
There was a problem hiding this comment.
Is it kind of sus and/or confusing? If the error occurs from the API then could we just look at props.iou.error as a dependency? If we didn't have an error before, but have one now then we should set the index to 0.
also having trouble understanding the case where we'd dismiss the modal. Anytime the iou changes and we have no errors and we're not in the process of creating the transaction via the API we call dismissModal()? Why is that exactly?
There was a problem hiding this comment.
Yeah I agree it is kind of confusing
If the error occurs from the API then could we just look at props.iou.error as a dependency?
We could, but we'd still need to check the previous props to see if we are done creating iou otherwise this would run each re-render
also having trouble understanding the case where we'd dismiss the modal. Anytime the iou changes and we have no errors and we're not in the process of creating the transaction via the API we call dismissModal()? Why is that exactly?
Don't have a great answer for this, I'm mostly just refactoring the existing logic in place
There was a problem hiding this comment.
My guess actually is that we have this logic in place for the offline handling of creating IOUs. Like if you dismiss the modal right after the transaction was created, then we don't have the ability to re-open and restart the money request. This handling is basically like - user comes back online? okay open the money request modal and then now that they are back online based on the success state of the IOU either restart the modal or dismiss it
There was a problem hiding this comment.
Ok, I think at a minimum we should just add a more specific dependency for creatingIOUTransaction? If I am understanding the logic correctly we only want to do "something" (show error or dismiss) when the transaction API call completes.
There was a problem hiding this comment.
I believe we're already kind of handling this with prevCreatingIOUTransactionStatusRef, like we need to check that creatingIOUTransaction is true but also that the previous creatingIOUTransaction is false to confirm this is the first time that the IOU was created
There was a problem hiding this comment.
More weirdness happening, creating this issue #16942 to investigate it more deeply but not block on this
|
Okay, there's some more weird logic to clean up but I don't want to keep holding this PR for these changes bc many other PRs are on hold for this. I created another issue to investigate more deeply here #16942 but this will at least unblock the other PRs. Right now, this uses mostly the same logic that was already existing |
|
Ready to review / test! |
Reviewer Checklist
Screenshots/VideosWebScreen.Recording.2023-04-04.at.9.20.42.PM.movOffline testScreen.Recording.2023-04-05.at.12.51.06.AM.movMobile Web - ChromeScreen.Recording.2023-04-04.at.10.45.25.PM.movMobile Web - SafariSimulator.Screen.Recording.-.iPhone.14.-.2023-04-04.at.22.24.30.mp4DesktopScreen.Recording.2023-04-04.at.9.35.28.PM.moviOSSimulator.Screen.Recording.-.iPhone.14.-.2023-04-04.at.22.18.06.mp4AndroidScreen.Recording.2023-04-04.at.10.51.47.PM.mov |
marcaaron
left a comment
There was a problem hiding this comment.
LGTM - full disclosure I did not test the changes yet, but will give it a go if needed.
| const participantsWithDetails = _.map(OptionsListUtils.getPersonalDetailsForLogins(reportParticipants, props.personalDetails), personalDetails => ({ | ||
| login: personalDetails.login, | ||
| text: personalDetails.displayName, | ||
| firstName: lodashGet(personalDetails, 'firstName', ''), | ||
| lastName: lodashGet(personalDetails, 'lastName', ''), | ||
| alternateText: Str.isSMSLogin(personalDetails.login) ? Str.removeSMSDomain(personalDetails.login) : personalDetails.login, | ||
| icons: [{ | ||
| source: ReportUtils.getAvatar(personalDetails.avatar, personalDetails.login), | ||
| name: personalDetails.login, | ||
| type: CONST.ICON_TYPE_AVATAR, | ||
| }], | ||
| keyForList: personalDetails.login, | ||
| payPalMeAddress: lodashGet(personalDetails, 'payPalMeAddress', ''), | ||
| phoneNumber: lodashGet(personalDetails, 'phoneNumber', ''), | ||
| })); | ||
|
|
||
| // Skip IOUParticipants step if participants are passed in | ||
| const steps = reportParticipants.length ? [Steps.IOUAmount, Steps.IOUConfirm] : [Steps.IOUAmount, Steps.IOUParticipants, Steps.IOUConfirm]; | ||
|
|
||
| const prevCreatingIOUTransactionStatusRef = useRef(lodashGet(props.iou, 'creatingIOUTransaction')); | ||
| const prevNetworkStatusRef = useRef(props.network.isOffline); | ||
|
|
||
| const [previousStepIndex, setPreviousStepIndex] = useState(0); | ||
| const [currentStepIndex, setCurrentStepIndex] = useState(0); | ||
| const [participants, setParticipants] = useState(participantsWithDetails); |
There was a problem hiding this comment.
Another possible follow up, but we should definitely use a lazy init for useState() here.. we need to pull the logic from line 109 down into the useState() call so we only calculate it once instead of on every render (might not be hurting anything but it's unnecessary work)...
| const participantsWithDetails = _.map(OptionsListUtils.getPersonalDetailsForLogins(reportParticipants, props.personalDetails), personalDetails => ({ | |
| login: personalDetails.login, | |
| text: personalDetails.displayName, | |
| firstName: lodashGet(personalDetails, 'firstName', ''), | |
| lastName: lodashGet(personalDetails, 'lastName', ''), | |
| alternateText: Str.isSMSLogin(personalDetails.login) ? Str.removeSMSDomain(personalDetails.login) : personalDetails.login, | |
| icons: [{ | |
| source: ReportUtils.getAvatar(personalDetails.avatar, personalDetails.login), | |
| name: personalDetails.login, | |
| type: CONST.ICON_TYPE_AVATAR, | |
| }], | |
| keyForList: personalDetails.login, | |
| payPalMeAddress: lodashGet(personalDetails, 'payPalMeAddress', ''), | |
| phoneNumber: lodashGet(personalDetails, 'phoneNumber', ''), | |
| })); | |
| // Skip IOUParticipants step if participants are passed in | |
| const steps = reportParticipants.length ? [Steps.IOUAmount, Steps.IOUConfirm] : [Steps.IOUAmount, Steps.IOUParticipants, Steps.IOUConfirm]; | |
| const prevCreatingIOUTransactionStatusRef = useRef(lodashGet(props.iou, 'creatingIOUTransaction')); | |
| const prevNetworkStatusRef = useRef(props.network.isOffline); | |
| const [previousStepIndex, setPreviousStepIndex] = useState(0); | |
| const [currentStepIndex, setCurrentStepIndex] = useState(0); | |
| const [participants, setParticipants] = useState(participantsWithDetails); | |
| // Skip IOUParticipants step if participants are passed in | |
| const steps = reportParticipants.length ? [Steps.IOUAmount, Steps.IOUConfirm] : [Steps.IOUAmount, Steps.IOUParticipants, Steps.IOUConfirm]; | |
| const prevCreatingIOUTransactionStatusRef = useRef(lodashGet(props.iou, 'creatingIOUTransaction')); | |
| const prevNetworkStatusRef = useRef(props.network.isOffline); | |
| const [previousStepIndex, setPreviousStepIndex] = useState(0); | |
| const [currentStepIndex, setCurrentStepIndex] = useState(0); | |
| const [participants, setParticipants] = useState(() => _.map(OptionsListUtils.getPersonalDetailsForLogins(reportParticipants, props.personalDetails), personalDetails => ({ | |
| login: personalDetails.login, | |
| text: personalDetails.displayName, | |
| firstName: lodashGet(personalDetails, 'firstName', ''), | |
| lastName: lodashGet(personalDetails, 'lastName', ''), | |
| alternateText: Str.isSMSLogin(personalDetails.login) ? Str.removeSMSDomain(personalDetails.login) : personalDetails.login, | |
| icons: [{ | |
| source: ReportUtils.getAvatar(personalDetails.avatar, personalDetails.login), | |
| name: personalDetails.login, | |
| type: CONST.ICON_TYPE_AVATAR, | |
| }], | |
| keyForList: personalDetails.login, | |
| payPalMeAddress: lodashGet(personalDetails, 'payPalMeAddress', ''), | |
| phoneNumber: lodashGet(personalDetails, 'phoneNumber', ''), | |
| }))); |
There was a problem hiding this comment.
Good point, will add that to the list of things to do in #16942
|
Gonna merge this so we can keep the Manual Requests train rolling 🚅 |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚀 Deployed to staging by https://github.com/marcaaron in version: 1.2.95-0 🚀
|
|
🚀 Deployed to production by https://github.com/thienlnam in version: 1.2.95-0 🚀
|
| }; | ||
|
|
||
| const ModalHeader = props => ( | ||
| <View style={[styles.headerBar]}> |
There was a problem hiding this comment.
There was some inconsistency in styling Header with Back button. It was fixed by this PR.
Details
Fixed Issues
$ https://github.com/Expensify/Expensify/issues/270592
$ #16276
Tests
This is a refactor of the IOUModal so lets test the main flows in all sorts of ways
You've selected the maximum number (8) of participants.Verify that the nothing about the IOU functionality has changed
Verify that no errors appear in the JS console
Offline tests
QA Steps
Same steps as Tests
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodWaiting for Copylabel for a copy review on the original GH to get the correct copy.STYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)/** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)Avataris modified, I verified thatAvataris working as expected in all cases)ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Web
Screen.Recording.2023-04-03.at.4.48.28.PM.mov
Mobile Web - Chrome
Screen.Recording.2023-04-04.at.11.29.55.AM.mov
Mobile Web - Safari
Screen.Recording.2023-04-04.at.11.36.47.AM.mov
Desktop
Screen.Recording.2023-04-03.at.6.05.03.PM.mov
iOS
Android