Skip to content

Instantly share code, notes, and snippets.

@silvioramalho
Last active May 30, 2024 12:05
Show Gist options
  • Save silvioramalho/29389b4b3c16b696a5b0a8b3db81e5e7 to your computer and use it in GitHub Desktop.
Save silvioramalho/29389b4b3c16b696a5b0a8b3db81e5e7 to your computer and use it in GitHub Desktop.
Creating React Native Authentication

Creating React Native Authentication

Creating a complete authentication flow using context-api. (Step-by-step)

This flow can be replicated to React-JS (Web)

Creating App

npx react-native init reactNativeAuth --template react-native-template-typescript

Open Ios Emulator

yarn ios

Note: If the Metro Bundler not open automaticly, enter inside directory and run: yarn start

Creating App.tsx

  • delete App.tsx from root project.
  • create a new folder: src
  • create a App.tsx inside src
import React from 'react';
import { View } from 'react-native';

// FC = Function Component
const App: React.FC = () => {
    return <View />;
}

export default App;

Update reference on index.js

...
import App from './src/App';
...

Install dependencies

yarn add @react-navigation/native

Depencies Instructions on: https://reactnavigation.org/docs/getting-started

yarn add react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view

npx pod-install ios

Include on the first line off App.tsx

import 'react-native-gesture-handler';

Wrap the whole app in NavigationContainer. Usually you'd do this in your entry file, such as index.js or App.js:

...
import { NavigationContainer } from '@react-navigation/native';

const App: React.FC = () => {
  return (
    <NavigationContainer>
      <View />
    </NavigationContainer>
  );
};

Run yarn ios again to restart the emulator because components of the native library have been installed.

Install stack navigation

yarn add @react-navigation/stack

Create Flow

Creating folders and files structure

mkdir src/pages
mkdir src/pages/Dashboard
touch src/pages/Dashboard/index.tsx
mkdir src/pages/SignIn
touch src/pages/SignIn/index.tsx
mkdir src/routes
touch src/routes/app.routes.tsx
touch src/routes/auth.routes.tsx

Add content to src/pages/Dashboard/index.tsx

import React from 'react';
import {View} from 'react-native';

const Dashboard: React.FC = () => <View />;

export default Dashboard;

Add content to src/pages/SignIn/index.tsx

import React from 'react';
import {View} from 'react-native';

const SignIn: React.FC = () => <View />;

export default SignIn;

src/routes/app.routes.ts

import React from 'react';
import Dashboard from '../pages/Dashboard/index';

import { createStackNavigator } from '@react-navigation/stack';

const AppStack = createStackNavigator();

const AppRoutes: React.FC = () => (
    <AppStack.Navigator>
        <AppStack.Screen name="Dashboard" component={Dashboard} />
    </AppStack.Navigator>
);

export default AppRoutes;

src/routes/auth.routes.ts

Note: Routes for user authentication.

import React from 'react';
import SignIn from '../pages/SignIn/index';

import { createStackNavigator } from '@react-navigation/stack';

const AuthStack = createStackNavigator();

const AuthRoutes: React.FC = () => (
    <AuthStack.Navigator>
        <AuthStack.Screen name="SignIn" component={SignIn} />
    </AuthStack.Navigator>
);

export default AuthRoutes;

Creating Service

mkdir src/services
touch src/services/auth.tsx
touch src/routes/index.tsx

src/services/auth.tsx

interface Response {
    token: string;
    user: {
        name: string;
        email: string;
    }
}

export function signIn(): Promise<Response> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        token: 'ajsdhflajkshdkfljaldsfhkajsfdklaskdjhlajsdhf',
        user: {
          name: 'Silvio',
          email: '[email protected]',
        },
      });
    }, 2000);
  });
}


src/routes/index.tsx

import React from 'react';

import AuthRoutes from './auth.routes';
import AppRoutes from './app.routes';

const Routes: React.FC = () => {
    return <AuthRoutes />;
}

export default Routes;

Include button into SignIn and style

import React from 'react';
import {View, Button, StyleSheet} from 'react-native';

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

const SignIn: React.FC = () => (
  <View style={styles.container}>
    <Button title="Sign In" onPress={() => {}} />
  </View>
);

export default SignIn;

Adding routes to App.tsx

...
import Routes from './routes/index';


// FC = Function Component
const App: React.FC = () => {
  return (
    <NavigationContainer>
      <Routes />
    </NavigationContainer>
  );
};

...

Adding Service to SignIn

...
import { signIn } from '../../services/auth';
...

