Authentication in React Native with AsyncStorage - reactjs

In my project, when uid is saved in AsyncStorage, it means the user has already logged in.
Current code
Index.js
import React from 'react';
import Auth from 'app/src/common/Auth';
import { Text } from 'react-native';
export default class Index extends React.Component {
constructor(props) {
super(props);
this.state = {
auth: true,
};
}
async componentDidMount() {
await this.setState({ auth: await Auth.user() });
}
render() {
const { auth } = this.state;
return (
{!auth ? (
<Text>You need to Log In!</Text>
) : (
<Text>You are Logged In!</Text>
)}
)
}
}
Auth.js
import { AsyncStorage } from 'react-native';
export default {
async user() {
let result = true;
const response = await AsyncStorage.getItem('uid');
if (!response) {
result = false;
}
return result;
},
};
This code is working but I would like to make this more simple like one function.
I would appreciate it if you could give me any advice.

You can use promise and Do all Job in Index.js like
AsyncStorage.getItem('uid').then((uid) => {
this.setState({
auth : (uid) ? true : false
})
})
or simply use
const uid = await AsyncStorage.getItem('uid');
this.setState({ auth : (uid) ? true : false });

Are you going to be using that in more than one spot? why not just do it in your component?
async componentDidMount() {
const auth = await AsyncStorage.getItem('uid');
this.setState({ auth });
}

Related

LoginPage in React with Redirecting

I have response from server about auth status current user. Based on this information I'm rendering HomePage or LoginPage. How do I can redirect all unauthorized users to '/login' url and others to homepage. When I`m using history.push('/login') it saves permament in url, because in first rendering auth status is always false
import React from 'react';
import './App.scss';
import MainPage from './components/MainPage/MainPage';
import { withRouter, Route } from 'react-router-dom';
import { getAuthStatusThunk } from './redux/authReducer';
import { compose } from 'redux';
import { connect } from 'react-redux';
import Preloader from './components/commons/Preloader/Preloader';
class App extends React.Component {
constructor() {
super()
this.state = { isCheckingLogin: false }
this.getAuthStatusThunk = this.props.getAuthStatusThunk.bind(this)
}
componentDidMount() {
getAuthStatusThunk()
}
async getAuthStatusThunk() {
this.setState({ isCheckingLogin: true })
let res = await (checkIsLoggedIn())
if (res.isLoggedIn) {
this.setState({ isCheckingLogin: false })
}
else {
this.props.history.push('/login')
this.setState({ isCheckingLogin: false })
}
}
render() {
return (
<React.Fragment>
{this.isCheckingLogin ? <Preloader isLoading={true} /> : <MainPage />}
</React.Fragment>
)
}
}
let mapStateToProps = (state) => ({
isAuth: state.authPage.isAuth,
successStatus: state.authPage.successStatus
})
let FunctionRender = compose(
withRouter,
connect(mapStateToProps, { getAuthStatusThunk }))(App)
export default FunctionRender;
You can keep loading and error value also in the state.
Then check if any one of the state value is set or not.
Based on the state value navigate the user accordingly.
You can set a state isCheckingLogin to true while you call the getAuthStatusThunk. Here I include a sample snippet for implementing it.
class App extends React.Component {
constructor() {
super()
this.state = { isCheckingLogin: false }
this.getAuthStatusThunk = this.getAuthStatusThunk.bind(this)
}
componentDidMount() {
getAuthStatusThunk()
}
async getAuthStatusThunk() {
this.setState({ isCheckingLogin: true })
let res = await (checkIsLoggedIn())
if (res.isLoggedIn) {
this.setState({ isCheckingLogin: false })
}
else {
this.props.history.push('/login')
this.setState({ isCheckingLogin: false })
}
}
render() {
return (
<React.Fragment>
{isCheckingLogin ? <Loader /> : <MainPage />}
</React.Fragment>
)
}
}
The Loader component can be used for showing a spinner or some sort of animation while login status is checked if you want.

When and where to check on a Firebase user in React

