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 @@ ...@@ -7,7 +7,7 @@
"ios": "react-native run-ios", "ios": "react-native run-ios",
"start": "react-native start", "start": "react-native start",
"test": "jest --coverage --watchAll=false --verbose --collectCoverageFrom=\"src/**/*.tsx\"", "test": "jest --coverage --watchAll=false --verbose --collectCoverageFrom=\"src/**/*.tsx\"",
"test-only": "jest -t", "test-only": "jest --verbose -t",
"lint": "eslint . --ext .ts,.tsx --fix", "lint": "eslint . --ext .ts,.tsx --fix",
"prettify": "prettier --write src", "prettify": "prettier --write src",
"prep": "npx mrm lint-staged", "prep": "npx mrm lint-staged",
......
...@@ -14,6 +14,16 @@ export const invalidRegistrationValues: { [_: string]: any } = { ...@@ -14,6 +14,16 @@ export const invalidRegistrationValues: { [_: string]: any } = {
password2: '12345678', 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 = { export const authResponse: LoginResponse = {
access_token: 'ax41faf', access_token: 'ax41faf',
refresh_token: '9tka0kfa', refresh_token: '9tka0kfa',
......
...@@ -19,7 +19,7 @@ const App: FC = () => { ...@@ -19,7 +19,7 @@ const App: FC = () => {
<ContextProvider> <ContextProvider>
<NavigationContainer> <NavigationContainer>
<Stack.Navigator <Stack.Navigator
initialRouteName={ROUTES.registration} initialRouteName={ROUTES.initial}
screenOptions={screenOptions}> screenOptions={screenOptions}>
{navigation.map((nav, i) => ( {navigation.map((nav, i) => (
<Stack.Screen <Stack.Screen
......
...@@ -4,8 +4,13 @@ import { GoogleSignin } from '@react-native-google-signin/google-signin'; ...@@ -4,8 +4,13 @@ import { GoogleSignin } from '@react-native-google-signin/google-signin';
import { Toast } from 'components/core'; import { Toast } from 'components/core';
import CACHE_KEYS from 'constants/cacheKeys'; import CACHE_KEYS from 'constants/cacheKeys';
import { removeCache, getCache, setCache } from 'utils/cache'; import { removeCache, getCache, setCache } from 'utils/cache';
import { googleLoginApi, signupApi } from 'services/auth'; import { googleLoginApi, loginApi, signupApi } from 'services/auth';
import { User, RegistrationRequest } from 'services/auth/models'; import {
User,
RegistrationRequest,
LoginRequest,
LoginResponse,
} from 'services/auth/models';
import { set401Callback, setAuthHeader, resetAuthHeader } from 'services/api'; import { set401Callback, setAuthHeader, resetAuthHeader } from 'services/api';
import { iUserContext } from './types'; import { iUserContext } from './types';
...@@ -16,6 +21,19 @@ const initialUser = { ...@@ -16,6 +21,19 @@ const initialUser = {
name: '', 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>({ export const UserContext = createContext<iUserContext>({
user: initialUser, user: initialUser,
isAuthenticated: false, isAuthenticated: false,
...@@ -40,19 +58,15 @@ export const useUserContext = (): iUserContext => { ...@@ -40,19 +58,15 @@ export const useUserContext = (): iUserContext => {
const signup = async (registerData: RegistrationRequest) => { const signup = async (registerData: RegistrationRequest) => {
const response = await signupApi(registerData); const response = await signupApi(registerData);
await setUserFromResponse(response.success, setUser, response.data);
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);
}
return response; return response;
}; };
// TODO const login = async (loginData: LoginRequest) => {
const login = async () => {}; const response = await loginApi(loginData);
await setUserFromResponse(response.success, setUser, response.data);
return response;
};
const logout = useCallback(async () => { const logout = useCallback(async () => {
await GoogleSignin.signOut(); await GoogleSignin.signOut();
......
import { ApiResponse } from 'services/api'; 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 { export interface iUserContext {
user: User; user: User;
isAuthenticated: boolean; isAuthenticated: boolean;
isLoading: boolean; isLoading: boolean;
signup: (data: RegistrationRequest) => ApiResponse<LoginResponse>; signup: (data: RegistrationRequest) => ApiResponse<LoginResponse>;
login: () => Promise<void>; login: (data: LoginRequest) => ApiResponse<LoginResponse>;
loginWithGoogle: () => Promise<void>; loginWithGoogle: () => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
} }
import React from 'react'; import React from 'react';
import { render } from 'utils/testing'; import { render, fireEvent, waitFor } from 'utils/testing';
import * as ROUTES from 'constants/routes'; import * as ROUTES from 'constants/routes';
import axios from 'axios';
import Login from '.'; 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', () => { describe('Login page', () => {
it('renders correctly', () => { it('renders correctly', () => {
render(<Login />, ROUTES.login); 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 React, { FC, useContext, useState } from 'react';
import { View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { UserContext } from 'provider'; import { UserContext } from 'provider';
import { useAuthEffect, useForm } from 'hooks';
import { GoogleLoginButton } from '../components'; 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 { 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 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(); useAuthEffect();
return ( return (
<View style={layoutStyles}> <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> </View>
); );
}; };
const styles = StyleSheet.create({
nonfieldError: { color: 'red' },
});
export default Login; 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'; ...@@ -3,15 +3,15 @@ import { useAuthEffect, useForm } from 'hooks';
import { ScrollView } from 'react-native-gesture-handler'; import { ScrollView } from 'react-native-gesture-handler';
import { BigButton, Toast } from 'components/core'; import { BigButton, Toast } from 'components/core';
import { Section } from 'components/layout';
import { TextField } from 'components/form'; import { TextField } from 'components/form';
import { GoogleLoginButton } from '../components';
import { fieldValidation, initialValues, textField } from './schema'; import { fieldValidation, initialValues, textField } from './schema';
import { generateValidationSchema } from 'utils/form'; import { generateValidationSchema } from 'utils/form';
import { UserContext } from 'provider'; import { UserContext } from 'provider';
import { layoutStyles } from 'styles'; import { layoutStyles } from 'styles';
import { GoogleLoginButton } from '../components';
import { Section } from 'components/layout';
const isPasswordField = (name: string) => const isPasswordField = (name: string) =>
name === 'password1' || name === 'password2'; name === 'password1' || name === 'password2';
......
...@@ -3,6 +3,7 @@ import { api, RequestMethod, ApiResponse } from '../api'; ...@@ -3,6 +3,7 @@ import { api, RequestMethod, ApiResponse } from '../api';
import * as apiUrls from './urls'; import * as apiUrls from './urls';
import { import {
GoogleLoginRequest, GoogleLoginRequest,
LoginRequest,
LoginResponse, LoginResponse,
RegistrationRequest, RegistrationRequest,
} from './models'; } from './models';
...@@ -18,3 +19,7 @@ export const signupApi = ( ...@@ -18,3 +19,7 @@ export const signupApi = (
): ApiResponse<LoginResponse> => { ): ApiResponse<LoginResponse> => {
return api(RequestMethod.POST, apiUrls.signup, body); 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 { ...@@ -9,6 +9,14 @@ export interface RegistrationRequest {
password2: string; password2: string;
} }
export type Role = 'client' | 'nutritionist' | 'admin';
export interface LoginRequest {
email: string;
password: string;
role: Role;
}
export interface User { export interface User {
id: number | null; id: number | null;
email: string; email: string;
......
...@@ -2,3 +2,4 @@ const auth = 'auth/'; ...@@ -2,3 +2,4 @@ const auth = 'auth/';
export const google = `${auth}google/`; export const google = `${auth}google/`;
export const signup = `${auth}registration/`; export const signup = `${auth}registration/`;
export const login = `${auth}user-login/`;
...@@ -2,7 +2,9 @@ import { Props as TextFieldProps } from 'components/form/TextField/types'; ...@@ -2,7 +2,9 @@ import { Props as TextFieldProps } from 'components/form/TextField/types';
import { Props as FormLabelProps } from 'components/form/FormLabel/types'; import { Props as FormLabelProps } from 'components/form/FormLabel/types';
import { Choice } from 'components/form/MultipleChoice/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 { export interface RadioButtonGroupSchema extends FormLabelProps {
choices: Choice[]; 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