-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor IOUModal to MoneyRequestModal #16679
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking pretty 👌. Left a few notes on the hooks stuff.
src/pages/iou/MoneyRequestModal.js
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
const [withNetwork, NetworkProvider] = createOnyxContext(ONYXKEYS.NETWORK); | |
const [withPersonalDetails, PersonalDetailsProvider] = createOnyxContext(ONYXKEYS.PERSONAL_DETAILS); |
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/pages/iou/MoneyRequestModal.js
Outdated
const selectedCurrencyCode = lodashGet(props, 'iou.selectedCurrencyCode'); | ||
if (prevSelectedCurrencyCodeRef.current !== selectedCurrencyCode) { | ||
IOU.setIOUSelectedCurrency(selectedCurrencyCode); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... 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
src/pages/iou/MoneyRequestModal.js
Outdated
</View> | ||
</View> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty much all of these functions should use useCallback()
if they have dependencies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
src/pages/iou/MoneyRequestModal.js
Outdated
* 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nab, should we change some of these IOU
to Request
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
src/pages/iou/MoneyRequestModal.js
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 Issues
section aboveTests
sectionOffline steps
sectionQA steps
sectiontoggleReport
and notonIconClick
)src/languages/*
files and using the translation methodWaiting for Copy
label for a copy review on the original GH to get the correct copy.STYLE.md
) were followedAvatar
, I verified the components usingAvatar
are working as expected)/** comment above it */
this
properly so there are no scoping issues (i.e. foronClick={this.submit}
the methodthis.submit
should be bound tothis
in the constructor)this
are necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);
ifthis.submit
is never passed to a component event handler likeonClick
)StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG
)Avatar
is modified, I verified thatAvatar
is working as expected in all cases)ScrollView
component to make it scrollable when more elements are added to the page.main
branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTest
steps.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