I am trying to figure out how to use Firebase.
I have a config with an auth listener:
onAuthUserListener(next, fallback) {
// onUserDataListener(next, fallback) {
return this.auth.onAuthStateChanged(authUser => {
if (!authUser) {
// user not logged in, call fallback handler
fallback();
return;
}
this.user(authUser.uid).get()
.then(snapshot => {
let snapshotData = snapshot.data();
let userData = {
...snapshotData, // snapshotData first so it doesn't override information from authUser object
uid: authUser.uid,
email: authUser.email,
emailVerified: authUser.emailVerifed,
providerData: authUser.providerData
};
setTimeout(() => next(userData), 0); // escapes this Promise's error handler
})
.catch(err => {
// TODO: Handle error?
console.error('An error occured -> ', err.code ? err.code + ': ' + err.message : (err.message || err));
setTimeout(fallback, 0); // escapes this Promise's error handler
});
});
}
// ... other methods ...
// }
I have read the documentation about creating a listener to see if there is an authUser and have got this authentication listener plugged in.
import React from 'react';
import { AuthUserContext } from '../Session/Index';
import { withFirebase } from '../Firebase/Index';
const withAuthentication = Component => {
class WithAuthentication extends React.Component {
constructor(props) {
super(props);
this.state = {
authUser: null,
};
}
componentDidMount() {
this.listener = this.props.firebase.auth.onAuthStateChanged(
authUser => {
authUser
? this.setState({ authUser })
: this.setState({ authUser: null });
},
);
}
componentWillUnmount() {
this.listener();
};
render() {
return (
<AuthUserContext.Provider value={this.state.authUser}>
<Component {...this.props} />
</AuthUserContext.Provider>
);
}
}
return withFirebase(WithAuthentication);
};
export default withAuthentication;
Then in the consumer component I have:
import React from 'react';
import {
BrowserRouter as Router,
Route,
Link,
Switch,
useRouteMatch,
} from 'react-router-dom';
import * as ROUTES from '../../constants/Routes';
import { compose } from 'recompose';
import { Divider, Layout, Card, Tabs, Typography, Menu, Breadcrumb, Icon } from 'antd';
import { withFirebase } from '../Firebase/Index';
import { AuthUserContext, withAuthorization, withEmailVerification } from '../Session/Index';
const { Title, Text } = Typography
const { TabPane } = Tabs;
const { Header, Content, Footer, Sider } = Layout;
const { SubMenu } = Menu;
class Dashboard extends React.Component {
state = {
collapsed: false,
loading: false,
};
onCollapse = collapsed => {
console.log(collapsed);
this.setState({ collapsed });
};
render() {
return (
<AuthUserContext.Consumer>
{ authUser => (
<div>
<Text style={{ float: 'right', color: "#fff"}}>
{/*
{
this.props.firebase.db.collection('users').doc(authUser.uid).get()
.then(doc => {
console.log( doc.data().name
)
})
}
*/}
</div>
)}
</AuthUserContext.Consumer>
);
}
}
export default withFirebase(Dashboard);
It works fine the first time the page is loaded.
However, on a page refresh, the system is slower than the code and returns null error messages that say:
TypeError: Cannot read property 'uid' of null (anonymous function)
I have seen this article which proposes solutions for Angular.
I can't find a way to implement this so that it works in react.
The article suggests:
firebase.auth().onAuthStateChanged( user =>; {
if (user) { this.userId = user.uid }
});
So, in my listener I tried putting if in front of authUser - but that doesn't seem to be an approach that works.
Any advice on what to try next to make a listener that lets firebase load the user before it runs the check?
Try react-with-firebase-auth this library.
This library makes a withFirebaseAuth() function available to you.
import * as React from 'react';
import * as firebase from 'firebase/app';
import 'firebase/auth';
import withFirebaseAuth, { WrappedComponentProps } from 'react-with-firebase-auth';
import firebaseConfig from './firebaseConfig';
const firebaseApp = firebase.initializeApp(firebaseConfig);
const App = ({
/** These props are provided by withFirebaseAuth HOC */
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signInWithGoogle,
signInWithFacebook,
signInWithGithub,
signInWithTwitter,
signInAnonymously,
signOut,
setError,
user,
error,
loading,
}: WrappedComponentProps) => (
<React.Fragment>
{
user
? <h1>Hello, {user.displayName}</h1>
: <h1>Log in</h1>
}
{
user
? <button onClick={signOut}>Sign out</button>
: <button onClick={signInWithGoogle}>Sign in with Google</button>
}
{
loading && <h2>Loading..</h2>
}
</React.Fragment>
);
const firebaseAppAuth = firebaseApp.auth();
/** See the signature above to find out the available providers */
const providers = {
googleProvider: new firebase.auth.GoogleAuthProvider(),
};
/** providers can be customised as per the Firebase documentation on auth providers **/
providers.googleProvider.setCustomParameters({hd:"mycompany.com"});
/** Wrap it */
export default withFirebaseAuth({
providers,
firebaseAppAuth,
})(App);