const SignIn: React.FC = () => {
  async function handleSignIn() {
    const response = await signIn();
    console.log(response);
  }

  return (
    <View style={styles.container}>
      <Button title="Sign In" onPress={handleSignIn} />
    </View>
  );
};

...

Creating Contexts

mkdir src/contexts
touch src/contexts/auth.tsx

src/contexts/auth.tsx

import { createContext } from 'react';

const AuthContext = createContext({signed: true});

export default AuthContext;

Import context in App.tsx

...
import AuthContext from './contexts/auth';

// FC = Function Component
const App: React.FC = () => {
  return (
    <NavigationContainer>
      <AuthContext.Provider value={{signed: true}}>
        <Routes />
      </AuthContext.Provider>
    </NavigationContainer>
  );
};
...

Adding Context to SignIn

import React, {useContext} from 'react';
import {View, Button, StyleSheet} from 'react-native';

import { signIn } from '../../services/auth';
import AuthContext from '../../contexts/auth';

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

const SignIn: React.FC = () => {
  const {signed} = useContext(AuthContext);

  console.log(signed);

  async function handleSignIn() {
    const response = await signIn();
    console.log(response);
  }

  return (
    <View style={styles.container}>
      <Button title="Sign In" onPress={handleSignIn} />
    </View>
  );
};

export default SignIn;


Note: console show true as on App.tsx <AuthContext.Provider value={{signed: true}}>

Control Authentication state

src/contexts/auth.tsx

import React, {createContext} from 'react';

const AuthContext = createContext({signed: true});

export const AuthProvider: React.FC = ({children}) => (
  <AuthContext.Provider value={{signed: true}}>
      {children}
  </AuthContext.Provider>
);

export default AuthContext;

scr/App.tsx

...
import { AuthProvider } from './contexts/auth';
...

const App: React.FC = () => {
  return (
    <NavigationContainer>
      <AuthProvider>
        <Routes />
      </AuthProvider>
    </NavigationContainer>
  );
};
...

Adding interface and service to src/contexts/auth.tsx

import React, {createContext} from 'react';
import * as auth from '../services/auth';

interface AuthContextData {
  signed: boolean;
  user: object | null;
  signIn(): Promise<void>;
}

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

