Fakultas Ilmu Komputer UI

Commit 94558afb authored by Doan Andreas Nathanael's avatar Doan Andreas Nathanael Committed by Muzaki Azami
Browse files

Login Form UI & API Integration

parent 6f0e02cb
......@@ -7,7 +7,7 @@
"ios": "react-native run-ios",
"start": "react-native start",
"test": "jest --coverage --watchAll=false --verbose --collectCoverageFrom=\"src/**/*.tsx\"",
"test-only": "jest -t",
"test-only": "jest --verbose -t",
"lint": "eslint . --ext .ts,.tsx --fix",
"prettify": "prettier --write src",
"prep": "npx mrm lint-staged",
......
......@@ -14,6 +14,16 @@ export const invalidRegistrationValues: { [_: string]: any } = {
password2: '12345678',
};
export const validLoginValues: { [_: string]: any } = {
email: 'doan@dietela.com',
password: 'g8ake1afig',
};
export const invalidLoginValues: { [_: string]: any } = {
email: 'doan',
password: '12345678',
};
export const authResponse: LoginResponse = {
access_token: 'ax41faf',
refresh_token: '9tka0kfa',
......
......@@ -19,7 +19,7 @@ const App: FC = () => {
<ContextProvider>
<NavigationContainer>
<Stack.Navigator
initialRouteName={ROUTES.registration}
initialRouteName={ROUTES.initial}
screenOptions={screenOptions}>
{navigation.map((nav, i) => (
<Stack.Screen
......
......@@ -4,8 +4,13 @@ import { GoogleSignin } from '@react-native-google-signin/google-signin';
import { Toast } from 'components/core';
import CACHE_KEYS from 'constants/cacheKeys';
import { removeCache, getCache, setCache } from 'utils/cache';
import { googleLoginApi, signupApi } from 'services/auth';
import { User, RegistrationRequest } from 'services/auth/models';
import { googleLoginApi, loginApi, signupApi } from 'services/auth';
import {
User,
RegistrationRequest,
LoginRequest,
LoginResponse,
} from 'services/auth/models';
import { set401Callback, setAuthHeader, resetAuthHeader } from 'services/api';
import { iUserContext } from './types';
......@@ -16,6 +21,19 @@ const initialUser = {
name: '',
};
const setUserFromResponse = async (
success: boolean,
setUser: React.Dispatch<React.SetStateAction<User>>,
data?: LoginResponse,
) => {
if (success && data) {
await setCache(CACHE_KEYS.authToken, data.access_token);
await setCache(CACHE_KEYS.refreshToken, data.refresh_token);
setUser(data.user);
}
};
export const UserContext = createContext<iUserContext>({
user: initialUser,
isAuthenticated: false,
......@@ -40,19 +58,15 @@ export const useUserContext = (): iUserContext => {
const signup = async (registerData: RegistrationRequest) => {
const response = await signupApi(registerData);
if (response.success && response.data) {
await setCache(CACHE_KEYS.authToken, response.data?.access_token);
await setCache(CACHE_KEYS.refreshToken, response.data?.refresh_token);
setUser(response.data.user);
}
await setUserFromResponse(response.success, setUser, response.data);
return response;
};
// TODO
const login = async () => {};
const login = async (loginData: LoginRequest) => {
const response = await loginApi(loginData);
await setUserFromResponse(response.success, setUser, response.data);
return response;
};
const logout = useCallback(async () => {
await GoogleSignin.signOut();
......
import { ApiResponse } from 'services/api';
import { LoginResponse, RegistrationRequest, User } from 'services/auth/models';
import {
LoginRequest,
LoginResponse,
RegistrationRequest,
User,
} from 'services/auth/models';
export interface iUserContext {
user: User;
isAuthenticated: boolean;
isLoading: boolean;
signup: (data: RegistrationRequest) => ApiResponse<LoginResponse>;
login: () => Promise<void>;
login: (data: LoginRequest) => ApiResponse<LoginResponse>;
loginWithGoogle: () => Promise<void>;
logout: () => Promise<void>;
}
import React from 'react';
import { render } from 'utils/testing';
import { render, fireEvent, waitFor } from 'utils/testing';
import * as ROUTES from 'constants/routes';
import axios from 'axios';
import Login from '.';
import {
authResponse,
invalidLoginValues,
validLoginValues,
} from '__mocks__/auth';
import { textField } from './schema';
jest.mock('react-native-toast-message');
jest.mock('axios');
const mockAxios = axios as jest.Mocked<typeof axios>;
describe('Login page', () => {
it('renders correctly', () => {
render(<Login />, ROUTES.login);
});
it('success when field is valid and submit success', async () => {
const loginApi = () =>
Promise.resolve({
status: 201,
data: authResponse,
});
mockAxios.request.mockImplementationOnce(loginApi);
const { getByPlaceholderText, queryByText, getByTestId } = render(
<Login />,
ROUTES.login,
);
textField.map(({ name, placeholder }) => {
const formField = getByPlaceholderText(placeholder as string);
fireEvent.changeText(formField, validLoginValues[name]);
});
const loginButton = getByTestId('loginButton');
await waitFor(() => fireEvent.press(loginButton));
const toastWarning = queryByText(/Profile/i);
expect(toastWarning).toBeTruthy();
});
it('fails when field is invalid and submit success', async () => {
const loginApi = () =>
Promise.reject({
status: 400,
response: {
data: 'error',
},
});
mockAxios.request.mockImplementationOnce(loginApi);
const { getByPlaceholderText, queryByText, getByTestId } = render(
<Login />,
ROUTES.login,
);
textField.map(({ name, placeholder }) => {
const formField = getByPlaceholderText(placeholder as string);
fireEvent.changeText(formField, invalidLoginValues[name]);
});
const loginButton = getByTestId('loginButton');
await waitFor(() => fireEvent.press(loginButton));
const toastWarning = queryByText(/Profile/i);
expect(toastWarning).toBeFalsy();
});
afterAll(() => {
jest.clearAllMocks();
});
});
import React, { FC, useContext } from 'react';
import { View } from 'react-native';
import React, { FC, useContext, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { UserContext } from 'provider';
import { useAuthEffect, useForm } from 'hooks';
import { GoogleLoginButton } from '../components';
import { BigButton, Toast } from 'components/core';
import { fieldValidation, initialValues, textField } from './schema';
import { generateValidationSchema } from 'utils/form';
import { layoutStyles } from 'styles';
import { useAuthEffect } from 'hooks';
import { TextField } from 'components/form';
import { Section } from 'components/layout';
const isPasswordField = (name: string) => name === 'password';
const Login: FC = () => {
const { isLoading, loginWithGoogle } = useContext(UserContext);
const { login, isLoading, loginWithGoogle } = useContext(UserContext);
const [nonFieldError, setNonFieldError] = useState<string | null>();
const {
getTextInputProps,
handleSubmit,
isSubmitting,
setFieldError,
} = useForm({
initialValues,
validationSchema: generateValidationSchema(fieldValidation),
onSubmit: async (values) => {
const response = await login(values);
if (!response.success) {
const error = response.error;
setFieldError('email', error.email);
setFieldError('password', error.password);
setNonFieldError(error.non_field_errors);
Toast.show({
type: 'error',
text1: 'Gagal login akun',
text2: 'Terjadi kesalahan login. Silakan coba lagi',
});
}
},
});
useAuthEffect();
return (
<View style={layoutStyles}>
<GoogleLoginButton onPress={loginWithGoogle} isLoading={isLoading} />
{textField.map(({ name, label, required, placeholder }, i) => (
<TextField
key={`field${i}`}
label={label}
required={required}
placeholder={placeholder}
{...getTextInputProps(name)}
secureTextEntry={isPasswordField(name)}
/>
))}
{nonFieldError && (
<Text style={styles.nonfieldError}>{nonFieldError}</Text>
)}
<Section>
<BigButton
title="login"
onPress={handleSubmit}
loading={isSubmitting}
testID="loginButton"
/>
</Section>
<Section>
<GoogleLoginButton onPress={loginWithGoogle} isLoading={isLoading} />
</Section>
</View>
);
};
const styles = StyleSheet.create({
nonfieldError: { color: 'red' },
});
export default Login;
import { LoginRequest, Role } from 'services/auth/models';
import { TextFieldSchema } from 'types/form';
import { FieldType, FieldValidation } from 'utils/form';
export const textField: TextFieldSchema[] = [
{
label: 'Email address',
placeholder: 'Masukkan email Anda',
required: true,
name: 'email',
},
{
label: 'Password',
placeholder: 'Masukkan password Anda',
required: true,
name: 'password',
},
];
export const initialValues: LoginRequest = {
email: '',
password: '',
role: 'client',
};
export const fieldValidation: FieldValidation[] = [
{
name: 'email',
required: true,
label: 'Email address',
type: FieldType.EMAIL,
},
{
name: 'password',
required: true,
label: 'Password',
type: FieldType.PASSWORD,
},
];
export const setRole = (role: Role) => (initialValues.role = role);
......@@ -3,15 +3,15 @@ import { useAuthEffect, useForm } from 'hooks';
import { ScrollView } from 'react-native-gesture-handler';
import { BigButton, Toast } from 'components/core';
import { Section } from 'components/layout';
import { TextField } from 'components/form';
import { GoogleLoginButton } from '../components';
import { fieldValidation, initialValues, textField } from './schema';
import { generateValidationSchema } from 'utils/form';
import { UserContext } from 'provider';
import { layoutStyles } from 'styles';
import { GoogleLoginButton } from '../components';
import { Section } from 'components/layout';
const isPasswordField = (name: string) =>
name === 'password1' || name === 'password2';
......
......@@ -3,6 +3,7 @@ import { api, RequestMethod, ApiResponse } from '../api';
import * as apiUrls from './urls';
import {
GoogleLoginRequest,
LoginRequest,
LoginResponse,
RegistrationRequest,
} from './models';
......@@ -18,3 +19,7 @@ export const signupApi = (
): ApiResponse<LoginResponse> => {
return api(RequestMethod.POST, apiUrls.signup, body);
};
export const loginApi = (body: LoginRequest): ApiResponse<LoginResponse> => {
return api(RequestMethod.POST, apiUrls.login, body);
};
......@@ -9,6 +9,14 @@ export interface RegistrationRequest {
password2: string;
}
export type Role = 'client' | 'nutritionist' | 'admin';
export interface LoginRequest {
email: string;
password: string;
role: Role;
}
export interface User {
id: number | null;
email: string;
......
......@@ -2,3 +2,4 @@ const auth = 'auth/';
export const google = `${auth}google/`;
export const signup = `${auth}registration/`;
export const login = `${auth}user-login/`;
......@@ -2,7 +2,9 @@ import { Props as TextFieldProps } from 'components/form/TextField/types';
import { Props as FormLabelProps } from 'components/form/FormLabel/types';
import { Choice } from 'components/form/MultipleChoice/types';
export type TextFieldSchema = TextFieldProps & { name: string };
export type TextFieldSchema = TextFieldProps & {
name: string;
};
export interface RadioButtonGroupSchema extends FormLabelProps {
choices: Choice[];
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment