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

Wait for Onyx to clear before signing in the transitioning user #8793

Closed
wants to merge 8 commits into from

Conversation

neil-marcellini
Copy link
Contributor

@neil-marcellini neil-marcellini commented Apr 26, 2022

Details

As an alternative to modifying how Onyx updates subscribers when initializing the default key states, wait for Onyx to finish clearing and initializing before signing in the transitioning user.

Related Issue

$ #8676

Tests

Below are tests for all of the transition flows from Web-Expensify to NewDot. This PR specifically fixes flow C, but we should test all of them.

  • A. Set up my company for free
  1. Make sure you are signed out of NewDot
  2. Go to OldDot with a mobile screen size and sign up with a new account
  3. Click join
  4. Click "Set up my company for free".
  5. You should be directed to NewDot and have a new workspace created.
  6. Repeat steps 2-5 to test the case where you are signed in to a different account on NewDot
  • B. Select a free workspace
  1. Make sure you are signed out of NewDot
  2. Go to OldDot and sign in to an account if needed
  3. Go to Settings > Policies > Group and click New Policy in the top right
  4. Click select on the free plan
  5. You should be directed to NewDot and have a new workspace created.
  6. Repeat steps 2-5 to test the case where you are signed in to the same account on NewDot
  7. Sign in to a different account on OldDot
  8. Repeat steps 3-5 to test the case where you are signed in to a different account on NewDot
  • C. Clicking on the name of a free policy
  1. Make sure you are signed out of NewDot
  2. Go to OldDot and sign in (if needed) to an account that has a free workspace / group policy
  3. Go to Settings > Policies > Group and click on the name of a free workspace
  4. You should be directed to NewDot and the workspace settings should open
  5. Repeat steps 2-4 to test the case where you are signed in to the same account on NewDot
  6. Sign in to a different account on OldDot that has a free workspace / group policy
  7. Repeat steps 3-4 to test the case where you are signed in to a different account on NewDot
  • D. Get Started Inbox Task
  1. Make sure you are signed out of NewDot
  2. Go to OldDot and sign up for a new account with an @gmail.com address
  3. Click on the "Get started" on the inbox task that says "Would you like to get started with our free plan?".
  4. You should be directed to NewDot and have a new workspace created, and the workspace settings should open.
  5. Go back to OldDot and go to Settings > Policies > Group
  6. Refresh the page and verify that 1 free workspace policy has been created
  7. Repeat steps 2-6 to test the case where you are signed in to a different account on NewDot
  • E. Pricing select free

  • E.1 Creating a free policy

  1. Make sure you are signed out of NewDot
  2. Go to OldDot and sign in with any account
  3. Go to Settings > Policies > Group, refresh the page, then delete any free group policies
  4. Go to expensify.com.dev/pricing and click "Select" on the free plan
  5. You should be directed to NewDot and have a new workspace created, and the workspace settings should open.
  6. Go back to OldDot and go to Settings > Policies > Group
  7. Refresh the page and verify that 1 free workspace policy has been created
  8. Repeat steps 3-7 to test the case where you are signed in to the same account
  9. Go to OldDot and sign in with a different account
  10. Repeat steps 3-7 to test the case where you are signed in to a different account.
  • E.2 Navigating to an existing free policy
  1. Make sure you are signed out of NewDot
  2. Go to OldDot and sign in with any account with free group policies
  3. Go to expensify.com.dev/pricing and click "Select" on the free plan
  4. You should be directed to NewDot and the workspace settings should open for some workspace.
  5. Repeat steps 3-4 to test the case where you are signed in to the same account
  6. Go to OldDot and sign in with a different account with free group policies
  7. Repeat steps 3-4 to test the case where you are signed in to a different account.
  • F. Continue setup Inbox task
  1. Make sure you are signed out of NewDot
  2. Go to OldDot and sign in with any account with free group policies and without a bank account set up
  3. In the OldDot inbox click "Continue setup" on the inbox task that says "Finish setting up your bank account".
  4. The Connect bank account page should open in the right sidebar
  5. Repeat steps 3-4 to test the case where you are signed in to the same account
  6. Go to OldDot and sign in with a different account with free group policies and without a bank account set up
  7. Repeat steps 3-4 to test the case where you are signed in to a different account.
  • Verify that no errors appear in the JS console

PR Review Checklist