export const AuthProvider: React.FC = ({children}) => {
  async function signIn() {
    const response = await auth.signIn();

    console.log(response);
  }

  return (
    <AuthContext.Provider value={{signed: false, user: {}, signIn}}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

Adding signIn in src/SignIn/index.tsx

...

const SignIn: React.FC = () => {
  const {signed, user, signIn} = useContext(AuthContext);

  console.log(signed);
  console.log(user);

  async function handleSignIn() {
    await signIn();
    
  }

  return (
    <View style={styles.container}>
      <Button title="Sign In" onPress={handleSignIn} />
    </View>
  );
};
...

Set state in src/contexts/auth.tsx

import React, {createContext, useState} from 'react';
import * as auth from '../services/auth';

interface AuthContextData {
  signed: boolean;
  user: object | null;
  signIn(): Promise<void>;
}

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

export const AuthProvider: React.FC = ({children}) => {
  const [user, setUser] = useState<object | null>(null);

  async function signIn() {
    const response = await auth.signIn();

    setUser(response.user);

    console.log(response);
  }

  return (
    <AuthContext.Provider value={{signed: !!user, user: user, signIn}}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

Decide Route based on signed information src/routes/index.tsx

import React, {useContext} from 'react';

import AuthContext from '../contexts/auth';

import AuthRoutes from './auth.routes';
import AppRoutes from './app.routes';

const Routes: React.FC = () => {
  const {signed} = useContext(AuthContext);

  return signed ? <AppRoutes /> : <AuthRoutes />;
};

export default Routes;

Adding signOut function to src/contexts/auth.tsx

...
interface AuthContextData {
  signed: boolean;
  user: object | null;
  signIn(): Promise<void>;
  signOut(): void;
}
...


...
export const AuthProvider: React.FC = ({children}) => {
...

 function signOut() {
    setUser(null);
  }

  return (
    <AuthContext.Provider value={{signed: !!user, user: user, signIn, signOut}}>
      {children}
    </AuthContext.Provider>
  );
};
...

Adding Dashboard logout button

import React, {useContext} from 'react';
import {View, Button, StyleSheet} from 'react-native';

import AuthContext from '../../contexts/auth';

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

const Dashboard: React.FC = () => {
  const {signOut} = useContext(AuthContext);

  async function handleSignOut() {
    await signOut();
  }

  return (
    <View style={styles.container}>
      <Button title="Sign Out" onPress={handleSignOut} />
    </View>
  );
};

export default Dashboard;

Using async storage to save token and user

Install component

yarn add @react-native-community/async-storage

npx pod-install ios

yarn ios

src/contexts/auth.tsx

import React, {createContext, useState, useEffect} from 'react';
import AsyncStorage from '@react-native-community/async-storage';
import * as auth from '../services/auth';

interface AuthContextData {
  signed: boolean;
  user: object | null;
  signIn(): Promise<void>;
  signOut(): void;
}

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

export const AuthProvider: React.FC = ({children}) => {
  const [user, setUser] = useState<object | null>(null);

  useEffect(() => {
    async function loadStorageData() {
      // TODO: Try using multiget instead of 2 awaits
      const storageUser = await AsyncStorage.getItem('@reactNativeAuth:user');
      const storageToken = await AsyncStorage.getItem('@reactNativeAuth:token');

      if (storageUser && storageToken) {
        setUser(JSON.parse(storageUser));
      }
    }

    loadStorageData();
  });

  async function signIn() {
    const response = await auth.signIn();

    setUser(response.user);

    await AsyncStorage.setItem(
      '@reactNativeAuth:user',
      JSON.stringify(response.user),
    );
    await AsyncStorage.setItem('@reactNativeAuth:token', response.token);
  }

  function signOut() {
    AsyncStorage.clear().then(() => {
      setUser(null);
    });
  }

  return (
    <AuthContext.Provider value={{signed: !!user, user: user, signIn, signOut}}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

src/contexts/auth.tsx -> Including Loading

import { View, ActivityIndicator } from 'react-native';

...
    const [loading, setLoading] = useState(true);
...

...

    // A kind of sleep
    await new Promise((resolve) => setTimeout(resolve, 2000));

    if (storageUser && storageToken) {
        setUser(JSON.parse(storageUser));
        setLoading(false);
      }
...

...
  if (loading) {
    return (
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center'}}>
            <ActivityIndicator size="large" color="#666" />
        </View>
    )
  }


  return (
    <AuthContext.Provider value={{signed: !!user, user: user, signIn, signOut}}>
      {children}
    </AuthContext.Provider>
  );
...

OR send via interface to src/routes/index.tsx

interface AuthContextData {
  signed: boolean;
  user: object | null;
  loading: boolean;
  signIn(): Promise<void>;
  signOut(): void;
}

...
    const [loading, setLoading] = useState(true);
...

...

    // A kind of sleep
    await new Promise((resolve) => setTimeout(resolve, 2000));

    if (storageUser && storageToken) {
        setUser(JSON.parse(storageUser));
        setLoading(false);
      }
...

And in src/routes/index.tsx

...
import {View, ActivityIndicator} from 'react-native';
...

...
const Routes: React.FC = () => {
  const {signed, loading} = useContext(AuthContext);

  if (loading) {
    return (
      <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
        <ActivityIndicator size="large" color="#666" />
      </View>
    );
  }

  return signed ? <AppRoutes /> : <AuthRoutes />;
};
...

Request to server

yarn add axios

touch src/services/api.ts

Content src/services/api.ts

import axios from 'axios';

const api = axios.create({
  baseURL: 'http://127.0.0.1:8000/api/',
});

export default api;

Include into src/contexts/auth.tsx

...
import api from '../services/api';
...

...
  useEffect(() => {
    async function loadStorageData() {
      // TODO: Try using multiget instead of 2 awaits
      const storageUser = await AsyncStorage.getItem('@reactNativeAuth:user');
      const storageToken = await AsyncStorage.getItem('@reactNativeAuth:token');

      // A kind of sleep
      await new Promise((resolve) => setTimeout(resolve, 2000));

      if (storageUser && storageToken) {
        api.defaults.headers['Authorization'] = `Bearer ${storageToken}`;
        setUser(JSON.parse(storageUser));
        setLoading(false);
      }
    }

    loadStorageData();
  });

  async function signIn() {
    const response = await auth.signIn();

    setUser(response.user);

    api.defaults.headers['Authorization'] = `Bearer ${response.token}`;

    await AsyncStorage.setItem(
      '@reactNativeAuth:user',
      JSON.stringify(response.user),
    );
    await AsyncStorage.setItem('@reactNativeAuth:token', response.token);
  }
...

Creating a Hook useAuth()

src/contexts/auth.tsx

import React, {createContext, useState, useEffect, useContext} from 'react';
...

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

export const AuthProvider: React.FC = ({children}) => {
  ...
};

export function useAuth() {
  const context = useContext(AuthContext);

  return context;
}

Change every useContext to useAuth() on app.

SignIn
Dashboard
Routes

GIST

Everything created in this article can be aply to ReactJS. Just AsyncStorage must be localStorage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment