node.jsreact-nativemobile-application

How do I keep user logged in even after reload of the app?


How do I keep the current registered user of my app logged in the app even after reloading the app or closing it out? The current state sends the user straight to my Login screen after reloading the app, but, if I just close it out (swipe up on iPhone for ex) it keeps the user logged in no problem. I am using AsyncStorage to capture and later access that information in different screens or for historical users, however I cannot seem to figure out how to accomplish this after realod. For context, I am using Expo with IOS simulator. Below you will find my App.js, Login.js screen and DBAdapter component used to pull/add user information from my database.

App.js:

import React, { createContext, useState, useEffect } from 'react';
import { StyleSheet, } from 'react-native';
import BarCodeScan from './screens/BScanner';
import Favorites from './screens/Favorites';
import ProductInfo from './screens/ProductInfo';
import BottomTabs from './components/BottomTabs';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import TermsAndService from './screens/terms_and_service';
import { ListProvider } from './screens/ListContext';
import AboutUs from './screens/About Us';
import ProductDetailScreen from './screens/ProductDetail';
import WikiScreen from './screens/WikiScreen';
import Nutrition from './screens/Nutrition';
import Login from './components/Login';
import { initializeDB } from './DBA/DBAdapter';
import ChangePassword from './components/ChangePassword';
import Settings from './screens/Settings';
import UserProfile from './screens/UserProfile';
import AsyncStorage from '@react-native-async-storage/async-storage';

const Stack = createStackNavigator();

export const GlobalContext = createContext();

