Setting up authentication and authorization using AWS
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
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
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
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
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
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
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
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
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
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
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
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.