Notes
Notes
React Native note

Authentication flow notes for Cognito, Amplify, and custom UI wiring in Expo.

← Back to React Native topics

Setting up authentication and authorization using AWS

Skip straight to code - native Skip straight to code - web

Installing Dependencies

To get started, install required dependencies

Shell
npm add @aws-amplify/react-native aws-amplify @react-native-community/netinfo @react-native-async-storage/async-storage

Building Context

Context is a component that makes data available to component in tree, no matter how nested the component resides in.

To create a context we can use built in API.

src/contexts/Auth/index.tsx

// File Path -  src/contexts/Auth/index.tsx

import { createContext } from 'react';
import {
  AuthContextType,
  AuthProviderProps,
  ConfirmUserProps,
  CreateUserProps,
} from './types';
import { signUp, confirmSignUp } from 'aws-amplify/auth';

const AuthContext = createContext<AuthProviderProps | null>(null);

const AuthProvider = ({ children }: AuthContextType) => {

  const register = async ({ username, password }: CreateUserProps) => {
    try {
      await signUp({ username, password });
    } catch (e) {
      console.error(e);
      throw e;
    }
  };

  const confirmUser = async ({
    username,
    confirmationCode,
  }: ConfirmUserProps) => {
    try {
      await confirmSignUp({ username, confirmationCode });
    }catch (e) {
      console.error(e);
      throw e;
    }
  };

  return (
    <AuthContext.Provider
      value={{
        register,
        confirmUser,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

export { AuthContext, AuthProvider };


For brevity purpose, create a separate file for types

Typescript

//File Path - src/contexts/Auth/types.ts

import { ReactNode } from 'react';
import {
  ConfirmSignUpInput,
  SignUpInput,
} from 'aws-amplify/auth';

export type AuthContextType = {
  children: ReactNode;
};

export type CreateUserProps = SignUpInput;

export type ConfirmUserProps = ConfirmSignUpInput;

export interface AuthProviderProps {
  register: (args: CreateUserProps) => Promise<void>;
  confirmUser: (args: ConfirmUserProps) => Promise<void>;
}

To get the data from context we can create a custom hook that wraps, React's useContext hook

src/hooks/useAuth.ts

// File Path -  src/hooks/useAuth.ts

import {useContext} from 'react';
import {AuthContext} from '../contexts';

export const useAuth = () => {
  const authContext = useContext(AuthContext);
  if (authContext == undefined) {
    throw new Error('useAuth must be used within a AuthProvider')
  }
  return authContext;
}

Don't forget to add Auth context to entry file

App.tsx

// File Path - App.tsx

import {AuthProvider} from "./src/contexts/Auth";

export function App() {
  return (
    <AuthProvider>
      <SomeComponents />
    </AuthProvider>
  );
}

Configure Cognito

Provide your cognito user pool and user client id. These values can be obtained from AWS Console.

Store these values in .env file and then pass these value to Amplify.configure function.

App.tsx

// File Path - App.tsx

import {Amplify} from "aws-amplify";

Amplify.configure({
  Auth: {
    Cognito: {
      userPoolId: `${process.env.EXPO_PUBLIC_USER_POOL_ID}`,
      userPoolClientId: `${process.env.EXPO_PUBLIC_USER_POOL_CLIENT_ID}`,
    },
  }
});

Building Components

For simplicity, lets use UI component library to build components.

For this example, we are using tamagui

Shell
npm add tamagui @tamagui/config @tamagui/babel-plugin

After the installation is complete, update babel.config.js

babel.config.js

// File Path - babel.config.js

module.exports = function (api) {
  api.cache(true)
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      [
        '@tamagui/babel-plugin',
        {
          components: ['tamagui'],
          config: './tamagui.config.ts',
          logTimings: true,
          disableExtraction: process.env.NODE_ENV === 'development',
        },
      ],
      'react-native-reanimated/plugin',
    ],
  }
}

For now, lets use default configuration provided by Tamagui for theme

tamagui.config.ts

// File Path - tamagui.config.ts

import {createTamagui} from "tamagui";
import {config as tamaguiConfig} from "@tamagui/config/v4";

export const config = createTamagui(tamaguiConfig);

Add TamaguiProvider to our entry file

App.tsx

// File Path - App.tsx

import {AuthProvider} from "./src/contexts";
import {TamaguiProvider} from "tamagui";
import {config} from "./tamagui.config";

export function App() {
  return (
    <TamaguiProvider config={config}>
      <AuthProvider>
        <SomeComponents />
      </AuthProvider>
    </TamaguiProvider>
  );
}

Unit testing

jest is a popular testing framework.

Native testing library provides queries that work with React Native and not rely on DOM

jest-expo mocks out native module, making it easier to test projects that use expo packages

Shell
npm add jest jest-expo @testing-library/react-native @types/react-test-renderer react-test-renderer @types/jest --save-dev

Create a jest.config.ts, setupTests.ts file at the root of the project. We can then add our jest configuration in this file. While you can use package.json to add jest config, its better to separate configuration from dependency file

jest.config.ts

// File Path - jest.config.ts

import type {Config} from 'jest';

const config: Config = {
  clearMocks: true,
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageProvider: 'v8',
  preset: 'jest-expo',
  setupFilesAfterEnv: ['./setupTests.ts'],
  transformIgnorePatterns: [
    'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)/expo-modules-core|aws-amplify/.*|aws-amplify|@aws-amplify/react-native'
  ],
};

export default config;
setupTests.ts

// File Path - setupTests.ts

jest.useFakeTimers();
jest.mock('@react-native-async-storage/async-storage', () =>
  require('@react-native-async-storage/async-storage/jest/async-storage-mock')
);

jest.mock('@aws-amplify/react-native', () => ({
  loadBase64: jest.fn().mockImplementation(() => ({
    encode: jest.fn(),
  })),
  loadGetRandomValues: jest.fn(),
  loadUrlPolyfill: jest.fn(),
  loadAsyncStorage: jest.fn(),
  loadAppState: jest.fn(() => ({
    addEventListener: jest.fn(),
  })),
}));

Unit test for AuthContext

__tests__/contexts/Auth.test.tsx

// File Path - __tests__/contexts/Auth.test.tsx

import * as React from 'react';
import { render, screen, userEvent } from '@testing-library/react-native';
import * as Auth from 'aws-amplify/auth';
import { AuthProvider, Button, ThemeProvider, useAuth } from '../../src';

jest.mock('aws-amplify/auth');

type TriggerProps = {
  triggerType: TriggerType;
  username?: string;
  password?: string;
  confirmationCode?: string;
};

const Trigger = ({
  triggerType,
  username = '',
  password = '',
  confirmationCode = '',
}: TriggerProps)  => {
  const { register, confirmUser } = useAuth();
  const onPress = async () => {
    const lookUp = {
      register: () => register({ username, password }),
      confirmUser: () => confirmUser({ username, confirmationCode }),
    }[triggerType];
    await lookUp();
  };
  return (
    <Button testID="test-button" onPress={onPress}>
      Press Me
    </Button>
  );
};
export type TriggerType = 'register' | 'confirmUser' | 'login' | 'logout';
const ComponentUnderTest = ({ triggerType }: { triggerType?: TriggerType }) => {
  return (
    <ThemeProvider defaultTheme="dark">
      <AuthProvider>
        <Trigger triggerType={triggerType} />
      </AuthProvider>
    </ThemeProvider>
  );
};
describe('Auth Context', () => {
  it('renders without crashing', () => {
    render(<ComponentUnderTest />);
    expect(screen.getByText('Press Me')).toBeTruthy();
  });

  it(`calls signUp from amplify when register is called`, async () => {
    const user = userEvent.setup();
    const mockedSignUp = jest.fn();
    const expectedParams = { password: '', username: '' };

    (Auth as jest.Mocked<typeof Auth>).signUp = mockedSignUp.mockResolvedValue(
      {}
    );
    render(<ComponentUnderTest triggerType="register" />);
    const element = screen.getByTestId('test-button');
    await user.press(element);
    expect(mockedSignUp).toHaveBeenCalledTimes(1);
    expect(mockedSignUp).toHaveBeenCalledWith(expectedParams);
  });

  it('calls confirmSignUp from amplify when confirmUser is called', async () => {
    const user = userEvent.setup();
    const mockedConfirmSignUp = jest.fn();
    const expectedParams = { confirmationCode: '', username: '' };
    (Auth as jest.Mocked<typeof Auth>).confirmSignUp =
      mockedConfirmSignUp.mockResolvedValue({});
    render(<ComponentUnderTest triggerType="confirmUser" />);
    const element = screen.getByTestId('test-button');
    await user.press(element);
    expect(mockedConfirmSignUp).toHaveBeenCalledTimes(1);
    expect(mockedConfirmSignUp).toHaveBeenCalledWith(expectedParams);
  });
});


  • Integration testing

  • Detox is a popular library that enables testing an application on simulator. It can be used to add integration testing to a product.

    API calls can be mocked if need be.

    More on detox coming soon in another notes

    References