export default function App() {
  const [user, setUser] = useState(null);

  const loginUser = (username) => {
    //console.log('Logging in user:', username);
    const user = { id: 1, name: username,  };
    setUser(user);
  };

  const logoutUser = () => {
    setUser(null);
  };

  useEffect(() => {
    initializeDB();

    const loadUserData = async () => {
      try {
        const userData = await AsyncStorage.getItem('userData');
        if (userData) {
          const parsedUserData = JSON.parse(userData);
          loginUser(parsedUserData.username);
        } else {
          console.log('No user data found in AsyncStorage.');
        }
      } catch (error) { 
        console.error('Error loading user data from AsyncStorage:', error);
      }
    };
    
    loadUserData();
  }, []);

  return (
    <GlobalContext.Provider value={{ loginUser, logoutUser }}>
      <ListProvider>
        <NavigationContainer>
          <Stack.Navigator initialRouteName="Login">
            <Stack.Screen name="BottomTabs" options={{ headerShown: false, title: 'Back'}} component={({ navigation }) => ( <BottomTabs screenProps={{ navigation }} /> )} />
            <Stack.Screen name="Login" component={Login} options={{ headerShown: false }} />
            <Stack.Screen name="ProductInfo" component={ProductInfo} options={{ title: 'Product Details' }} />
            <Stack.Screen name="WikiScreen" component={WikiScreen} options={{ title: 'Wikipedia' }} />
            <Stack.Screen name="ProductDetails" component={ProductDetailScreen} options={{ title: 'Product Details' }} />
            <Stack.Screen name="Nutrition" component={Nutrition} options={{ title: 'Nutritional Facts' }} />
            <Stack.Screen name="ChangePassword" component={ChangePassword} options={{ headerShown: false }} />
            <Stack.Screen name="BarCodeScan" component={BarCodeScan} />
            <Stack.Screen name="TermsAndService" component={TermsAndService} />
            <Stack.Screen name="About Us" component={AboutUs} />
            <Stack.Screen name="Favorites" component={Favorites} />
            <Stack.Screen name="Settings" component={Settings} options={{ title: 'Settings' }} />
            <Stack.Screen name="UserProfile" component={UserProfile} options={{ title: 'User Profile' }} />
          </Stack.Navigator>
        </NavigationContainer>
      </ListProvider>
    </GlobalContext.Provider>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

Login.js:

import React, { useContext, useState } from 'react';
import { View, TextInput, TouchableOpacity, Text, StyleSheet, Keyboard, TouchableWithoutFeedback, Image, } from 'react-native';
import Toast from 'react-native-toast-message';
import { verifyUser, addUser } from '../DBA/DBAdapter';
import { useNavigation } from '@react-navigation/native';
import { GlobalContext } from '../App';
import AsyncStorage from '@react-native-async-storage/async-storage';
import Icon from 'react-native-vector-icons/FontAwesome';

const Login = () => {
  const [isLogin, setIsLogin] = useState(true);
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [email, setEmail] = useState('');
  const navigation = useNavigation();
  const { loginUser } = useContext(GlobalContext);
  const [loginError, setLoginError] = useState('');
  const [registerError, setRegisterError] = useState('');
  const [showPassword, setShowPassword] = useState(false);

  const showToast = (message) => {
    Toast.show({
      text1: message,
      position: 'top',
      type: 'error',
      topOffset: 100,
      borderColor: 'red',
      duration: 1000,
    });
  };

  const handleToggle = () => {
    setIsLogin(!isLogin);
    setUsername('');
    setPassword('');
    setEmail('');
    setLoginError('');
    setRegisterError('');
  };

  const handleLogin = () => {
    if (username.length === 0 || password.length === 0) {
      setLoginError('Please fill in both username and password.');
      showToast('Please fill in both username and password.');
    } else if (username.length <= 7) {
      setLoginError('Username should be at least 8 characters long.');
      showToast('Username should be at least 8 characters long.');
    } else if (password.length <= 7) {
      setLoginError('Password should be at least 8 characters long.');
      showToast('Password should be at least 8 characters long.');
    } else {
      verifyUser(username, password, (result) => {
        if (result.success) {
          loginUser(result.user);
          const userData = result.user;
          AsyncStorage.setItem('userData', JSON.stringify(userData));
          navigation.navigate('BottomTabs', { user: result.user });
        } else {
          setLoginError('Invalid credentials. Please check your username and password.');
          showToast('Invalid credentials. Please check your username and password.');
        }
      });
    }
  };

  const isValidEmail = (email) => {
    const emailPattern = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/;
    return emailPattern.test(email);
  };

  const handleRegister = () => {
    if (username.length === 0) {
        setRegisterError('Please fill in the username.');
        showToast('Please fill in the username.');
    } else if (password.length === 0) {
        setRegisterError('Please fill in the password.');
        showToast('Please fill in the password.');
    } else if (email.length === 0 || !isValidEmail(email)) {
        setRegisterError('Email field is invalid.');
        showToast('Email field is invalid.');
    } else if (username.length <= 7) {
        setRegisterError('Username should be at least 8 characters long.');
        showToast('Username should be at least 8 characters long.');
    } else if (password.length <= 7) {
        setRegisterError('Password should be at least 8 characters long.');
        showToast('Password should be at least 8 characters long.');
    } else {
        addUser(username, password, email, (result) => {
            if (result.success) {
                loginUser(result.user);
                const userData = {
                  username,
                  password,
                  email,
                };
                AsyncStorage.setItem('userData', JSON.stringify(userData));
                navigation.navigate('BottomTabs', { user: result.user });
            } else {
                setRegisterError(result.error);
                showToast(result.error);
            }
        });
    }
};

  const handleClear = () => {
    setUsername('');
    setPassword('');
    setEmail('');
  };

  return (
    <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
    <View style={styles.container}>
    <View style={styles.imageContainer}>
      <Image source={require('../assets/halalfinds2.png')} style={styles.image} resizeMode="contain" />
    </View>
        <View style={{ backgroundColor: "white", paddingBottom: 45 }}>
            <Text
            style={{
                fontWeight: 'bold',
                textAlign: 'center',
                fontSize: 30,
                paddingTop: 30,
                color: "rgba(79, 193, 35, 1)",
            }}
            >HalalFinds</Text>
            <Text
            style={{
                fontWeight: 'bold',
                textAlign: 'center',
                fontSize: 22,
                paddingTop: 10,
                color: "rgba(164, 164, 164, 1)",
            }}
            >Find Halal Products Easier</Text>
        </View>  
      <Toast />
      <View style={styles.toggleContainer}>
        <TouchableOpacity
          style={[styles.toggleButton, isLogin ? styles.activeButton : null]}
          onPress={handleToggle}
          disabled={isLogin}
        >
          <Text
            style={{
              ...styles.toggleButtonText,
              color: isLogin ? 'white' : 'gray',
            }}
          >
            Login
          </Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={[
            styles.toggleButton,
            !isLogin ? styles.activeButton : null,
          ]}
          onPress={handleToggle}
          disabled={!isLogin}
        >
          <Text
            style={{
              ...styles.toggleButtonText,
              color: isLogin ? 'gray' : 'white',
            }}
          >
            Register
          </Text>
        </TouchableOpacity>
      </View>
      <View>
        <TextInput
          style={styles.input}
          placeholder="Username or email"
          onChangeText={(text) => setUsername(text)}
          value={username}
          placeholderTextColor="gray"
        />
        <View style={styles.passwordInput}>
            <TextInput
              style={styles.passInput}
              placeholder="Password"
              onChangeText={(text) => setPassword(text)}
              value={password}
              secureTextEntry={!showPassword} // Toggle secureTextEntry based on showPassword state
              placeholderTextColor="gray"
            />
            <TouchableOpacity
              onPress={() => setShowPassword(!showPassword)}
              style={styles.eyeIcon}
            >
              <Icon
                name={showPassword ? 'eye' : 'eye-slash'}
                size={20}
                color="gray"
              />
            </TouchableOpacity>
            </View>
        {!isLogin && (
          <TextInput
            style={styles.input}
            placeholder="Email"
            onChangeText={(text) => setEmail(text)}
            value={email}
            secureTextEntry={false}
            placeholderTextColor="gray"
          />
        )}
        <Text style={styles.errorText}>{isLogin ? loginError : registerError}</Text>
        <View style={styles.buttonContainer}>
          {username.length > 0 && (
            <TouchableOpacity
              style={styles.clearButton}
              onPress={handleClear}
            >
              <Text style={styles.clearButtonText}>Clear</Text>
            </TouchableOpacity>
          )}
          <TouchableOpacity
            style={styles.button}
            onPress={isLogin ? handleLogin : handleRegister}
          >
            <Text style={styles.buttonText}>
              {isLogin ? 'Login' : 'Register'}
            </Text>
          </TouchableOpacity>
        </View>
      </View>
    </View>
    </TouchableWithoutFeedback>
  );
};

const styles = StyleSheet.create({
    imageContainer: {
        alignItems: 'center',
        justifyContent: 'center',
        marginBottom: 20,
      },
      image: {
        width: 160,
        height: 160,
      },
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingHorizontal: 16,
    backgroundColor: 'white',
    marginBottom: 60,
  },
  toggleContainer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    marginBottom: 16,
  },
  toggleButton: {
    color: 'rgba(79, 193, 35, 1)',
    paddingHorizontal: 16,
    paddingVertical: 8,
    borderRadius: 8,
    borderWidth: 1,
    borderColor: '#888',
  },
  activeButton: {
    backgroundColor: 'rgba(79, 193, 35, 1)',
    borderColor: 'rgba(79, 193, 35, 1)',
  },
  toggleButtonText: {
    color: 'black',
    fontSize: 16,
    fontWeight: 'bold',
  },
  input: {
    height: 40,
    borderColor: 'gray',
    borderWidth: 1,
    marginBottom: 16,
    paddingHorizontal: 10,
    borderRadius: 8,
    color: 'black',
  },
  buttonContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginBottom: 16,
  },
  button: {
    flex: 1,
    backgroundColor: 'rgba(79, 193, 35, 1)',
    borderRadius: 8,
    paddingVertical: 12,
  },
  clearButton: {
    flex: 1,
    backgroundColor: 'red',
    borderRadius: 8,
    paddingVertical: 12,
    marginRight: 8,
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: 'bold',
    textAlign: 'center',
  },
  clearButtonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: 'bold',
    textAlign: 'center',
  },
  errorText: {
    color: 'red',
    marginBottom: 16,
  },
  passwordInput: {
    flexDirection: 'row',
    alignItems: 'center',
    borderWidth: 1,
    borderColor: 'gray',
    borderRadius: 8,
    marginBottom: 16,
    paddingHorizontal: 10,
  },
  passInput: {
    flex: 1,
    height: 40,
    borderColor: 'gray',
    borderWidth: 0,
    borderRadius: 0,
    marginBottom: 0,
    paddingHorizontal: 0,
  },
  eyeIcon: {
    padding: 10,
  },
});

