Skip to content

Commit

Permalink
Add Authentication, local storage, auto login and auto logout
Browse files Browse the repository at this point in the history
    - Auto logout is not very fancy, and should be refactored in the future
    facebook/react-native#12981

* 4_Shop_App/App.js

    - Imported a new component a Wrapper just to have access to our store

* 4_Shop_App/navigation/ShopNavigator.js

    - Create this new component to check first (after the splash screen) if there is a logged user. if not redirect  to Auth screen
    - We need to create this component to have access to our store
    - With the help of useRef hook, we can have access to the properties of the component

* 4_Shop_App/navigation/ShopNavigator.js

    - Added a react component to the side drawer, so we can click to logout
    - Added our StartupScreen as the first screen of the stack

* 4_Shop_App/store/actions/auth.js

    - Changed SIGNUP and LOGIN  to AUTHENTICATE
    - Added a helper function authenticate() to dispatch the (userId, token, expiryTime)
    - Added a logout(), inside this function we call AsyncStorage.removeItem('') to remove the item from our local storage
    - Added a auto logout, it checks for the expiration date, and creates a setTimeout with the remaining time
    - Added a saveDataToStorage() to save the user info in the local storage

* 4_Shop_App/store/reducers/auth.js

    - Removed the cases LOGIN and SIGNUP
    - Added LOGOUT -> basically sets to the initial state

* 4_Shop_App/screens/StartupScreen.js

    - This is the first page of the app
    - It's very fast, and we wont see it (it's just an ActivityIndicator)
    - Basically it checks if there is a user in the local storage
        - if yes, checks the expiration date
            - if everything is ok, redirects to "Shop" screen
        - if no, or expiration date is expired
            - redirects to "Auth" screen
  • Loading branch information
Roger-Takeshita committed Jun 12, 2020
1 parent 5c54505 commit 7b7baab
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 38 deletions.
15 changes: 7 additions & 8 deletions 4_Shop_App/App.js
@@ -1,15 +1,14 @@
import React, { useState } from 'react';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { AppLoading } from 'expo';
import * as Font from 'expo-font';
import React, { useState } from 'react';
import { Provider } from 'react-redux';
import { applyMiddleware, combineReducers, createStore } from 'redux';
import ReduxThunk from 'redux-thunk';

import productsReducer from './store/reducers/products';
import NavigationContainer from './navigation/NavigationContainer';
import authReducer from './store/reducers/auth';
import cartReducer from './store/reducers/cart';
import ordersReducer from './store/reducers/orders';
import authReducer from './store/reducers/auth';
import ShopNavigator from './navigation/ShopNavigator';
import productsReducer from './store/reducers/products';

