Amplify-js: Amplify React Native auth.signout() does not return to Sign In page when called from App component

Created on 29 Aug 2018  路  39Comments  路  Source: aws-amplify/amplify-js

New iOS App From Scratch
Integrated React Native into existing app as per React Native Docs
Integrated AWS Amplify
Versions:
react: 16.4.1
react-native: 0.56.0
aws-amplify: 1.0.7
aws-amplify-react-native: 1.0.7
amazon-cognito-auth-js: 1.2.4
amazon-cognito-identity-js: 2.0.22

Description:
Sign Out fails

Steps To Recreate:

Create New iOS App In Xcode
Integrate Auth using withAuthenticator into the React Native app as per the instructions here: https://docs.aws.amazon.com/aws-mobile/latest/developerguide/react-native-add-user-sign-in.html
Create a sample "App Screen" that only has a single button (which calls Auth.signout()) from the auth package of aws-amplify
Build & Launch App
Enter credentials on Sign In page
Click Login
Successfully navigated to main App home page
Click "Sign Out" button
Notice that you are not returned back to the Sign In page

Things To Note:
You are being signed out (if you are running against the RN packager and refresh the app, you are taken directly to Sign In page). So it does appear that the actual sign out is happening, but the only issue that you are not redirected back to the sign in page.

This does happen with aws-amplify: 1.0.7 & aws-amplify-react-native: 1.0.7, but the issue does not occur if i drop back down to 1.0.6 for both packages.

Any help much appreciated. Thanks!

Update: OK so I did a bit more testing, and it seems that:

It does work when you use the greetings.js component (by passing in the true flag as the second param in withAuthenticator)
It does not work when you make a call to auth.signout() from within one of your app components.
Looking at the greetings.js components, it actually does two things:

Calls auth.signout()
Changes the authState to "signedOut"
Auth.signOut().then(() => this.changeState('signedOut')).catch(err => this.error(err));

However the app components (to the best of my knowledge) do not have access to the changeState function.

Note: Simply calling auth.signout() from within an app component on V1.0.6 works fine with no extra logic required. Simply upgrading to 1.0.7, 1.0.8 or 2.0.0 causes this to not work. Something has changed with the recent amplify changes it would seem.

Note: This is a reopened version of this issue (but now with updated steps to recreate) which was closed: https://github.com/aws-amplify/amplify-js/issues/1527

Auth React Native documentation

Most helpful comment

@markmckim - I simply do this in my App component to load the sign in component again. App is passed to the withAuthenticator.

Auth.signOut()
.then(() => {
this.props.onStateChange('signedOut', null);
})
.catch(err => {
console.log('err: ', err)
})

All 39 comments

+1 having this issue too

When using the Auth.signOut from within the withAuthenticator it will not sign out because it is only updating the session locally in AsyncStorage. You need to have a way to rerender the actual withAuthenticator component.

You can accomplish this with something like this:

// main component (index.js or something)
import MainApp from './App.js'
class AuthWrapper extends React.Component {
  rerender = () => this.forceUpdate()
  render() {
    return <MainApp rerender={this.rerender} />
  }
}

// component that is being wrapped with withAuthenticator component
class App extends Component {
  signOut = async () => {
    await Auth.signOut()
    this.props.rerender()
  }
  render() {
    return (
      <div style={styles.container}>
        <p style={styles.welcome}>Hello World</p>
      </div>
    );
  }
}

// new default export for withAuthenticator (this is to receive props & force the rerender)
export default props =>  {
  const AppComponent = withAuthenticator(App)
  return <AppComponent {...props} />
}

Let me know if this works @ejdigby @markmckim

Haven't had a chance to try this yet, but am I correct in assuming the last part of your example should look like this instead of what you have above:

// new default export for withAuthenticator (this is to receive props & force the rerender)
export default props =>  {
  const AppComponent = withAuthenticator(**App**)
  return <AppComponent {...props} />
}

I'll try and get a chance to test this out, but even so, do we know why this has changed with the most recent version when it has worked fine without this for some time? Some change in withAuthenticator/Authenticator?

Hey @markmckim, I had no idea this was actually working before this update. I'm not sure what changed.

I've also updated my example above to hopefully be more clear.

Hopefully someone else that worked on the change can chime in on why it worked before & why it doesn't now, & if they will be updating the functionality to match the past or not.

Just implemented this today and can confirm it works.

Thanks @dabit3