Contributor (PR Author) Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • iOS / native
    • Android / native
    • iOS / Safari
    • Android / Chrome
    • MacOS / Chrome
    • MacOS / Desktop
  • I verified there are no console errors (if there’s a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained “why” the code was doing something instead of only explaining “what” the code was doing.
    • I verified any copy / text shown in the product was added in all src/languages/* files
    • I verified any copy / text that was added to the app is correct English and approved by marketing by tagging the marketing team on the original GH to get the correct copy.
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named “index.js”. All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.js or at the top of the file that uses the constant) are defined as such
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • Any functional components have the displayName property
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose and it is
  • If a new CSS style is added I verified that:
    • A similar style doesn’t already exist
    • The style can’t be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.

PR Reviewer Checklist

  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I verified tests pass on all platforms & I tested again on:
    • iOS / native
    • Android / native
    • iOS / Safari
    • Android / Chrome
    • MacOS / Chrome
    • MacOS / Desktop
  • I verified there are no console errors (if there’s a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained “why” the code was doing something instead of only explaining “what” the code was doing.
    • I verified any copy / text shown in the product was added in all src/languages/* files
    • I verified any copy / text that was added to the app is correct English and approved by marketing by tagging the marketing team on the original GH to get the correct copy.
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named “index.js”. All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.js or at the top of the file that uses the constant) are defined as such
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • Any functional components have the displayName property
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose and it is broken down into smaller components in order to separate concerns and functions
  • If a new CSS style is added I verified that:
    • A similar style doesn’t already exist
    • The style can’t be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.

QA Steps

  • Verify that no errors appear in the JS console

Screenshots

Web

Mobile Web

Desktop

iOS

Android

@neil-marcellini neil-marcellini requested review from marcaaron and a team as code owners April 26, 2022 23:29
@neil-marcellini neil-marcellini self-assigned this Apr 26, 2022
@melvin-bot melvin-bot bot requested review from sketchydroide and removed request for a team April 26, 2022 23:29
@neil-marcellini
Copy link
Contributor Author

I just pushed one commit that I accidentally left out due to a merge conflict that I didn't notice. Ready for review.

@marcaaron
Copy link
Contributor

Have a few thoughts on this one, but they are better to share 1:1

@neil-marcellini
Copy link
Contributor Author

Ready for review again. I have this other PR up to address the betas. Don't wait for betas while transitioning

@neil-marcellini
Copy link
Contributor Author

Fixed merge conflicts after "Don't wait for betas" was merged.

@neil-marcellini
Copy link
Contributor Author

I just tested all of the flows again and they are working.

@@ -36,6 +37,10 @@ export default function () {
},
});

// We need to set IS_ONYX_DONE_CLEARING outside of the initial key states, because
// initial key states are set within Onyx.clear.
App.setOnyxDoneClearing();
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we try explaining what is happening a different way? I've read that comment a few times, but not sure I get why we need to set the IS_ONYX_DONE_CLEARING key in this setup method instead of when Onyx.clear() is called.

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 will try an approach where the initial key state is [ONYXKEYS.IS_ONYX_DONE_CLEARING]: true. I don't remember why I didn't do that to begin with, maybe I will find out.

@@ -35,6 +35,7 @@ function clearStorageAndRedirect(errorMessage) {

// `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set`.
Onyx.merge(ONYXKEYS.SESSION, {error: errorMessage});
Onyx.set(ONYXKEYS.IS_ONYX_DONE_CLEARING, true);
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Use App.setOnyxDoneClearing()

}
if (this.signInIfNeeded()) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

thought: having this logic in componentDidUpdate() makes it hard to connect the dots on which scenarios we are dealing with.

e.g. this.props.isOnyxDoneClearing is covering a very specific situation which is:

previous user was signed in so we signed them out and we are now waiting for previous user to fully sign out so we can sign in the new user

I'm curious if we can rework this so that it's clear which situation we are handling each time this component updates or mounts. Or at least document which checks are relevant for the different flows?

if (!this.props.isOnyxDoneClearing) {
return;
}
if (this.state.isSigningIn) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible that we only have this because we are setting state which triggers the component to update?

return false;
}
Log.info('[LoginWithShortLivedTokenPage] User not signed in - signing in with short lived token');
this.setState({isSigningIn: true});
Copy link
Contributor

Choose a reason for hiding this comment

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

question: Why do we need to set this to state?

I guess my thinking here is that if we are calling signInIfNeeded then there are two scenarios...

  1. User was not signed in and needs to sign in
  2. User was signed in then signed out and now needs to sign in

Regardless, doesn't this component entirely mount again in both of these cases?

User gets signed in > Component mounts again in AuthScreens > this.state.isSigningIn would be false again because the component remounted?

Comment on lines +69 to +70
// We want to display the Workspace chat first since that means a user is already in a Workspace and doesn't need to create another one
// Only navigate to the workspace chat if we are not displaying a workspace route
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm slightly confused by some of this logic so gonna rubber duck for a second...

  • When we are logging in for the first time we check to see if either the exitTo or url is sending us to a "workspace" page
  • We also check the list of reports to see if we already have a workspace.
  • If we are intentionally navigating to a specific workspace then we do nothing otherwise we navigate to the first workspace we find.

Does that all sound correct?

Copy link
Contributor

Choose a reason for hiding this comment

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

If so, I guess my next question would be...

Is this related to waiting for Onyx to clear? Or is it related to a different issue where we were incorrectly redirecting a user based on whether they have any workspace instead of checking if we should let the routing do it's thing?

I guess overall I'm just confused at the interplay between the LoginWithShortLivedTokenPage seemingly responsible for some navigation stuff and then the SidebarScreen is also responsible for navigation here:

componentDidMount() {
Performance.markStart(CONST.TIMING.SIDEBAR_LOADED);
Timing.start(CONST.TIMING.SIDEBAR_LOADED, true);
const routes = lodashGet(this.props.navigation.getState(), 'routes', []);
WelcomeAction.show({routes, hideCreateMenu: this.hideCreateMenu});
}

Kinda feels a bit all over the place. Gonna cc @luacmartins here too to get some thoughts.

Copy link
Contributor

Choose a reason for hiding this comment

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

What exactly am I looking at?

Kinda feels a bit all over the place. Gonna cc @luacmartins here too to get some thoughts.

I agree that we should keep all this navigation in one place if possible.

// We need to wait for Onyx to finish clearing before signing in because
// it will re-initialize with the initialKeyStates, which can notify
// subscribers with an out of date value causing the user to be signed
// out again. See https://github.com/Expensify/react-native-onyx/issues/125
Copy link
Contributor

Choose a reason for hiding this comment

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

thought: This comment should be removed as it provides a lot of specific context that doesn't quite make sense unless you have a lot of context on your specific view of what's happening here and the issue.

Separately, we are linking to a bug report which implies we should be fixing an issue in Onyx. Left a comment on that issue and think we should give it more thought.

suggestion: If we want to say anything here about why we are waiting for Onyx to finish clearing we could say something like:

If we are signing an old user out then we'll want to wait until they are fully signed out and Onyx has reinitialized as a blank slate before signing in the next user.

@neil-marcellini
Copy link
Contributor Author

I admit that this implementation is confusing and doesn't read well. I will go through and address each of your comments as best I can, but before doing so, should I implement another approach?

Should I create public and authenticated versions of this component as you mentioned yesterday? Or should I extract all of this logic into an action? I would prefer extracting it into an action. I can try to rework this to make it more readable, but I think the time would be better spent on a different implementation.

@marcaaron
Copy link
Contributor

Should I create public and authenticated versions of this component as you mentioned yesterday? Or should I extract all of this logic into an action? I would prefer extracting it into an action. I can try to rework this to make it more readable, but I think the time would be better spent on a different implementation.

It's hard for me to say if extracting things into an action would be preferable, because I'm not sure what the proposal is exactly and can't envision it.

I do think it would help to think about the different scenarios a bit and maybe it will help lead us to a better solution.

I suggested having two separate pages to handle this stuff because it feels like we are mixing concerns. If you imagine LogInWithShortLivedTokenPage as a single function it has a lot of responsibility and the things it does don't really line up in a way that is easy to follow.

Possible navigation scenarios after we are signed in

A. Navigate to an exitTo
B. Don't explicitly navigate + Create a policy in AuthScreens (and navigate to the policy settings)

If we are in PublicScreens we only need to do the automatic sign in part because these are the two scenarios...

  • Not signed in -> Sign In -> remount in AuthScreens -> A
  • Not signed in -> Sign In -> remount in AuthScreens -> B

If we are in AuthScreens we need to do one of these:

  • Automatic sign out if different user is signed in -> See PublicScreens
  • Already signed in -> A
  • Already signed in -> B

And when we are in AuthScreens it seems we have another navigation concern in the SidebarScreen which is that we want to navigate us to the workspace chat, but only if we are not already doing A or B.

I think that covers everything, but lemme know if I missed something.

@neil-marcellini
Copy link
Contributor Author

That's a great breakdown of the different scenarios that happen. If possible it would be great to eliminate B by having a separate component for workspace/new that would handle this code block from the authScreens.

if (this.shouldCreateFreePolicy(url)) {
Policy.createAndGetPolicyList();
return;
}

I'm not really sure what the solution is for the WelcomeAction logic. It was a small tweak that I had to do and it has nothing to do with waiting for Onyx to clear.

I will post a proposal for implementing this as an action. I think it will be more straight forward, especially in regards to waiting for Onyx to clear. If you think it shows promise we can go ahead with that, otherwise I can split it into two different pages.

@marcaaron
Copy link
Contributor

If possible it would be great to eliminate B by having a separate component for workspace/new that would handle this code block from the authScreens.

Oh hmm that's interesting. I was actually thinking that we could have an if/else in AuthScreens to handle the exitTo and based on what kind of exitTo we have we can redirect, create policy and redirect, etc.

// pseudocode
if (exitTo === ROUTES.WORKSPACE_NEW) {
    createPolicyAndNavigate();
} else {
    Navigation.navigate(exitTo)
}

That way the only thing LoginWithShortLivedToken needs to do is log the user in or out?

@neil-marcellini
Copy link
Contributor Author

Proposal

Simplify LoginWithShortLivedTokenPage so that it only renders a loading spinner and calls one action on componentDidMount():

componentDidMount() {
    const accountID = lodashGet(this.props.route.params, 'accountID', '');
    const email = lodashGet(this.props.route.params, 'email', '');
    const shortLivedToken = lodashGet(this.props.route.params, 'shortLivedToken', '');
    const exitTo = decodeURIComponent(lodashGet(this.props.route.params, 'exitTo', ''));
    Session.transitionUser(accountID, email, shortLivedToken, exitTo);
}

At the top of the Session file connect to the Onyx session and store flag to mark when the user is transitioning.

let session = {};
Onyx.connect({
    key: ONYXKEYS.SESSION,
    callback: val => session = val,
});

let isUserTransitioning = false;

Create an action that will wait until the proper user is signed in and then navigate them to the exit route

function transitionUser(accountID, email, shortLivedToken, exitTo) {
    if (isUserTransitioning) {
        return;
    }

    return new Promise(resolve => {
        const isUserSignedIn = session && session.authToken;
        if (isUserSignedIn) {
            resolve();
        } else if (session.email !== email) {
            Log.info('[transitionUser] Different user signed in - signing out');
            resolve(signInDifferentUser(email, shortLivedToken))

        } else {
            Log.info('[transitionUser] User not signed in - signing in with short lived token');
            resolve(signInWithShortLivedToken(accountID, email, shortLivedToken))
        }
    })
    .then(() => {
        isUserTransitioning = false
        Log.info('[transitionUser] Dismissing LoginWithShortLivedTokenPage and navigating to exitTo');
        Navigation.dismissModal();
        Navigation.navigate(exitTo);
    });

}

signInDifferentUser would sign the old user out, wait for onyx to clear, sign the new user in, and then resolve. I would also modify signInWithShortLivedToken to return a promise. The isUserTransitioning flag ensures that we only run this action once. Or perhaps we could find a better place to trigger it.

Does this look like a mess or is it somewhat promising?

@neil-marcellini
Copy link
Contributor Author

neil-marcellini commented Apr 28, 2022

That way the only thing LoginWithShortLivedToken needs to do is log the user in or out?

The auth screens can mount when we are logged in as the wrong user, so wouldn't we have to handle basically the same cases?

@marcaaron
Copy link
Contributor

marcaaron commented Apr 28, 2022

The auth screens can mount when we are logged in as the wrong user, so wouldn't we have to handle basically the same cases?

Not sure I understand the question, mind rephrasing?

The proposal doesn't look too bad, but there is still a lot of responsibility for one method and I think we are really just dealing with 3 different states (sign in, sign out if needed, perform side effect based on route when logged in). But I'm probably missing something.

I am assuming here that the responsibilities would go something like this:

  • LoginWithShortLivedTokenPage (AuthScreens) would recognize that you are logged in as the wrong user and then log you out.
  • You'd get kicked to LoginWithShortLivedTokenPage (PublicScreens) which would see that you need to be logged in because nobody is logged in.
  • You'd land on LoginWithShortLivedTokenPage (AuthScreens) and it would do nothing because you are already logged in.
  • Logic in AuthScreens would look at the route and determine which action to take

The /transition route needs to either SignOutUserIfNeeded() which would possibly log out a user that doesn't match the route params and if we are not logged in at all then we always need to SignInUser().

Anything that happens after that would depend on the exitTo param which would just go into AuthScreens somewhere.

I think the only problem with this is that AuthScreens doesn't know if we are an old user signing out, but it's easy enough to check that based on the route params.

@neil-marcellini
Copy link
Contributor Author

The proposal doesn't look too bad, but there is still a lot of responsibility for one method and I think we are really just dealing with 3 different states

Yeah I think you are right that it is doing too much for one thing. As I was writing that proposal I started to see how nice it would be to have it split into two separate pages. I will work on a solution in the next few days that uses two separate pages and handles the navigation in the AuthScreens.

I think the only problem with this is that AuthScreens doesn't know if we are an old user signing out, but it's easy enough to check that based on the route params.

That's sort of what I was getting at with my question, but I suppose it's not too many cases since we know that we are signed in. Thanks for helping me think through this.

@neil-marcellini
Copy link
Contributor Author

neil-marcellini commented Apr 29, 2022

@marcaaron Based our discussion above, here is what I'm planning to implement in another branch. Please let me know if this sounds good or if you have suggestions.

  • Create a component called LogOutOldUserPage stored under a /transition folder.
  • Move LogInWithShortLivedTokenPage under the /transition folder
  • Add LogOutOldUserPage to the auth screens in place of LogInWithShortLivedTokenPage
    <RootStack.Screen
    name={SCREENS.LOG_IN_WITH_SHORT_LIVED_TOKEN}
    options={defaultScreenOptions}
    component={LogInWithShortLivedTokenPage}
    />
  • LogOutOldUserPage is only responsible for logging out the older user.
class LogOutOldUserPage extends Component {
    componentDidMount() {
        const email = lodashGet(this.props.route.params, 'email', '');
        if (this.props.session && this.props.session.email !== email) {
            Log.info('[LogOutOldUserPage] Different user signed in - signing out');
            Session.signOutAndRedirectToSignIn();
        }
    }

    render() {
        return <FullScreenLoadingIndicator />;
    }
}
  • LogInWithShortLivedTokenPage is responsible for logging in a transitioning user with their short lived token, and waiting until Onyx has finished clearing to do so.
class LogInWithShortLivedTokenPage extends Component {
    componentDidMount() {
         if (!this.props.isOnyxDoneClearing) {
            return;
        }
        this.signInTransitioningUser();
    }

    componentDidUpdate() {
        if (!this.props.isOnyxDoneClearing) {
            return;
        }
        this.signInTransitioningUser();
    }

    signInTransitioningUser() {
        const accountID = lodashGet(this.props.route.params, 'accountID', '');
        const email = lodashGet(this.props.route.params, 'email', '');
        const shortLivedToken = lodashGet(this.props.route.params, 'shortLivedToken', '');
        Log.info('[LoginWithShortLivedTokenPage] signing in the transitioning user');
        Session.signInWithShortLivedToken(accountID, email, shortLivedToken);
    }

    render() {
        return <FullScreenLoadingIndicator />;
    }
}
  • And finally, we navigate to the exit route in the AuthScreens if we are signed in to the proper account and if it's not equal to workspace/new
        Linking.getInitialURL()
            .then((url) => {
                if (this.shouldCreateFreePolicy(url)) {
                    Policy.createAndGetPolicyList();
                    return;
                } else {
                    Policy.getPolicyList();
                    // exitTo is URI encoded because it could contain a variable number of slashes (i.e. "workspace/new" vs "workspace/<ID>/card")
                    const exitTo = decodeURIComponent(lodashGet(this.props.route.params, 'exitTo', ''));
                    // In order to navigate to a modal, we first have to dismiss the current modal. Without dismissing the current modal, if the user cancels out of the workspace modal,
                    // then they will be routed back to /transition/<accountID>/<email>/<authToken>/workspace/<policyID>/card and we don't want that. We want them to go back to `/`
                    // and by calling dismissModal(), the /transition/... route is removed from history so the user will get taken to `/` if they cancel out of the new workspace modal.
                    Log.info('[AuthScreens] Dismissing LogOutOldUserPage and navigating to the transition exit route');
                    Navigation.dismissModal();
                    Navigation.navigate(exitTo);
                }
            });

@marcaaron
Copy link
Contributor

marcaaron commented Apr 29, 2022

Create a component called LogOutOldUserPage stored under a /transition folder.
Move LogInWithShortLivedTokenPage under the /transition folder

NAB, but I think it can just stay in /pages

Add LogOutOldUserPage to the auth screens in place of LogInWithShortLivedTokenPage

👍

LogOutOldUserPage is only responsible for logging out the older user.

👍

LogInWithShortLivedTokenPage is responsible for logging in a transitioning user with their short lived token, and waiting until Onyx has finished clearing to do so.

I still question whether we should use componentDidUpdate() to tell that Onyx is clearing. I think this case is exceptional enough that we might consider finding a way to contain the "onyx is clearing" stuff inside the action itself and keep it out of this view (but I think maybe this is not a blocker as long as we document that we might be waiting for an old user to cleanly sign out)

And finally, we navigate to the exit route in the AuthScreens if we are signed in to the proper account and if it's not equal to workspace/new

That works for now though it would be better if the logic in the .then() was basically a switch statement for the exitTo. Looking at what you've got there and it doesn't quite makes sense because if shouldCreateFreePolicy() returns false then we are always navigating to an exitTo and dismissing a modal (which seems wrong).

@neil-marcellini
Copy link
Contributor Author

Ok cool, I will start working on that.

I think this case is exceptional enough that we might consider finding a way to contain the "onyx is clearing" stuff inside the action itself and keep it out of this view (but I think maybe this is not a blocker as long as we document that we might be waiting for an old user to cleanly sign out)

How would you wait for Onyx to finish clearing? My best idea would be to store a promise in the session file.

let afterOnyxClears = new Promise(resolve => resolve());

Then I would make redirectToSignIn return the Onyx.clear() promise and set that equal to afterOnyxClears:

function signOutAndRedirectToSignIn() {
    signOut();
    afterOnxyClears = redirectToSignIn();
    Log.info('Redirecting to Sign In because signOut() was called');
}

Then in signInWithShortLivedToken I would wait for onyx to finish clearing

function signInWithShortLivedToken(email, shortLivedToken) {
    Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, loading: true});

    afterOnyxClears.then(() => {
        afterOnyxClears = new Promise(resolve => resolve());
        createTemporaryLogin(shortLivedToken, email).then((response) => {
            if (response.jsonCode === 200) {
                User.getUserDetails();
                Onyx.merge(ONYXKEYS.ACCOUNT, {success: true});
            } else {
                const error = lodashGet(response, 'message', 'Unable to login.');
                Onyx.merge(ONYXKEYS.ACCOUNT, {error});
            }
        }).finally(() => {
            Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false});
        });
    });
}

I could probably refactor that to make it cleaner, but does that roughly work and make sense? I'm not super skilled with promise magic. How would you do it?

@neil-marcellini
Copy link
Contributor Author

I'm implementing the plan for separate transition pages. I'm closing this PR in favor of #8855, so let's move any additional conversations there.

@marcaaron
Copy link
Contributor

I could probably refactor that to make it cleaner, but does that roughly work and make sense? I'm not super skilled with promise magic. How would you do it?

There's this createOnReadyTask pattern we could use which basically creates a promise that is "waiting for some action to resolve it" and then has another function that sets it as "ready". Then we could forget about the React lifecycles and just do something like:

componentDidMount() {
    waitForOnyxToClear().then(() => signInWithShortLivedToken()))
}

That said, I think your proposal is fine without this - and I think anytime we have to use createOnReadyTask it feels like it points to some edge case or flaw with Onyx that we might want to rethink later.

@neil-marcellini neil-marcellini deleted the neil-wait-for-onyx branch May 2, 2022 20:36
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