Firebase Auth, Logged out on page refresh

Technologies: I'm using Firebase Auth with NextJS & React.
Problematic: Logged users can use the web app with firebase auth normally only if they navigate within the app via Next routing, whenever they refresh the page or open a new account tab they are not logged in anymore.
Issue: It is extremely frustrating because this problem only occurs on production. There's no problem at all on the staging & localhost environment.
firebase.js: Initialize firebase.
import getConfig from "next/config";
import * as firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/analytics';
const { publicRuntimeConfig } = getConfig();
export async function initializeFirebase() {
if (!firebase.apps.length) {
firebase.initializeApp(JSON.parse(publicRuntimeConfig.FIREBASE_CONFIG));
if (publicRuntimeConfig.FIREBASE_ANALYTICS) {
firebase.analytics();
}
}
}
export const auth = firebase.auth
export const db = firebase.firestore;
export default firebase;
AuthHoC.js: To make sure the user is connected I wrapper my pages with a HOC.
export default App => (
class AuthHoC extends App {
_isMounted = false;
constructor(props) {
super(props)
this.state = {
loading: false,
isVerified: false,
idToken: undefined,
isAuthenticated: false
}
}
async componentDidMount() {
this._isMounted = true;
await initializeFirebase();
// onAuthStateChanged return a function that we'll use to unsubscribe our listener
this.unsubscribeMethod = await auth().onAuthStateChanged(this._handleStateChange);
}
// is user is null, we're no longer authenticated
_handleStateChange = (user) => {
let that = this;
if (user) {
// NOT PASSING HERE ON PAGE REFRESH...
user.getIdToken().then(function(idToken) {
that.setState({
loading: true,
idToken: idToken,
isVerified: user.emailVerified,
isAuthenticated: !!user
});
});
} else {
...
}
}
componentWillUnmount() {
if (this.unsubscribeMethod) {
this.unsubscribeMethod();
}
this._isMounted = false;
}
render() {
return ( <>
{this.state.loading ?
<App {...this.props} {...this.state} />
:
... loading ...
}
</> )
}
});
_app.js: (NextJS) Wrap every pages with the Higher Order Component.
import App from "next/app";
import AuthHoC from '../utils/authentication/authHoC';
class MyApp extends App {
render() {
const { Component, pageProps, isAuthenticated, idToken, isVerified } = this.props;
return (
<Component
{...pageProps}
isAuth={isAuthenticated}
idToken={idToken}
isVerified={isVerified}
/>
);
}
}
export default AuthHoC(MyApp);
What could be the issue? All these codes work on localhost & staging url, just not on production.
EDIT:
I pinpointed the problem, I just switched my production keys with staging and it works, that means that the problem is not coming from the Heroku or my code but my Firebase configuration itself.
The main difference is that the prod use analytics. If you have any info I forgot to configure any suggestion would help.

App not re-rendering on history.push when run with jest