Great. Thanks @dabit3. However do we know if there is a reason the original behaviour was changed / no longer works? I'm guessing it should continue to work the way it always has without the steps needed above?

Hey @markmckim, good question. Let me cc @haverchuck @powerful23 & see if they have any insight on this.

@markmckim Hi can you provide some code snippets about how you are using Amplify to sign out?

Sure, I was literally just using auth.signout() hooked up to a button click on one of my app components. Nothing else, just that one line (and importing the auth module at the top of the file).

@markmckim so if I am understanding correctly, is it something like:

class App extends Component {
     signOut() {
           const { onStateChange } = this.props;
           Auth.signOut().then(() => {
                    onStateChange('signedOut');
           });
     }

     render() {
         return (
               <Button onPress={this.signOut} text='sign out'/>
        )
     }
}

export default withAuthenticator(App);

@powerful23 - No, previously, this worked just using something like this...

class App extends Component {
     signOut() {
           Auth.signOut();
     }

     render() {
         return (
               <Button onPress={this.signOut} text='sign out'/>
        )
     }
}

export default withAuthenticator(App);

@markmckim that's weird. I don't know how it would work but you need to let the Authenticator know that the auth state has changed by using onStateChange.

Was there any changes recently to Authenticator/withAuthenticator that would have caused this? I took a quick look but didn't see anything that jumped out.

Worst case scenario, I can pass down the onStateChange down to the app, but I guess it would be better to understand what changed / why it worked before

Maybe this commit caused it: https://github.com/aws-amplify/amplify-js/commit/8352bdb

Specifically the changes around line 90

@markmckim That change was to fix another bug so we can't actually revert it. So there are two different ways to set the current auth state:

  1. when the page is loaded or the app is loaded, the Authenticator will be mounted and call checkUser to check if the user is logged in
  2. when the user is logged in, the Authenticator will be unmounted so when you want to log the user out, you need to explicitly change the auth state to signedOut or signIn so that the Authenticator will be mounted again.

I think the reason why it worked without changing the auth state is that the Authenticator didn't check if it's mounted or not before checking the user. And Auth.signOut() will send a 'signOut' event which will be captured by Authenticator and then trigger that checkUser function. I am sorry that your code is broken due to that fix but we should follow the correct pattern to log user out when using Authenticator/withAuthenticator.

We will also working on to improve our documentation by providing more sample code. Thanks!

Hey @powerful23 - I totally agree. I think what you have described makes logical sense. I think previously I was relying on a loophole/defect in the code. I just thought I would check in case that was always the intentional behaviour and something had broken it. I can easily update my code as per the examples above. Thanks!!

@dabit3 - I've tried passing in a rerender function and calling it from my App but it doesn't appear to be triggering a rerender. The function is definitely reachable (i tried putting an alert in), but calling forceUpdate() doesn't seem to cause the withAuthenticator to rerender. Any ideas?

@markmckim did you rewrite the withAuthenticator component to be separate & receive the props? 馃憞

export default props =>  {
  const AppComponent = withAuthenticator(App)
  return <AppComponent {...props} />
}

@dabit3 I've got a slightly different pattern (as I'm passing in my own theme, and also using my own Auth components...although i don't believe this is related to the issue i'm seeing)...see below... (i've tried to replicate your example above but I may have the rerender defined and passed in the wrong place). Thanks again!

import React from "react";
import App from "./App";
import { AuthenticatorHOC } from "./auth";

// Wrap the main App class with a custom authenticator higher order component
class AuthenticatorApp extends React.Component {
  render() {
    const WrappedApp = AuthenticatorHOC(App);
    return <WrappedApp />;
  }
}

export default AuthenticatorApp;
import React from "react";
import { AmplifyTheme, withAuthenticator } from "aws-amplify-react-native";
import {
  ConfirmSignIn,
  ConfirmSignUp,
  ForgotPassword,
  Loading,
  RequireNewPassword,
  SignIn,
  SignUp,
  VerifyContact
} from ".";
import styles from "./styles";

// Override the default Amplify Theme to remove paddingTop
const Container = Object.assign(
  {},
  AmplifyTheme.container,
  styles.authenticatorContainer
);
const AuthTheme = Object.assign({}, AmplifyTheme, {
  container: Container
});

// Custom Authenticator Higher Order Component
function AuthenticatorHOC(App) {
  const AuthenticatorWrappedComponent = withAuthenticator(App, null, [
    <SignIn key="SignIn" />,
    <ConfirmSignIn key="ConfirmSignIn" />,
    <VerifyContact key="VerifyContact" />,
    <SignUp key="SignUp" />,
    <ConfirmSignUp key="ConfirmSignUp" />,
    <ForgotPassword key="ForgotPassword" />,
    <RequireNewPassword key="RequireNewPassword" />,
    <Loading key="Loading" />
  ]);
  return class WrappedComponent extends React.Component {
    rerender = () => this.forceUpdate();
    render() {
      // Add the custom theme
      return (
        <AuthenticatorWrappedComponent
          theme={AuthTheme}
          rerender={this.rerender}
        />
      );
    }
  };
}

export default AuthenticatorHOC;

Yeah, so the rerender method needs to be one level above the component that is the withAuthenticator component. I've rewritten my example to be clearer & in the correct order.

@dabit3 Perfect!!! That worked!!! I'm able to sign-out as expected from my App component. Your solution worked perfectly. Thanks for the help!

Hey @markmckim that's awesome glad it worked :).

@markmckim - I simply do this in my App component to load the sign in component again. App is passed to the withAuthenticator.

Auth.signOut()
.then(() => {
this.props.onStateChange('signedOut', null);
})
.catch(err => {
console.log('err: ', err)
})

Thanks @vutronic - Presumably with your solution below you did need to pass the onStateChange function down into the app component as it doesn't automatically get passed down?

(I did manage to get this working using @dabit3 's method however)

@markmckim the onStateChange is passed to my App component automatically by the Authenticator via props.

I'm using "aws-amplify-react-native": "^2.0.1".

Oh wow, yeah that's much simpler. I was totally overcomplicating it. Tried your approach @vutronic and that also worked...but without needing to explicitly pass in a rerender function.

Thanks for this!

Still cannot get any of this to work with any of the given examples. Would be nice if someone gave a full end to end example instead of continuously giving snippets of code and saying "THIS WORKS!"

@et304383 sorry you're having trouble. I've put together a gist at https://gist.github.com/dabit3/6c4f0a06b5e39fd53567a4a521143efa

import React from 'react'
import { View, Text } from 'react-native'
import { withAuthenticator } from 'aws-amplify-react-native'

function App(props) {
  function signOut() {
    Auth.signOut()
      .then(() => {
        props.onStateChange('signedOut', null);
      })
      .catch(err => {
        console.log('err: ', err)
      })
  }

  return (
    <View>
      <Text>Hello World</Text>
      <Text onPress={signOut}>Sign Out</Text>
    </View>
  )
}

export default withAuthenticator(App)

Have a look & let me know if that helps out. It should be a full working example.

I'll give this a try but our challenge lies in the fact that our logout function is in a hamburger menu that's loaded in app.js. Our app.js is not wrapped with an authenticator.

So we are having scope issues trying to access props where the onStateChange event resides from the menu when only some of our components are wrapped.

I had a similar issue when I needed props.onStateChange inside my interceptor. This is what I did.

import { setInterceptors } from './services/api';

class App extends React.Component {
  constructor(props) {
    super(props);
    setInterceptors(props.onStateChange);
  }

  render() {
    return (
      <Provider store={store}>
        <View style={styles.container}>
          <RootNavigation {...this.props} />
        </View>
      </Provider>
    );
  }
}
const setInterceptors = onStateChange => {
  axios.interceptors.request.use(
    tokenProvider({
      header: 'Authorization',
      headerFormatter: token => `Bearer ${token}`,
      getToken: async () => {
        const session = await Auth.currentSession();
        return session.getAccessToken().getJwtToken();
      },
    }),
  );

  axios.interceptors.response.use(
    response => {
      return response;
    },
    error => {
      if (error && error.response && error.response.status === 401) {
        return Auth.signOut().then(() => {
          onStateChange('signedOut');
        });
      }
      return Promise.reject(error);
    },
  );
};

export { setInterceptors };

in your case, create a service which is singleton and inject onStateChange when you create an instance of that service

btw. my App class is wrapped with withAuthenticator and some of my auth states are managed by custom classes

@et304383 Another way to handle this would be to use the Hub class & listen for auth changes.

import { Hub } from 'aws-amplify'

componentDidMount() {
  Hub.listen('auth', function(authData) {
    console.log('authData: ', authData)
    if (authData.payload.event === 'signOut') {
      // rerender or do something here in the hamburger menu
    }
  })
}

https://aws-amplify.github.io/docs/js/hub#listening-for-messages

function App(props) {
  function signOut() {
    Auth.signOut()
      .then(() => {
        props.onStateChange('signedOut', null);
      })
      .catch(err => {
        console.log('err: ', err)
      })
  }

  return (
      <Button onPress={signOut}>Sign Out</Text>
  )
}

export default withAuthenticator(App)

The above worked for me, but let's say I have a Sign Out button component in a separate js file in my code that I import at multiple different pages on my app. How can I make that button work? I can't find a way to bind that button's onClick event to the sign out function in the App function above

More specifically, how do I use it when I'm using a navigator and I want to sign out from one of the pages?

I configure a navigator and then pass it to withAuthenticator(), I don't think the same kind of onStateChange prop is passed to this App object

App = createAppContainer(TabNavigator);
export default withAuthenticator(App);

Thanks

Hey I did a mix of @dabit3 solutions because I needed my Auth.signOut() in an other component.
Maybe it can work for you as well @anirudh-goyal

Navbar.jsx

import { Auth } from "aws-amplify";

export default function SimpleMenu(props) {
 return (
  <div>
   <button onClick={() => Auth.signOut()}>Sign out</button>
  </div
}

App.js

import Navbar from "./components/navbar.jsx";
import Amplify from 'aws-amplify';
import { Authenticator} from "aws-amplify-react";

class App extends React.Component {
  constructor(props) {
    super(props);
  };

componentDidMount() {
    Hub.listen('auth', (authData) => {
      console.log('authData: ', authData)
      if (authData.payload.event === 'signOut') {
        this.props.onStateChange('signedOut', null);
      }
    })
  }

render() {
    return (
      <div>
        <Navbar ></Navbar>
      </div>
    );
  }
}

class AppWithAuth extends React.Component {

  render() {
    return (
      <Authenticator>
        <App />
      </Authenticator>
    );
  }
}

export default AppWithAuth;

@Aelaiig - I think you can just simply pass a function as prop to the nav bar:

Navbar.j

export default function SimpleMenu(props) {
 return (
  <div>
   <button onClick={() => {props.signOut()}>Sign out</button>
  </div
}

App.js

import Navbar from "./components/navbar.jsx";
import Amplify, { Auth }  from 'aws-amplify';
import { Authenticator} from "aws-amplify-react";

class App extends React.Component {
  constructor(props) {
    super(props);
  };

signOut() {
    Auth.signOut().then(() => {
        this.props.onStateChange(("signedOut");
    });
}

render() {
    return (
      <div>
        <Navbar signOut="{this.signOut.bind(this)}"></Navbar>
      </div>
    );
  }
}

class AppWithAuth extends React.Component {

  render() {
    return (
      <Authenticator>
        <App />
      </Authenticator>
    );
  }
}

export default AppWithAuth;

@vvantruong I tried this way but if you do like that when the signOut method is called, the app component don't have the good props and don't recognise the onStateChange() method

@Aelaiig Did you figure it out? I have split my app into components and used redux, but cant trigger a signout back to the sign in page

@eyalabadi98 the solution I wrote before work for me. In my app component I use Hub from Amplify who listen payloads of the app.
When a signOut is done from any component the Hub.listen('auth', async (authData) => { ... }) receive the payload and it can apply the good state of the app component with this.props.onStateChange('signedOut', null);

Has anyone tried to implement this when the sign out button is under a TabNavigator? onStateChange does get passed down to App and even the TabNavigator itself, but not individual tabs

Shall I able to pass any params while onStateChange() in component.

In my custom screen while navigating this.props.onStateChange('signIn',{}); I need to pass params and how can I get this in my signIn screen?

Another one clarification kindly clarify, If I got any error in anyone of my authentication screen, it still there if I go to anyother screen and back to the screen which i already saw the error.

Scenario:

  1. Go to signUp screen
  2. Produce any error in the screen(Username is not empty)
  3. Click on the Back to sign In
  4. It navigates to Sign In screen
  5. Again click on the SignUp link
  6. It navigates to SignUp screen and still shows the (Username is not empty)

How can i resolve this error retaining in the screen?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kennu picture kennu  路  64Comments

jiachen247 picture jiachen247  路  79Comments

gHashTag picture gHashTag  路  135Comments

nomadus picture nomadus  路  53Comments

danielgeri picture danielgeri  路  78Comments