Search code examples
react-nativetokenreact-contextauthentication-flows

Authentication flows | React Navigation 6 - How to get webToken?


When I learn authentication flow of react navigation 6.0, I read the sample code which used redux, it used dummy token, but In my react native project, I have to get the real token from server, So I tried to add some code in the sample project.

import * as React from 'react';
import { Button, Text, TextInput, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { useMutation, gql } from '@apollo/client';
import { 
  ApolloClient, 
  ApolloProvider,
  createHttpLink,
  InMemoryCache 
} from '@apollo/client';

import { setContext } from 'apollo-link-context';

const SIGNIN_USER = gql`
    mutation signIn($email: String!, $password: String!) {
        signIn(email: $email, password: $password)
    }
`;

// for example, my server address
const API_URI='https://xxxx.herokuapp.com/api';
const cache = new InMemoryCache();

const client = new ApolloClient({
  uri: API_URI,
  cache: new InMemoryCache()
});

const AuthContext = React.createContext();

function SplashScreen() {
  return (
    <View>
      <Text>Loading...</Text>
    </View>
  );
}

function HomeScreen() {
  const { signOut } = React.useContext(AuthContext);

  return (
    <View>
      <Text>Signed in!</Text>
      <Button title="Sign out" onPress={signOut} />
    </View>
  );
}

function SignInScreen() {
  const [username, setUsername] = React.useState('');
  const [password, setPassword] = React.useState('');

  const { signIn } = React.useContext(AuthContext);

  return (
    <View>
      <TextInput
        placeholder="Username"
        value={username}
        onChangeText={setUsername}
      />
      <TextInput
        placeholder="Password"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      <Button title="Sign in" onPress={() => signIn({ username, password })} />
    </View>
  );
}

const Stack = createStackNavigator();

export default function App({ navigation }) {
  const [state, dispatch] = React.useReducer(
    (prevState, action) => {
      switch (action.type) {
        case 'RESTORE_TOKEN':
          return {
            ...prevState,
            userToken: action.token,
            isLoading: false,
          };
        case 'SIGN_IN':
          return {
            ...prevState,
            isSignout: false,
            userToken: action.token,
          };
        case 'SIGN_OUT':
          return {
            ...prevState,
            isSignout: true,
            userToken: null,
          };
      }
    },
    {
      isLoading: true,
      isSignout: false,
      userToken: null,
    }
  );

  React.useEffect(() => {
    // Fetch the token from storage then navigate to our appropriate place
    const bootstrapAsync = async () => {
      let userToken;

      try {
        // Restore token stored in `SecureStore` or any other encrypted storage
        // userToken = await SecureStore.getItemAsync('userToken');
      } catch (e) {
        // Restoring token failed
      }

      // After restoring token, we may need to validate it in production apps

      // This will switch to the App screen or Auth screen and this loading
      // screen will be unmounted and thrown away.
      dispatch({ type: 'RESTORE_TOKEN', token: userToken });
    };

    bootstrapAsync();
  }, []);

  const authContext = React.useMemo(
    () => ({
      signIn: async ({username, password}) => {

        useMutation(SIGNIN_USER, {
          variables:{
            email: username,
            password: password
          },
          onCompleted: data => {
            console.log(data);
          } 
        });

        // In a production app, we need to send some data (usually username, password) to server and get a token
        // We will also need to handle errors if sign in failed
        // After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
        // In the example, we'll use a dummy token

        dispatch({ type: 'SIGN_IN', token: data.signIn });
      },
      signOut: () => dispatch({ type: 'SIGN_OUT' }),
      signUp: async (data) => {
        // In a production app, we need to send user data to server and get a token
        // We will also need to handle errors if sign up failed
        // After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
        // In the example, we'll use a dummy token

        dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
      },
    }),
    []
  );

  return (
    <AuthContext.Provider value={authContext}>
      <ApolloProvider client={client}>
        <NavigationContainer>
          <Stack.Navigator>
            {state.isLoading ? (
              // We haven't finished checking for the token yet
              <Stack.Screen name="Splash" component={SplashScreen} />
            ) : state.userToken == null ? (
              // No token found, user isn't signed in
              <Stack.Screen
                name="SignIn"
                component={SignInScreen}
                options={{
                  title: 'Sign in',
                  // When logging out, a pop animation feels intuitive
                  animationTypeForReplace: state.isSignout ? 'pop' : 'push',
                }}
              />
            ) : (
              // User is signed in
              <Stack.Screen name="Home" component={HomeScreen} />
            )}
          </Stack.Navigator>
        </NavigationContainer>
      </ApolloProvider>
    </AuthContext.Provider>
  );
}

And I get the error,

 WARN  Possible Unhandled Promise Rejection (id: 0):
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app

I searched it, it is because I use hooks in useMemo.

https://reactjs.org/warnings/invalid-hook-call-warning.html
To avoid confusion, it’s not supported to call Hooks in other cases:

🔴 Do not call Hooks in class components.
🔴 Do not call in event handlers.
🔴 Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.

But, I have to send some data to server to get a token, and save token by Securestore or something else, If I can't write this section here, where?

I have no idea about where to write this section of code. My understanding to Redux in this situation: when a user click button of 'sign in', call the function of signIn which is declared in authContext, useMemo, in use useMemo, get the token, save the token, then dispatch to notify data change, then go to the reducer.


Solution

  • After read a basic tutorial of redux, I have finished that by:

    1. In SignInScreen, get the web token by 'login' function, which is triggered by the button of 'Sign in'
    2. After execute it successfully, invoke the 'signIn' to update the state(by call dispatch).
    3. I was confused with the name of 'signIn' function before, which is the same with my eventHandler function of 'signIn' button in SignInScreen. Actually, they are in duty of different logic.

    import * as React from 'react';
    import { Button, Text, TextInput, View } from 'react-native';
    import { NavigationContainer } from '@react-navigation/native';
    import { createStackNavigator } from '@react-navigation/stack';
    import { useMutation, gql } from '@apollo/client';
    import { 
      ApolloClient, 
      ApolloProvider,
      createHttpLink,
      InMemoryCache 
    } from '@apollo/client';
    
    import { setContext } from 'apollo-link-context';
    
    const SIGNIN_USER = gql`
        mutation signIn($email: String!, $password: String!) {
            signIn(email: $email, password: $password)
        }
    `;
    
    const API_URI='https://jseverywhere.herokuapp.com/api';
    const cache = new InMemoryCache();
    
    const client = new ApolloClient({
      uri: API_URI,
      cache: new InMemoryCache()
    });
    
    const AuthContext = React.createContext();
    
    function SplashScreen() {
      return (
        <View>
          <Text>Loading...</Text>
        </View>
      );
    }
    
    function HomeScreen() {
      const { signOut } = React.useContext(AuthContext);
    
      return (
        <View>
          <Text>Signed in!</Text>
          <Button title="Sign out" onPress={signOut} />
        </View>
      );
    }
    
    function SignInScreen() {
      const [username, setUsername] = React.useState('');
      const [password, setPassword] = React.useState('');
    
      const { signIn } = React.useContext(AuthContext);
    
      const [logIn, {loading, error}] = useMutation(SIGNIN_USER, {
        variables:{
          email: username,
          password: password
        },
        onCompleted: data => {
          console.log(data);
          signIn(data);
        } 
      });
    
      if(loading)
        return <Text>Loading...</Text>;
      if(error)
        return <Text>Error--{error.message}</Text>;
    
      return (
        <View>
          <TextInput
            placeholder="Username"
            value={username}
            onChangeText={setUsername}
          />
          <TextInput
            placeholder="Password"
            value={password}
            onChangeText={setPassword}
            secureTextEntry
          />
          <Button title="Sign in" onPress={() => logIn()} />
        </View>
      );
    }
    
    const Stack = createStackNavigator();
    
    export default function App({ navigation }) {
      const [state, dispatch] = React.useReducer(
        (prevState, action) => {
          switch (action.type) {
            case 'RESTORE_TOKEN':
              return {
                ...prevState,
                userToken: action.token,
                isLoading: false,
              };
            case 'SIGN_IN':
              return {
                ...prevState,
                isSignout: false,
                userToken: action.token,
              };
            case 'SIGN_OUT':
              return {
                ...prevState,
                isSignout: true,
                userToken: null,
              };
          }
        },
        {
          isLoading: true,
          isSignout: false,
          userToken: null,
        }
      );
    
      React.useEffect(() => {
        // Fetch the token from storage then navigate to our appropriate place
        const bootstrapAsync = async () => {
          let userToken;
    
          try {
            // Restore token stored in `SecureStore` or any other encrypted storage
            // userToken = await SecureStore.getItemAsync('userToken');
          } catch (e) {
            // Restoring token failed
          }
    
          // After restoring token, we may need to validate it in production apps
    
          // This will switch to the App screen or Auth screen and this loading
          // screen will be unmounted and thrown away.
          dispatch({ type: 'RESTORE_TOKEN', token: userToken });
        };
    
        bootstrapAsync();
      }, []);
    
      const authContext = React.useMemo(
        () => ({
          signIn: async (data) => {
    
            // In a production app, we need to send some data (usually username, password) to server and get a token
            // We will also need to handle errors if sign in failed
            // After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
            // In the example, we'll use a dummy token
    
            dispatch({ type: 'SIGN_IN', token: data.signIn });
          },
          signOut: () => dispatch({ type: 'SIGN_OUT' }),
          signUp: async (data) => {
            // In a production app, we need to send user data to server and get a token
            // We will also need to handle errors if sign up failed
            // After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
            // In the example, we'll use a dummy token
    
            dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
          },
        }),
        []
      );
    
      return (
        <AuthContext.Provider value={authContext}>
          <ApolloProvider client={client}>
            <NavigationContainer>
              <Stack.Navigator>
                {state.isLoading ? (
                  // We haven't finished checking for the token yet
                  <Stack.Screen name="Splash" component={SplashScreen} />
                ) : state.userToken == null ? (
                  // No token found, user isn't signed in
                  <Stack.Screen
                    name="SignIn"
                    component={SignInScreen}
                    options={{
                      title: 'Sign in',
                      // When logging out, a pop animation feels intuitive
                      animationTypeForReplace: state.isSignout ? 'pop' : 'push',
                    }}
                  />
                ) : (
                  // User is signed in
                  <Stack.Screen name="Home" component={HomeScreen} />
                )}
              </Stack.Navigator>
            </NavigationContainer>
          </ApolloProvider>
        </AuthContext.Provider>
      );
    }