export default Login;

DBAdapter.js:

import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('Main.db');

export const initializeDB = () => {
    return new Promise((resolve, reject) => {
        db.transaction((tx) => {
            tx.executeSql(
                "CREATE TABLE IF NOT EXISTS Users (ID INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, password TEXT, email TEXT, joinedDate TEXT);",
                [],
                () => {
                    console.log('Table Created Successfully');
                    resolve();
                },
                (tx, error) => {
                    console.log('Error Creating Table:', error);
                    reject(error);
                }
            );
        });
    });
};

export const addUser = (username, password, email, callback) => {
    db.transaction((tx) => {
        try {
            const joinedDate = new Date().toISOString().split('T')[0];

            tx.executeSql(
                "SELECT * FROM Users WHERE username = ? OR email = ?",
                [username, email],
                (_, { rows }) => {
                    if (rows.length > 0) {
                        callback({ success: false, error: 'User with the same credentials already exists, use Login' });
                    } else {
                        tx.executeSql(
                            "INSERT INTO Users (username, password, email, joinedDate) VALUES (?,?,?,?)",
                            [username, password, email, joinedDate],
                            (_, { insertId }) => {
                                const user = {
                                    username,
                                    password,
                                    email,
                                    joinedDate
                                };
                                callback({ success: true, insertId, user });
                            },
                            (tx, e) => {
                                console.log('Error while inserting user: ', e);
                                callback({ success: false, error: 'Error inserting user' });
                            }
                        );
                    }
                },
                (tx, e) => {
                    console.log('Error checking existing users: ', e);
                    callback({ success: false, error: 'Error checking existing users' });
                }
            );
        } catch (error) {
            console.log('Error adding the user: ', error);
            callback({ success: false, error: 'Error adding the user' });
        }
    });
};