const rootReducer = combineReducers({
products: productsReducer,
Expand Down Expand Up @@ -43,7 +42,7 @@ export default function App() {

return (
<Provider store={store}>
<ShopNavigator />
<NavigationContainer />
</Provider>
);
}
67 changes: 67 additions & 0 deletions 4_Shop_App/README.md
Expand Up @@ -22,6 +22,7 @@
- [When The App Launches](#whenapplaunches)
- [Authentication Screen](#authscreen)
- [Auth Config](#actionauth)
- [Local Storage - AsyncStorage](#localstorage)

<h1 id='shopapp'>Shop App</h1>

Expand Down Expand Up @@ -905,3 +906,69 @@

export default authReducer;
```

<h1 id='localstorage'>Local Storage - AsyncStorage</h1>

[Go Back to Summary](#summary)

- In `store/actions/auth.js`

- Import **AsyncStorage** from `react-native`
- `removeItem('key')` to remove key/value from local storage
- `setItem('key', 'value')` to add key/value pair in the local storage
- It has to be a string, so we have to JSON.stringify()
- `getItem('key')` to get an item from local storage

```JavaScript
import { AsyncStorage } from 'react-native';
import { FIREBASE_KEY } from 'react-native-dotenv';
export const AUTHENTICATE = 'AUTHENTICATE';
export const LOGOUT = 'LOGOUT';
let timer;

export const authenticate = (userId, token, expiryTime) => {
return (dispatch) => {
dispatch(setLogoutTimer(expiryTime));
dispatch({ type: AUTHENTICATE, userId, token });
};
};

export const signup = (email, password) => {
return async (dispatch) => {...};
};

export const login = (email, password) => {
return async (dispatch) => {...};
};

export const logout = () => {
clearLogoutTimer();
AsyncStorage.removeItem('userData');
return { type: LOGOUT };
};

const clearLogoutTimer = () => {
if (timer) {
clearTimeout(timer);
}
};

const setLogoutTimer = (expirationTime) => {
return (dispatch) => {
timer = setTimeout(() => {
dispatch(logout());
}, expirationTime);
};
};

const saveDataToStorage = (token, userId, expirationDate) => {
AsyncStorage.setItem(
'userData',
JSON.stringify({
token,
userId,
expiryDate: expirationDate.toISOString(),
}),
);
};
```
19 changes: 19 additions & 0 deletions 4_Shop_App/navigation/NavigationContainer.js
@@ -0,0 +1,19 @@
import React, { useEffect, useRef } from 'react';
import { NavigationActions } from 'react-navigation';
import { useSelector } from 'react-redux';
import ShopNavigator from './ShopNavigator';

function NavigationContainer(props) {
const navRef = useRef();
const isAuth = useSelector((state) => !!state.auth.token);

useEffect(() => {
if (!isAuth) {
navRef.current.dispatch(NavigationActions.navigate({ routeName: 'Auth' }));
}
}, [isAuth]);

return <ShopNavigator ref={navRef} />;
}

export default NavigationContainer;
47 changes: 37 additions & 10 deletions 4_Shop_App/navigation/ShopNavigator.js
@@ -1,19 +1,20 @@
import { Ionicons } from '@expo/vector-icons';
import React from 'react';
import { createStackNavigator } from 'react-navigation-stack';
import { Button, Platform, SafeAreaView, StyleSheet, View } from 'react-native';
import { createAppContainer, createSwitchNavigator } from 'react-navigation';
import { createDrawerNavigator } from 'react-navigation-drawer';
import { Platform } from 'react-native';
import { Ionicons } from '@expo/vector-icons';

import { createDrawerNavigator, DrawerNavigatorItems } from 'react-navigation-drawer';
import { createStackNavigator } from 'react-navigation-stack';
import { useDispatch } from 'react-redux';
import Colors from '../css/Colors';

import ProductsOverviewScreen from '../screens/shop/ProductsOverviewScreen';
import ProductDetailsScreen from '../screens/shop/ProductDetailsScreen';
import CartScreen from '../screens/shop/CartScreen';
import OrdersScreen from '../screens/shop/OrdersScreen';
import UserProductsScreen from '../screens/user/UserProductsScreen';
import EditProductScreen from '../screens/user/EditProductScreen';
import ProductDetailsScreen from '../screens/shop/ProductDetailsScreen';
import ProductsOverviewScreen from '../screens/shop/ProductsOverviewScreen';
import StartupScreen from '../screens/StartupScreen';
import AuthScreen from '../screens/user/AuthScreen';
import EditProductScreen from '../screens/user/EditProductScreen';
import UserProductsScreen from '../screens/user/UserProductsScreen';
import * as authActions from '../store/actions/auth';

const defaultNavOptions = {
headerStyle: {
Expand Down Expand Up @@ -104,12 +105,38 @@ const ShopNavigator = createDrawerNavigator(
contentOptions: {
activeTintColor: Colors.primary,
},
contentComponent: (props) => {
const dispatch = useDispatch();
return (
<View style={styles.drawerButton}>
<SafeAreaView forceInset={{ top: 'always', horizontal: 'never' }}>
<DrawerNavigatorItems {...props} />
<Button
title="Logout"
color={Colors.primary}
onPress={() => {
dispatch(authActions.logout());
// props.navigation.navigate('Auth');
}}
/>
</SafeAreaView>
</View>
);
},
},
);

const MainNavigator = createSwitchNavigator({
Startup: StartupScreen,
Auth: AuthNavigator,
Shop: ShopNavigator,
});

const styles = StyleSheet.create({
drawerButton: {
flex: 1,
paddingTop: 20,
},
});

export default createAppContainer(MainNavigator);
52 changes: 52 additions & 0 deletions 4_Shop_App/screens/StartupScreen.js
@@ -0,0 +1,52 @@
import React, { useEffect } from 'react';
import { ActivityIndicator, AsyncStorage, StyleSheet, View } from 'react-native';
import { useDispatch } from 'react-redux';
import Colors from '../css/Colors';
import * as authActions from '../store/actions/auth';

function StartupScreen({ navigation }) {
const dispatch = useDispatch();

useEffect(() => {
const tryLogin = async () => {
const userData = await AsyncStorage.getItem('userData');

if (!userData) {
navigation.navigate('Auth');
return;
}

const transformedData = JSON.parse(userData);
const { token, userId, expiryDate } = transformedData;
const expirationDate = new Date(expiryDate);

if (expirationDate <= new Date() || !token || !userId) {
navigation.navigate('Auth');
return;
}

const expirationTime = expirationDate.getTime() - new Date().getTime();

navigation.navigate('Shop');
dispatch(authActions.authenticate(userId, token, expirationTime));
};

tryLogin();
}, [dispatch]);

return (
<View style={styles.screen}>
<ActivityIndicator size="large" color={Colors.primary} />
</View>
);
}

const styles = StyleSheet.create({
screen: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});

export default StartupScreen;
61 changes: 48 additions & 13 deletions 4_Shop_App/store/actions/auth.js
@@ -1,6 +1,15 @@
import { AsyncStorage } from 'react-native';
import { FIREBASE_KEY } from 'react-native-dotenv';
export const SIGNUP = 'SIGNUP';
export const LOGIN = 'LOGIN';
export const AUTHENTICATE = 'AUTHENTICATE';
export const LOGOUT = 'LOGOUT';
let timer;

export const authenticate = (userId, token, expiryTime) => {
return (dispatch) => {
dispatch(setLogoutTimer(expiryTime));
dispatch({ type: AUTHENTICATE, userId, token });
};
};

export const signup = (email, password) => {
return async (dispatch) => {
Expand Down Expand Up @@ -40,12 +49,9 @@ export const signup = (email, password) => {
}

const resData = await response.json();

dispatch({
type: SIGNUP,
token: resData.idToken,
userId: resData.localId,
});
dispatch(authenticate(resData.localId, resData.idToken, parseInt(resData.expiresIn) * 1000));
const expirationDate = new Date(new Date().getTime() + parseInt(resData.expiresIn) * 1000);
saveDataToStorage(resData.idToken, resData.localId, expirationDate);
} catch (error) {
throw error;
}
Expand Down Expand Up @@ -93,13 +99,42 @@ export const login = (email, password) => {
}

const resData = await response.json();
dispatch({
type: LOGIN,
token: resData.idToken,
userId: resData.localId,
});
dispatch(authenticate(resData.localId, resData.idToken, parseInt(resData.expiresIn) * 1000));
const expirationDate = new Date(new Date().getTime() + parseInt(resData.expiresIn) * 1000);
saveDataToStorage(resData.idToken, resData.localId, expirationDate);
} catch (error) {
throw error;
}
};
};

export const logout = () => {
clearLogoutTimer();
AsyncStorage.removeItem('userData');
return { type: LOGOUT };
};

const clearLogoutTimer = () => {
if (timer) {
clearTimeout(timer);
}
};

const setLogoutTimer = (expirationTime) => {
return (dispatch) => {
timer = setTimeout(() => {
dispatch(logout());
}, expirationTime);
};
};

const saveDataToStorage = (token, userId, expirationDate) => {
AsyncStorage.setItem(
'userData',
JSON.stringify({
token,
userId,
expiryDate: expirationDate.toISOString(),
}),
);
};
11 changes: 4 additions & 7 deletions 4_Shop_App/store/reducers/auth.js
@@ -1,4 +1,4 @@
import { LOGIN, SIGNUP } from '../actions/auth';
import { AUTHENTICATE, LOGOUT } from '../actions/auth';

const initialState = {
token: null,
Expand All @@ -7,16 +7,13 @@ const initialState = {

const authReducer = (state = initialState, action) => {
switch (action.type) {
case LOGIN:
return {
token: action.token,
userId: action.userId,
};
case SIGNUP:
case AUTHENTICATE:
return {
token: action.token,
userId: action.userId,
};
case LOGOUT:
return initialState;
default:
return state;
}
Expand Down

0 comments on commit 7b7baab

Please sign in to comment.