I'm trying to test my LoginForm component using jest and react-testing-library. When the login form is submitted successfully, my handleLoginSuccess function is supposed to set the 'user' item on localStorage and navigate the user back to the home page using history.push(). This works in my browser in the dev environment, but when I render the component using Jest and mock out the API, localStorage gets updated but the navigation to '/' doesn't happen.
I've tried setting localStorage before calling history.push(). I'm not sure what is responsible for re-rendering in this case, and why it works in dev but not test.
Login.test.jsx
import 'babel-polyfill'
import React from 'react'
import {withRouter} from 'react-router'
import {Router} from 'react-router-dom'
import {createMemoryHistory} from 'history'
import {render, fireEvent} from '#testing-library/react'
import Login from '../../pages/Login'
import API from '../../util/api'
jest.mock('../../util/api')
function renderWithRouter(
ui,
{route = '/', history = createMemoryHistory({initialEntries: [route]})} = {},
) {
return {
...render(<Router history={history}>{ui}</Router>),
// adding `history` to the returned utilities to allow us
// to reference it in our tests (just try to avoid using
// this to test implementation details).
history,
}
}
describe('When a user submits the login button', () => {
test('it allows the user to login', async () => {
const fakeUserResponse = {'status': 200, 'data': { 'user': 'Leo' } }
API.mockImplementation(() => {
return {
post: () => {
return Promise.resolve(fakeUserResponse)
}
}
})
const route = '/arbitrary-route'
const {getByLabelText, getByText, findByText} = renderWithRouter(<Login />, {route})
fireEvent.change(getByLabelText(/email/i), {target: {value: 'email#gmail.com '}})
fireEvent.change(getByLabelText(/password/i), {target: {value: 'Foobar123'}})
fireEvent.click(getByText(/Log in/i))
const logout = await findByText(/Log Out/i)
expect(JSON.parse(window.localStorage.getItem('vector-user'))).toEqual(fakeUserResponse.data.user)
})
})
relevant parts of LoginForm.jsx
class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
disableActions: false,
formErrors: null,
};
}
handleLoginSuccess = () => {
const { loginSuccessCallback, redirectOnLogin, history } = { ...this.props };
if (loginSuccessCallback) {
loginSuccessCallback();
} else {
history.push('/');
}
}
loginUser = ({ user }) => {
localStorage.setItem('vector-user', JSON.stringify(user));
}
handleLoginResponse = (response) => {
if (response.status !== 200) {
this.handleResponseErrors(response.errors);
} else {
this.loginUser(response.data);
this.handleLoginSuccess();
}
}
handleLoginSubmit = (event) => {
event.preventDefault();
const {
disableActions, email, password
} = { ...this.state };
if (disableActions === true) {
return false;
}
const validator = new Validator();
if (!validator.validateForm(event.target)) {
this.handleResponseErrors(validator.errors);
return false;
}
this.setState(prevState => ({ ...prevState, disableActions: true }));
new API().post('login', { email, password }).then(this.handleLoginResponse);
return true;
}
}
Login.jsx
import React from 'react';
import { withRouter, Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import LoginForm from '../components/LoginForm';
class Login extends React.Component {
constructor({ location }) {
super();
const originalRequest = location.state && location.state.originalRequest;
this.state = {
originalRequest
};
}
render() {
const { originalRequest } = { ...this.state };
return (
<div>
<h1>Login</h1>
<LoginForm redirectOnLogin={originalRequest && originalRequest.pathname} />
<Link to="/forgot">Forgot your password?</Link>
</div>
);
}
}
Login.propTypes = {
location: PropTypes.shape({
state: PropTypes.shape({
originalRequest: PropTypes.shape({
pathname: PropTypes.string
})
})
})
};
export default withRouter(Login);
Currently the await findByText() times out.
I think that's because in your tests you're not rendering any Route components. Without those react-router has no way to know what to render when the route changes. It will always render Login.

Change data on navbar when logout and login with another account

Aim :
I want to put firstName and lastName on my Navbar. So, I'm using axios request by id with userId
EDIT: Thanks to #Isaac, I have no more infinite loop when I'm using componentWillUpdate() now.
Problem : Data doesn't change (firstName and lastName) when I'm logout and login with another account
No problems from servers.
here a picture :
Description : I've login as a & g (firstName and lastName), then I've logout and login as j & j.
navbar.js:
import React, { Component } from 'react';
import { fade } from '#material-ui/core/styles/colorManipulator';
import { withStyles } from '#material-ui/core/styles';
import { connect } from 'react-redux';
import AuthA from '../store/actions/AuthA';
import { withRouter } from 'react-router-dom';
import '../Navbar.css';
import NavbarV from './NavbarV';
import PropTypes from 'prop-types';
import axios from 'axios';
class NavbarC extends Component {
constructor(props){
super(props);
this.state = {
client:[]
}
}
componentWillMount(){
this.getUser();
}
getUser(){
axios.get (`http://localhost:3002/api/clients/${localStorage.getItem("userId")}?access_token=${localStorage.getItem("token")}`)
.then(res => {
this.setState({client: res.data}, () => {
console.log(this.state)
})
})
}
shouldComponentUpdate(nextState){
return (this.state.client.firstName !== nextState.firstName ||
this.state.client.lastName !== nextState.lastName);
}
componentWillUpdate(){
this.getUser();
console.log(this.state)
}
logout = () => {
this.props.authfn.logout();
};
render() {
return(
<NavbarV logout = {this.logout}
firstName={this.state.client.firstName}
lastName={this.state.client.lastName}
userId={this.props.userId}
auth = {this.props.auth}
classes={this.props.classes}/>
)
}
}
NavbarC.propTypes = {
auth: PropTypes.bool.isRequired,
firstName: PropTypes.string.isRequired,
lastName: PropTypes.string.isRequired
};
const mapStateToProps = (state) => {
return {
auth: state.AuthR.auth,
firstName: state.AuthR.firstName,
lastName: state.AuthR.lastName,
userId: state.AuthR.userId
};
};
const mapDispatchToProps = dispatch => {
return {
authfn: AuthA(dispatch)
}
};
export default connect(mapStateToProps, mapDispatchToProps) (withStyles(styles)(withRouter(NavbarC)));
If someone have a solution or any questions, I'm here :)
thank you all in advance
First of all, you should avoid componentWillUpdate lifecycle as it's been deprecated.
And for your case, this.getUser(); will be triggered to pull data which then trigger this.setState({client: res.data}). When the app executing this.setState(), your component will be re-render so there's no need to have any other componentLifeCycle.
class NavbarC extends Component {
state = { client:[], userID: null, token: null };
componentDidMount(){
this.setState({
userID: localStorage.getItem("userId"),
token: localStorage.getItem("token")
}, () => {
this.getUser();
})
}
getUser(){
axios.get (`http://localhost:3002/api/clients/${this.state.userID}?access_token=${this.state.token}`)
.then(res => {
this.setState({ client: res.data }, () => {
console.log(this.state)
})
})
}
componentDidUpdate(prevProps, prevState){
if(prevState.userID !== this.state.userID) {
this.getUser();
}
}
logout = () => this.props.authfn.logout();
render() {
return(
<NavbarV
logout = {this.logout}
firstName={this.state.client.firstName}
lastName={this.state.client.lastName}
userId={this.props.userId}
auth = {this.props.auth}
classes={this.props.classes} />
)}
}
I solve it !
This is a solution :
componentDidMount(){
this.setState({
userId: localStorage.getItem("userId"),
token: localStorage.getItem("token")
}, () => {
this.getUser();
})
}
getUser = () => {
axios.get (`http://localhost:3002/api/clients/${this.state.userId}?access_token=${this.state.token}`)
.then(res => {
this.setState({ client: res.data, userId: localStorage.getItem("userId") }, () => {
console.log(this.state)
})
})
}
componentDidUpdate(prevProps, prevState){
if(prevState.userId !== this.props.userId) {
this.setState({ userId: this.props.userId }, () => {
this.getUser();
})
}
}

Resources