Skip to content
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

Fix infinite spinner on OldDot transition #5552

Closed
wants to merge 4 commits into from

Conversation

roryabraham
Copy link
Contributor

@roryabraham roryabraham commented Sep 28, 2021

Details

This PR fixes two related race conditions:

LoginWithShortLivedTokenPage race condition explanation

  1. Page loads, beta/session props are not present yet.
  2. Calls Session.signInWithShortLivedToken
  3. Before call to API.createLogin or betas load, LogInWithShortLivedTokenPage immediately runs this.setState({hasRun: true});
  4. In componentDidUpdate, this.state.hasRun is true, and then we are stuck in LogInWithShortLivedTokenPage / infinite spinner forever.

LoginWithShortLivedTokenPage race condition fix

Get rid of hasRun state just navigate to exitTo once betas are loaded and we are logged into the correct account.

AuthScreens race condition explanation

  1. We send an API request to create the policy
  2. We send API requests to fetch existing policies.
  3. First API request returns and policy is added to Onyx.
  4. Second API request(s) return, but at the time when the API was collecting the list of policies to send in the response, the new policy hadn't been created yet. So it's missing from this response and we remove it from Onyx.

AuthScreens race condition fix

Don't load policies until after we've finished with the transition and workspace/new pages. That way, the new workspace won't be overwritten.

Fixed Issues

$ #5518

Tests

  1. Add a setTimeout of 1000 around these lines. This consistently reproduced the AuthScreens race condition before these changes.
  2. Make sure Web-E is up-to-date, run grunt, and then run ngrok
  3. In a mobile browser, visit your ngrok url. This should open OldDot on dev.
  4. Sign in with a new account.
  5. Click Set up my company for free.
  6. Verify that you are taken into NewDot, a new workspace is created, and you are shown the workspace settings page. You should not get stuck on an infinite spinner.

QA Steps

  1. In a mobile browser, visit https://staging.expensify.com/
  2. Sign in with a new account under a domain in the freeCard beta, such as chivito.pw
  3. Click on Set up my company for free
  4. Verify that:
    • You are taken into New Expensify
    • A new workspace is created
    • You are shown the workspace settings page
    • You do not get stuck on an infinite spinner.
  5. Repeat steps 1-4 20 times to verify that the race condition is eradicated (sorry 😓)

Tested On

  • Web
  • Mobile Web
  • Desktop
  • iOS
  • Android

Screenshots

Web

Mobile Web

Screen.Recording.2021-09-27.at.6.15.58.PM.mov

Desktop

iOS

Android

@roryabraham roryabraham requested a review from a team as a code owner September 28, 2021 01:15
@roryabraham roryabraham self-assigned this Sep 28, 2021
@MelvinBot MelvinBot requested review from nickmurray47 and removed request for a team September 28, 2021 01:15
Copy link
Contributor

@marcaaron marcaaron left a comment

Choose a reason for hiding this comment

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

This solution makes sense to me. I am a little worried that this code might be difficult to understand looking back so it might be good to add additional comments in the shouldComponentUpdate() method.

@@ -235,11 +231,15 @@ class AuthScreens extends React.Component {
return true;
}

if (isTransitionOrNewWorkspacePage(this.props.currentURL) && !isTransitionOrNewWorkspacePage(nextProps.currentURL)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add some comments here to explain this check? I see that we want to prevent updating AuthScreens when moving from /transition or /workspace/new to another route, but unsure why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, my reasoning here was since I added the currentURL Onyx key, that will change any time we navigate anywhere:

  1. Opening settings modal
  2. Switching reports
  3. Etc...

And by default we probably don't want to re-render the whole component tree from AuthScreens down every time the route changes. The only time we do want to force a re-render and trigger componentDidUpdate is when we are moving from transition or workspace/new to anywhere else, because that means it's now safe to load policies without risking overwriting a newly-created policy. Overall this design doesn't feel great, but I think it works.

If that all makes sense, I could add a comment like this:

/*
 * We don't want to re-render this component every time the `currentURL` prop changes,
 * but we do want to re-render when we're switching from `transition` or `workspace/new` to any other page.
 * Re-rendering in that case will re-trigger `componentDidUpdate` and `loadPoliciesBehindBeta`,
 * which we only want to do after we're done with the `transition` and `workspace/new` pages.
 * If we don't wait to load the policies until after we're done with those pages,
 * we may accidentally overwrite the newly-created policy and land on an infinite loading spinner. 
 */

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another way we could maybe simplify this code without breaking the E/App philosophy would be to trigger this.loadPoliciesBehindBeta directly here in shouldComponentUpdate, but in general I'm hesitant to put side-effects in shouldComponentUpdate. Not sure I can explain why exactly, it just feels like a bad pattern. 🤷

Copy link
Contributor

@nickmurray47 nickmurray47 Sep 28, 2021

Choose a reason for hiding this comment

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

+1 to leaving the above comment for an explanation but I also think it could use a bit of re-working and additional lines to help with clarity. e.g.

We don't want to re-render this component every time the currentURL prop changes because xyz

src/libs/Navigation/AppNavigator/AuthScreens.js Outdated Show resolved Hide resolved
* We want to only load policy info if:
* - We are on the free plan beta
* - We are not on the transition or new-workspace pages (if we load policies while creating a new one we may accidentally overwrite the new policy due to race condition)
* - We have not already loaded policies
Copy link
Contributor

Choose a reason for hiding this comment

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

So we just keep calling this method anytime the component updates until all the conditions are met?

Seems fine but also kind of inside out to me. This might be off base, but I feel like we are starting to get into some territory where Onyx's async philosophy is hurting us in the code readability department.

Would there be any worry about a race condition or even a need to explain it if we could have something like...

const promises = [getBetas()];
if (shouldCreateWorkspace) {
    promises.push(createPolicy());
}

Promise.all(promises)
    .then(() => {
        getPolicyList();
        getPolicySummaries();
    });

Not sure if we can actually do this or where this logic would run. We can't do it in componentDidMount() because we are probably on /transition and not /workspace/new when that happens. Then again we don't have to switch to a route called /workspace/new and could instead trigger policy creation with a parameter instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I agree, and was suggesting something similar in this thread. In cases when we're just displaying a full-screen loading indicator while waiting for things to happen, our Onyx async philosophy doesn't seem to serve us well/leads to overly-complicated code.

I think that this whole flow could be simplified by just consolidating everything into a single /transition page (i.e: no workspace/new). It would just display a loading indicator, and depending on URL params would do any number of things that need to happen before we can actually display the app. It waits for all those things to be done using promises, and then displays the exitTo page or falls back on the homepage (optionally with an error growl).

Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Moved this conversation here.

@nickmurray47
Copy link
Contributor

nickmurray47 commented Oct 4, 2021

Just double-checking @roryabraham so that I'm not holding anything up, is this PR ready to be reviewed again or are we waiting for #5614 to get merged?

@roryabraham
Copy link
Contributor Author

Sorry for the lackluster communication here @nickmurray47 – discussion is ongoing in the linked issue

@roryabraham
Copy link
Contributor Author

Closing this out in preference of #5706

@roryabraham roryabraham closed this Oct 6, 2021
@roryabraham roryabraham deleted the Rory-TransitionInfiniteSpinner branch July 6, 2022 05:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants