React Native Logout with StackNavigator - can't find route

103 Views Asked by At

I have a react-native app that stores login credentials in a jwt. My logout function looks like this:

              logOut={() => {
                AsyncStorage.getAllKeys()
                  .then((keys) => AsyncStorage.multiRemove(keys))
                    .then(() =>
                        navigation.reset({
                          index: 0,
                          routes: [{ name: 'Login' }],
                          key: null,
                      }),
                    )

My StackNavigator (in App.jsx) looks like this:

       <NavigationContainer >
        <Stack.Navigator>
          {valid_token ? (
            consented ? ( 
              <>
              <Stack.Screen name="Home" component={Home} />
              <Stack.Screen name="Nav" component={Nav} /> // Nav is the component with the login function
          </>
            ): (
              <>
              <Stack.Screen name="ConsentForm" component={ConsentForm} />
              </>
            )
          ) : (
            <>
            <Stack.Screen name='Login'>
                  {(props) => <Login setExpired={setValidToken} setConsented={setConsented} />}
            </Stack.Screen>
            </>
          )}
        </Stack.Navigator>
      </NavigationContainer>

valid_token is set based on the value of the jwt, which is in async storage. I am clearing out the token in the logout function, however, App.jsx doesn't see that. I also am not sure if I really have to pass the setValidToken and setConsented props throughout the whole app (the nav bar, which contains the logout, is on every screen). When I don't pass props and run the code as is, I get this error:

The action 'RESET' with payload {"index":0,"routes":[{"name":"Login"}],"key":null} was not handled by any navigator.

I tried replacing the navigation.reset part of the logout function with navigation.dispatch(StackActions.popToTop()) - this takes me to the Home page.

Is passing props throughout the whole app my only option?

EDIT: I tried adding a context called AuthContext as per the answer below. I am able to log in with the new AuthContext, however my error for log out is the exact same. I tried just having the navigate (without removing the jwt token) but it doesn't make a difference. I've added more code below to hopefully give more clarity.

Here's my AuthContext.jsx:

import {createContext } from "react";

const AuthContext = createContext();

export default AuthContext; 

Here's my new App.jsx:

import React, { useEffect } from 'react'
// import lots of other stuff
import AuthContext from './AuthContext';

export default function App() {

  const [consented, setConsented] = React.useState(false) // I'll figure this out later
  const [loggedIn, setLoggedIn] = React.useState(false)

  const Stack = createNativeStackNavigator()

  const getData = async () => {
    return await AsyncStorage.getItem('@myapp:jwt'); 
  }


  useEffect(() => {
    getData().then((value) => {
      value = JSON.parse(value)
      if(value == null){
        setLoggedIn(false)
      }else{
        const now = moment()
        setLoggedIn(!(now.isAfter(moment(value?.expiry))))
        setConsented(value?.consented)
      }
    })
  })

  return (
    <AuthContext.Provider
    value={{
      loggedIn,
      setLoggedIn
    }}>
      <NavigationContainer >
      <Stack.Navigator>
        {loggedIn ? (
          consented ? ( 
           <>
            <Stack.Screen name="Home" component={Home} />
            <Stack.Screen name="Nav" component={Nav} /> 
           </>
          ): (
            <>
            <Stack.Screen name="ConsentForm" component={ConsentForm} />
            </>
          )
        ) : (
          <>
          <Stack.Screen name='Login' component={Login}/>
          </>
        )}
      </Stack.Navigator>
    </NavigationContainer>
  </AuthContext.Provider>

  )
}

And finally, here's relevant stuff from my Login.jsx which should be the child component of App.jsx:

# many imports
import  AuthContext  from '../AuthContext'


const Login = ()  => {
  const navigation = useNavigation();
  const [text, onChangeText] = React.useState(null)
  const [number, onChangeNumber] = React.useState(null)
  const {loggedIn, setLoggedIn} = React.useContext(AuthContext)
  

  const postLoginInformation = () => {
    fetch(`${MYAPP_URL}/login/`, {
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        username: text,
        password: number,
      }),
    })
      .then(async (res) => {
        const response = await res.json()
        const jwt = JSON.parse(response).jwt
        const decoded_jwt = jwt_decode(jwt))
        await AsyncStorage.setItem(
          '@myapp:jwt',
          JSON.stringify(decoded_jwt),
        )
        setLoggedIn(true)
        //setConsented(decoded_jwt.consented) // I'll figure this out later
      }).catch((e) => console.log('login error', e, `${MYAPP_URL}/login/`))
  }

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <TextInput
          style={styles.input}
          onChangeText={onChangeText}
          value={text}
          placeholder="Username"
        />
        <TextInput
          style={styles.input}
          secureTextEntry
          onChangeText={onChangeNumber}
          value={number}
          placeholder="Password"
        />
        <TouchableOpacity
          style={{ height: 100 }}
          onPress={() => postLoginInformation()}
        >
          <Image
            source={require('../images/login/login_button.png')}
          />
        </TouchableOpacity>
      </View>
    </SafeAreaView>
  )
}

export default Login

Appreciate any help! Thank you

1

There are 1 best solutions below

0
On

Do not manually navigate when using conditionally rendering screens. Logout should just invalidate a state and login screen will automatically appear. Use Context to manage the state instead of passing props. Here is a super simple example:

const AuthContext = createContext();

function LoginScreen() {
  const { loggedIn, setLoggedIn } = useContext(AuthContext);
  return (
    <>
      <Button title="Login" onPress={() => setLoggedIn(true)} />
    </>
  );
}

function HomeScreen() {
  const { loggedIn, setLoggedIn } = useContext(AuthContext);
  return (
    <>
      <Button title="Logout" onPress={() => setLoggedIn(false)} />
    </>
  );
}

function App() {
  const [loggedIn, setLoggedIn] = useState(false);
  useEffect(() => {
    const getInitialState = async () => {
      const loggedIn = JSON.parse(await AsyncStorage.getItem('loggedIn'));
      setLoggedIn(loggedIn);
    };
    getInitialState();
  }, []);

  return (
    <AuthContext.Provider
      value={{
        loggedIn,
        setLoggedIn: (value) => {
          AsyncStorage.setItem('loggedIn', JSON.stringify(value));
          setLoggedIn(value);
        },
      }}>
      {loggedIn ? <HomeScreen /> : <LoginScreen />}
    </AuthContext.Provider>
  );
}

export default App;

See Authentication flows for details.