export const updatePassword = (username,newPassword) => {
    db.transaction((tx) => {
        try {
            tx.executeSql('UPDATE Users SET password = ? WHERE username = ?',[newPassword,username],()=>{
                console.log("Password Changed")
            },(tx,e)=>{
                console.log(e)
            })
        } catch {
            console.log("Error updating password")
        }
    })
}

export const getPassword = (username,callback) => {
    db.transaction((tx) => {
        try {
            tx.executeSql("Select * from Users WHERE username = ?",[username],
            (_,{ rows }) => {
                if (rows.length === 0) 
                {
                    callback({ success: false, error: 'Invalid Password' }); 
                } else {
                    const user = rows.item(0);
                    callback({success: true, password: user.password})
                }
            },(tx,e)=>{
                console.log(e)
            })
        } catch {
            console.log("Error updating password")
        }
    })
}

export const getUsername = (username, callback) => {
    db.transaction((tx) => {
      try {
        tx.executeSql(
          "SELECT * FROM Users WHERE username = ?",
          [username],
          (_, { rows }) => {
            if (rows.length === 0) {
              callback({ success: false, error: 'Invalid Username' });
            } else {
              const user = rows.item(0);
              callback({ success: true, username: user.username });
            }
          },
          (tx, e) => {
            console.log(e);
            callback({ success: false, error: 'Error fetching username' });
          }
        );
      } catch (error) {
        console.log('Error fetching username', error);
        callback({ success: false, error: 'Error fetching username' });
      }
    });
  };  

  export const verifyUser = (username, password, callback) => {
    db.transaction((tx) => {
      try {
        tx.executeSql(
          "Select * from Users WHERE username = ?",
          [username],
          (_, { rows }) => {
            if (rows.length === 0) {
              callback({ success: false, error: 'Invalid Username', user: null });
            } else {
              const user = rows.item(0);
              if (user.password === password) {
                callback({ success: true, user });
              } else {
                callback({ success: false, error: 'Invalid Password', user: null });
              }
            }
          },
          (_, e) => {
            console.log(e);
            callback({ success: false, error: 'Error verifying the user', user: null });
          }
        );
      } catch {
        console.log("Error verifying the user");
        callback({ success: false, error: 'Error verifying the user', user: null });
      }
    });
  };

I have tried calling userData in the loginUser method in App.js however, it still is not saving the user logged in after reload. I am expecting even after reloading the application, the user which was registered to be kept logged in instead of having to enter their credentials over and over again.


Solution

  • You have to revise your app navigation architecture.

    Typical app has both PUBLIC screens (Onboarding/Welcome, Login,Signup,Forget Password, Policies,...).

    Other screens which require user to be authenticated should be considered as PRIVATE.

    You can render PUBLIC/PRIVATE screens based on user authentication status as recommended by React Navigation - https://reactnavigation.org/docs/auth-flow

    A simplified version of app architecture:

    import React, { createContext, useState, useEffect } from "react";
    import { StyleSheet, LoadingIndicator } from "react-native";
    import BarCodeScan from "./screens/BScanner";
    import Favorites from "./screens/Favorites";
    import ProductInfo from "./screens/ProductInfo";
    import BottomTabs from "./components/BottomTabs";
    import { NavigationContainer } from "@react-navigation/native";
    import { createStackNavigator } from "@react-navigation/stack";
    import TermsAndService from "./screens/terms_and_service";
    import { ListProvider } from "./screens/ListContext";
    import AboutUs from "./screens/About Us";
    import ProductDetailScreen from "./screens/ProductDetail";
    import WikiScreen from "./screens/WikiScreen";
    import Nutrition from "./screens/Nutrition";
    import Login from "./components/Login";
    import { initializeDB } from "./DBA/DBAdapter";
    import ChangePassword from "./components/ChangePassword";
    import Settings from "./screens/Settings";
    import UserProfile from "./screens/UserProfile";
    import AsyncStorage from "@react-native-async-storage/async-storage";
    
    const Stack = createStackNavigator();
    
    export const GlobalContext = createContext();
    
    export default function App() {
      const [user, setUser] = useState(null);
      const [authenticating, setAuthenticating] = useState(true);
    
      const loginUser = (username) => {
        //console.log('Logging in user:', username);
        const user = { id: 1, name: username };
        setUser(user);
      };
    
      const logoutUser = () => {
        setUser(null);
      };
    
      useEffect(() => {
        initializeDB();
    
        const loadUserData = async () => {
          try {
            const userData = await AsyncStorage.getItem("userData");
            setAuthenticating(false);
            if (userData) {
              const parsedUserData = JSON.parse(userData);
              loginUser(parsedUserData.username);
            } else {
              console.log("No user data found in AsyncStorage.");
            }
          } catch (error) {
            console.error("Error loading user data from AsyncStorage:", error);
    
            setAuthenticating(false);
          }
        };
    
        loadUserData();
      }, []);
    
      const isAuthenticated = user !== null;
    
      if (authenticating) {
        return <LoadingIndicator size="large" />;
      }
    
      return (
        <GlobalContext.Provider value={{ loginUser, logoutUser }}>
          <ListProvider>
            <NavigationContainer>
              <Stack.Navigator initialRouteName="Login">
                {isAuthenticated ? (
                  <>
                    <Stack.Screen
                      name="BottomTabs"
                      options={{ headerShown: false, title: "Back" }}
                      component={({ navigation }) => (
                        <BottomTabs screenProps={{ navigation }} />
                      )}
                    />
                    <Stack.Screen
                      name="ProductInfo"
                      component={ProductInfo}
                      options={{ title: "Product Details" }}
                    />
                    <Stack.Screen
                      name="WikiScreen"
                      component={WikiScreen}
                      options={{ title: "Wikipedia" }}
                    />
                    <Stack.Screen name="BarCodeScan" component={BarCodeScan} />
                    <Stack.Screen
                      name="Nutrition"
                      component={Nutrition}
                      options={{ title: "Nutritional Facts" }}
                    />
                    <Stack.Screen
                      name="Settings"
                      component={Settings}
                      options={{ title: "Settings" }}
                    />
                    <Stack.Screen
                      name="UserProfile"
                      component={UserProfile}
                      options={{ title: "User Profile" }}
                    />
                  </>
                ) : (
                  <>
                    {" "}
                    <Stack.Screen
                      name="Login"
                      component={Login}
                      options={{ headerShown: false }}
                    />
                    <Stack.Screen
                      name="ProductDetails"
                      component={ProductDetailScreen}
                      options={{ title: "Product Details" }}
                    />
                    <Stack.Screen
                      name="ChangePassword"
                      component={ChangePassword}
                      options={{ headerShown: false }}
                    />
                    <Stack.Screen
                      name="TermsAndService"
                      component={TermsAndService}
                    />
                    <Stack.Screen name="About Us" component={AboutUs} />
                    <Stack.Screen name="Favorites" component={Favorites} />{" "}
                  </>
                )}
              </Stack.Navigator>
            </NavigationContainer>
          </ListProvider>
        </GlobalContext.Provider>
      );
    }
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        backgroundColor: "#fff",
        alignItems: "center",
        justifyContent: "center",
      },
    });