diff --git a/jestSetup.js b/jestSetup.js index 11d717c11dffe8ec6fab34412a35248d67fdb21b..8d0737a83fe3b67aae7b9cf7b4fe3ebde7675da2 100644 --- a/jestSetup.js +++ b/jestSetup.js @@ -4,6 +4,7 @@ import { NativeModules } from 'react-native'; jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper'); jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage); +jest.mock('axios'); NativeModules.RNGoogleSignin = { BUTTON_SIZE_ICON: 0, diff --git a/src/__mocks__/auth.ts b/src/__mocks__/auth.ts index 76f38bfc24e65cbfa414170f6baad5defc7c254a..5bcb78d198db3a023d502550f3a5a4a366698ce7 100644 --- a/src/__mocks__/auth.ts +++ b/src/__mocks__/auth.ts @@ -1,4 +1,4 @@ -import { LoginResponse } from 'services/auth/models'; +import { LoginResponse, UserRole } from 'services/auth/models'; export const validRegistrationValues: { [_: string]: any } = { name: 'Doan Didinding', @@ -31,5 +31,6 @@ export const authResponse: LoginResponse = { id: 1, email: validRegistrationValues.email, name: validRegistrationValues.name, + role: UserRole.CLIENT, }, }; diff --git a/src/app/index.test.tsx b/src/app/index.test.tsx index 887ae9cb741213bc91d004be1844742bc92205a4..272166db6f3c28d3e8198bb8f28b44e89fb59ab9 100644 --- a/src/app/index.test.tsx +++ b/src/app/index.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render } from '@testing-library/react-native'; -import { HeaderLeft, ErrorToast } from './styles'; +import { ErrorToast } from './styles'; import App from '.'; describe('Application', () => { @@ -9,10 +9,6 @@ describe('Application', () => { render(); }); - test('header left button renders correctly', () => { - render(); - }); - test('error toast renders correctly', () => { render(); }); diff --git a/src/app/styles.tsx b/src/app/styles.tsx index 6aec16d056c54164364b536421fb4e79620a0dda..e05042cf72ef559e561b54bf986d33efabc010fb 100644 --- a/src/app/styles.tsx +++ b/src/app/styles.tsx @@ -1,17 +1,12 @@ import React from 'react'; import { StyleSheet } from 'react-native'; import { - HeaderBackButton, - StackHeaderLeftButtonProps, StackNavigationOptions, + TransitionSpecs, } from '@react-navigation/stack'; import { BaseToast, BaseToastProps } from 'react-native-toast-message'; import { colors, typographyStyles, typography } from 'styles'; -export const HeaderLeft = (props: StackHeaderLeftButtonProps) => ( - -); - export const screenOptions: StackNavigationOptions = { cardStyle: { backgroundColor: '#fff', @@ -24,7 +19,16 @@ export const screenOptions: StackNavigationOptions = { headerTintColor: colors.primary, headerTitleStyle: typographyStyles.headingMedium, headerTitleAlign: 'center', - headerLeft: HeaderLeft, + transitionSpec: { + open: { + animation: 'timing', + config: { + duration: 1, + delay: 0, + }, + }, + close: TransitionSpecs.TransitionIOSSpec, + }, }; const styles = StyleSheet.create({ diff --git a/src/components/core/Loader/index.test.tsx b/src/components/core/Loader/index.test.tsx index a7568fdb942e3a3e55e47a3aa7192f56213f3d67..2e74ac89dd35a708a725cb38b69244cc83905719 100644 --- a/src/components/core/Loader/index.test.tsx +++ b/src/components/core/Loader/index.test.tsx @@ -1,10 +1,16 @@ import React from 'react'; import { render } from '@testing-library/react-native'; -import Loader from '.'; +import Loader, { DietelaCoverLoader } from '.'; describe('Loader component', () => { it('renders correctly', () => { render(); }); }); + +describe('DietelaCoverLoader component', () => { + it('renders correctly', () => { + render(); + }); +}); diff --git a/src/components/core/Loader/index.tsx b/src/components/core/Loader/index.tsx index b6daf4bcfffa8a98f0e2fe2b2e2ab260743e4174..b53fd37021d4fbdc5964bcb457ae89c7c2b2e2ce 100644 --- a/src/components/core/Loader/index.tsx +++ b/src/components/core/Loader/index.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { ActivityIndicator, StyleSheet, View } from 'react-native'; import { colors } from 'styles'; +import { dietelaLogo } from 'assets/images'; +import { Image } from 'react-native-elements'; const Loader = () => ( @@ -8,11 +10,35 @@ const Loader = () => ( ); +const DietelaCoverLoader = () => ( + + + +); + const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', }, + dietelaCover: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.primaryYellow, + }, + logo: { + width: 200, + height: 100, + }, + img: { + backgroundColor: colors.primaryYellow, + }, }); export default Loader; +export { DietelaCoverLoader }; diff --git a/src/components/core/index.ts b/src/components/core/index.ts index 0c60fed61377d58a56ae34113a3afe28abb6ca1a..a587993dd74e3e02a14d75d02f7a8bb0726af120 100644 --- a/src/components/core/index.ts +++ b/src/components/core/index.ts @@ -3,7 +3,7 @@ export { default as BigButton } from './BigButton'; export { default as CarouselPagination } from './CarouselPagination'; export { default as InfoCard } from './InfoCard'; export { default as Link } from './Link'; -export { default as Loader } from './Loader'; +export { default as Loader, DietelaCoverLoader } from './Loader'; export { default as ResultCard } from './ResultCard'; export { default as Statistic } from './Statistic'; export { default as Toast } from 'react-native-toast-message'; diff --git a/src/hooks/useAuthEffect/index.ts b/src/hooks/useAuthEffect/index.ts index 473ea2b6754f93f96d37e32fb76c8788679dbf26..413dc7468d245928c3f72789362ddbe715adcaaf 100644 --- a/src/hooks/useAuthEffect/index.ts +++ b/src/hooks/useAuthEffect/index.ts @@ -1,21 +1,61 @@ -import { useContext, useEffect } from 'react'; +import { useContext, useEffect, useCallback, useState } from 'react'; import { useNavigation } from '@react-navigation/native'; import { UserContext } from 'provider'; import * as ROUTES from 'constants/routes'; +import CACHE_KEYS from 'constants/cacheKeys'; +import { getCache } from 'utils/cache'; -const useAuthEffect = () => { - const { isAuthenticated } = useContext(UserContext); +const useAuthEffect = (isLogin?: boolean) => { + const { isAuthenticated, isUnpaidClient, isFirstLoading } = useContext( + UserContext, + ); const navigation = useNavigation(); + const [isLoading, setIsLoading] = useState(false); + + const checkCart = useCallback(async () => { + setIsLoading(true); + const dietProfileId = await getCache(CACHE_KEYS.dietProfileId); + const cartId = await getCache(CACHE_KEYS.cartId); + if (!dietProfileId) { + navigation.reset({ + index: 0, + routes: [ + { name: ROUTES.initial }, + { name: ROUTES.allAccessQuestionnaire }, + ], + }); + } else if (!cartId) { + navigation.reset({ + index: 0, + routes: [{ name: ROUTES.initial }, { name: ROUTES.choosePlan }], + }); + } + setIsLoading(false); + }, [navigation]); useEffect(() => { if (isAuthenticated) { - if (navigation.canGoBack()) { - navigation.goBack(); + if (isUnpaidClient) { + navigation.reset({ + index: 0, + routes: [{ name: ROUTES.checkout }], + }); } else { - navigation.navigate(ROUTES.profile); + navigation.reset({ + index: 0, + routes: [{ name: ROUTES.profile }], + }); } + } else if (isLogin) { + checkCart(); } - }, [isAuthenticated, navigation]); + + return () => { + setIsLoading(false); + }; + }, [checkCart, isLogin, isAuthenticated, isUnpaidClient, navigation]); + + return isFirstLoading || isLoading; }; export default useAuthEffect; diff --git a/src/hooks/useAuthGuardEffect/index.ts b/src/hooks/useAuthGuardEffect/index.ts index 4cf67fe78bd6cfaee20ccfaf429f18fdfef26bcb..f5c828b94ab01cfead917b8eb6c95ad1fefde37c 100644 --- a/src/hooks/useAuthGuardEffect/index.ts +++ b/src/hooks/useAuthGuardEffect/index.ts @@ -3,15 +3,15 @@ import { useNavigation } from '@react-navigation/native'; import { UserContext } from 'provider'; import * as ROUTES from 'constants/routes'; -const useAuthGuardEffect = () => { +const useAuthGuardEffect = (signupFallback?: boolean) => { const { isAuthenticated } = useContext(UserContext); const navigation = useNavigation(); useEffect(() => { if (!isAuthenticated) { - navigation.navigate(ROUTES.login); + navigation.navigate(signupFallback ? ROUTES.registration : ROUTES.login); } - }, [isAuthenticated, navigation]); + }, [isAuthenticated, signupFallback, navigation]); }; export default useAuthGuardEffect; diff --git a/src/provider/UserContext/index.ts b/src/provider/UserContext/index.ts index bf07d3c3aa71787ee47efdf29ad43576a8eeb856..ecf0a4d1b3e2fc00ee350b741f4fa81dc6840d29 100644 --- a/src/provider/UserContext/index.ts +++ b/src/provider/UserContext/index.ts @@ -4,12 +4,20 @@ 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, loginApi, signupApi } from 'services/auth'; + +import { + googleLoginApi, + loginApi, + signupApi, + retrieveUserApi, + linkUserDataApi, +} from 'services/auth'; import { User, RegistrationRequest, LoginRequest, LoginResponse, + UserRole, } from 'services/auth/models'; import { set401Callback, setAuthHeader, resetAuthHeader } from 'services/api'; @@ -19,25 +27,18 @@ const initialUser = { id: null, email: '', name: '', -}; - -const setUserFromResponse = async ( - success: boolean, - setUser: React.Dispatch>, - data?: LoginResponse, -) => { - if (success && data) { - await setCache(CACHE_KEYS.authToken, data.access_token); - await setCache(CACHE_KEYS.refreshToken, data.refresh_token); - - setUser(data.user); - } + role: null, }; export const UserContext = createContext({ user: initialUser, isAuthenticated: false, + isUnpaidClient: false, + isPaidClient: false, + isNutritionist: false, + isAdmin: false, isLoading: false, + isFirstLoading: false, signup: () => Promise.reject(), login: () => Promise.reject(), loginWithGoogle: () => Promise.reject(), @@ -47,36 +48,79 @@ export const UserContext = createContext({ export const useUserContext = (): iUserContext => { const [user, setUser] = useState(initialUser); const [isLoading, setIsLoading] = useState(false); + const [isFirstLoading, setIsFirstLoading] = useState(false); + const [clientHasPaid] = useState(false); + + const logout = useCallback(async () => { + await GoogleSignin.signOut(); + await removeCache(CACHE_KEYS.authToken); + await removeCache(CACHE_KEYS.refreshToken); + setUser(initialUser); + resetAuthHeader(); + }, []); const getUser = useCallback(async () => { + setIsFirstLoading(true); const token = await getCache(CACHE_KEYS.authToken); if (token) { setAuthHeader(token); - // TODO: fetch user data + const response = await retrieveUserApi(); + if (response.success && response.data) { + setUser(response.data); + } else { + await logout(); + Toast.show({ + type: 'error', + text1: 'Sesi Anda sudah berakhir.', + text2: 'Silakan coba masuk lagi.', + }); + } } - }, []); + setIsFirstLoading(false); + }, [logout]); + + const authSuccess = async (data: LoginResponse) => { + const accessToken = data.access_token; + await setCache(CACHE_KEYS.authToken, accessToken); + await setCache(CACHE_KEYS.refreshToken, data.refresh_token); + setUser(data.user); + setAuthHeader(accessToken); + }; + + const linkUserData = async (email: string) => { + const dietProfileId = await getCache(CACHE_KEYS.dietProfileId); + const cartId = await getCache(CACHE_KEYS.cartId); + if (dietProfileId && cartId) { + const response = await linkUserDataApi({ + email, + diet_profile_id: parseInt(dietProfileId, 10), + cart_id: parseInt(cartId, 10), + }); + return response; + } + return { + success: false, + }; + }; const signup = async (registerData: RegistrationRequest) => { const response = await signupApi(registerData); - await setUserFromResponse(response.success, setUser, response.data); + if (response.success && response.data) { + await authSuccess(response.data); + return await linkUserData(response.data.user.email); + } return response; }; const login = async (loginData: LoginRequest) => { const response = await loginApi(loginData); - await setUserFromResponse(response.success, setUser, response.data); + if (response.success && response.data) { + await authSuccess(response.data); + } return response; }; - const logout = useCallback(async () => { - await GoogleSignin.signOut(); - await removeCache(CACHE_KEYS.authToken); - setUser(initialUser); - resetAuthHeader(); - }, []); - const loginWithGoogle = async () => { - setIsLoading(true); try { await GoogleSignin.hasPlayServices(); await GoogleSignin.signIn(); @@ -85,16 +129,18 @@ export const useUserContext = (): iUserContext => { access_token: tokens.accessToken, }); if (response.success && response.data) { - await setCache(CACHE_KEYS.authToken, response.data.access_token); - await setCache(CACHE_KEYS.authToken, response.data.access_token); - setUser(response.data.user); + await authSuccess(response.data); + const linkResponse = await linkUserData(response.data.user.email); + if (!linkResponse.success) { + await logout(); + Toast.show({ + type: 'error', + text1: 'Gagal masuk dengan Google', + text2: 'Terjadi kesalahan di sisi kami. Silakan coba lagi', + }); + } } else { await logout(); - Toast.show({ - type: 'error', - text1: 'Sesi Anda sudah berakhir.', - text2: 'Silakan coba masuk lagi.', - }); } } catch (error) { console.log(error); @@ -116,7 +162,12 @@ export const useUserContext = (): iUserContext => { return { user, isAuthenticated: user.id !== null, + isUnpaidClient: user.role === UserRole.CLIENT, + isPaidClient: user.role === UserRole.CLIENT && clientHasPaid, + isNutritionist: user.role === UserRole.NUTRITIONIST, + isAdmin: user.role === UserRole.ADMIN, isLoading, + isFirstLoading, signup, login, loginWithGoogle, diff --git a/src/provider/UserContext/types.ts b/src/provider/UserContext/types.ts index 64db05c2a8cbfb50e65d9e723f2626e9c1ee4878..f0008b76b3e415ba608c9f9381dbfd82df5b5578 100644 --- a/src/provider/UserContext/types.ts +++ b/src/provider/UserContext/types.ts @@ -4,13 +4,21 @@ import { LoginResponse, RegistrationRequest, User, + LinkUserDataResponse, } from 'services/auth/models'; export interface iUserContext { user: User; isAuthenticated: boolean; + isUnpaidClient: boolean; + isPaidClient: boolean; + isNutritionist: boolean; + isAdmin: boolean; isLoading: boolean; - signup: (data: RegistrationRequest) => ApiResponse; + isFirstLoading: boolean; + signup: ( + data: RegistrationRequest, + ) => ApiResponse; login: (data: LoginRequest) => ApiResponse; loginWithGoogle: () => Promise; logout: () => Promise; diff --git a/src/provider/index.test.tsx b/src/provider/index.test.tsx index fb235ef96490d63ae6699c677721db0657761ed4..11e2ea067b1cc9ab71e651a98e5e0153a9f6802a 100644 --- a/src/provider/index.test.tsx +++ b/src/provider/index.test.tsx @@ -4,7 +4,7 @@ import { render } from '@testing-library/react-native'; import Provider from '.'; describe('Provider', () => { - it('renders correctly', () => { - render(); + it('renders correctly', async () => { + render(children); }); }); diff --git a/src/scenes/auth/Login/index.test.tsx b/src/scenes/auth/Login/index.test.tsx index be8d4e08b63f7cf928c716f09b59bfeba73a5b5b..8c0ff3346e9bb86c6a0fdd3b39789beccfc6054c 100644 --- a/src/scenes/auth/Login/index.test.tsx +++ b/src/scenes/auth/Login/index.test.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { render, fireEvent, waitFor } from 'utils/testing'; import * as ROUTES from 'constants/routes'; import axios from 'axios'; +import CACHE_KEYS from 'constants/cacheKeys'; +import { setCache } from 'utils/cache'; import Login from '.'; import { @@ -16,8 +18,23 @@ jest.mock('axios'); const mockAxios = axios as jest.Mocked; describe('Login page', () => { - it('renders correctly', () => { - render(, ROUTES.login); + it('shows dietela cover loader when is loading', async () => { + const { queryByText } = render(, ROUTES.login); + + await waitFor(() => + expect(queryByText(/Lanjut dengan Google/i)).toBeFalsy(), + ); + }); + + it('renders correctly if client has filled questionnaire and cart', async () => { + await setCache(CACHE_KEYS.cartId, 1); + await setCache(CACHE_KEYS.dietProfileId, 1); + + const { queryByText } = render(, ROUTES.login); + + await waitFor(() => + expect(queryByText(/Lanjut dengan Google/i)).toBeTruthy(), + ); }); it('success when field is valid and submit success', async () => { @@ -33,6 +50,10 @@ describe('Login page', () => { ROUTES.login, ); + await waitFor(() => + expect(queryByText(/Lanjut dengan Google/i)).toBeTruthy(), + ); + textField.map(({ name, placeholder }) => { const formField = getByPlaceholderText(placeholder as string); fireEvent.changeText(formField, validLoginValues[name]); @@ -40,9 +61,6 @@ describe('Login page', () => { 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 () => { @@ -60,6 +78,10 @@ describe('Login page', () => { ROUTES.login, ); + await waitFor(() => + expect(queryByText(/Lanjut dengan Google/i)).toBeTruthy(), + ); + textField.map(({ name, placeholder }) => { const formField = getByPlaceholderText(placeholder as string); fireEvent.changeText(formField, invalidLoginValues[name]); diff --git a/src/scenes/auth/Login/index.tsx b/src/scenes/auth/Login/index.tsx index d603d5606579aeb73ec45012094f2b8fd41d75ae..f28f7d31ccf2488cecc74d24bd2617a632875beb 100644 --- a/src/scenes/auth/Login/index.tsx +++ b/src/scenes/auth/Login/index.tsx @@ -4,7 +4,7 @@ import { UserContext } from 'provider'; import { useAuthEffect, useForm } from 'hooks'; import { GoogleLoginButton } from '../components'; -import { BigButton, Toast } from 'components/core'; +import { BigButton, Toast, Loader } from 'components/core'; import { fieldValidation, initialValues, textField } from './schema'; import { generateValidationSchema } from 'utils/form'; @@ -45,8 +45,11 @@ const Login: FC = () => { }, }); - useAuthEffect(); + const isProcessing = useAuthEffect(true); + if (isProcessing) { + return ; + } return ( {textField.map(({ name, label, required, placeholder }, i) => ( diff --git a/src/scenes/auth/ManualRegistrationPage/index.test.tsx b/src/scenes/auth/ManualRegistrationPage/index.test.tsx index 1eee47717547c622751f28b71caa94b586c4a238..05ef33f5f04d0e5b4790716adf82f4232d00ee12 100644 --- a/src/scenes/auth/ManualRegistrationPage/index.test.tsx +++ b/src/scenes/auth/ManualRegistrationPage/index.test.tsx @@ -28,7 +28,7 @@ describe('ManualRegistrationPage', () => { }); mockAxios.request.mockImplementationOnce(signupApi); - const { getByPlaceholderText, queryByText, getByTestId } = render( + const { getByPlaceholderText, getByTestId } = render( , ROUTES.registration, ); @@ -40,9 +40,6 @@ describe('ManualRegistrationPage', () => { const submitButton = getByTestId('submitButton'); await waitFor(() => fireEvent.press(submitButton)); - - const toastWarning = queryByText(/Profile/i); - expect(toastWarning).toBeTruthy(); }); it('fails when field is valid and submit fails', async () => { @@ -50,7 +47,7 @@ describe('ManualRegistrationPage', () => { Promise.reject({ status: 400, response: { - error: 'error', + data: 'error', }, }); mockAxios.request.mockImplementationOnce(signupApi); @@ -68,7 +65,7 @@ describe('ManualRegistrationPage', () => { const submitButton = getByTestId('submitButton'); await waitFor(() => fireEvent.press(submitButton)); - const nextPageText = queryByText(/Profile/i); + const nextPageText = queryByText(/Checkout/i); expect(nextPageText).toBeFalsy(); }); @@ -79,7 +76,7 @@ describe('ManualRegistrationPage', () => { Promise.reject({ status: 400, response: { - error: { + data: { name: 'Wrong name', email: alreadyRegistered, password1: 'Wrong password', @@ -102,7 +99,7 @@ describe('ManualRegistrationPage', () => { const submitButton = getByTestId('submitButton'); await waitFor(() => fireEvent.press(submitButton)); - const nextPageText = queryByText(/Profile/i); + const nextPageText = queryByText(/Checkout/i); expect(nextPageText).toBeFalsy(); }); }); diff --git a/src/scenes/cart/Checkout/index.tsx b/src/scenes/cart/Checkout/index.tsx index bca12b8f080983ff579d2bf35f2f9a815e19bd4d..9fceb157d51b40f80fe70c0d9b64f247e525d7b1 100644 --- a/src/scenes/cart/Checkout/index.tsx +++ b/src/scenes/cart/Checkout/index.tsx @@ -26,7 +26,7 @@ const Checkout: FC = () => { const { isLoading, data } = useApi(fetchCart); - useAuthGuardEffect(); + useAuthGuardEffect(true); if (isLoading) { return ; diff --git a/src/scenes/cart/ChoosePlan/index.tsx b/src/scenes/cart/ChoosePlan/index.tsx index 8807a4566621f73a7bd007e0fdc79b89d847208a..2c7da9fad0e85649c6c1e99f6418a7a1c58491d6 100644 --- a/src/scenes/cart/ChoosePlan/index.tsx +++ b/src/scenes/cart/ChoosePlan/index.tsx @@ -25,7 +25,7 @@ const ChoosePlan: FC = () => { const [currentPage, setCurrentPage] = useState(1); const [isSubmitting, setIsSubmitting] = useState(false); const [values, setValues] = useState(initialValues); - const [programs, setPrograms] = useState(defaultProgramRecommendations); + const [programs, setPrograms] = useState(undefined); const handleSubmit = async () => { setIsSubmitting(true); @@ -73,7 +73,7 @@ const ChoosePlan: FC = () => { retrieveNutritionistsApi, ); - if (isLoading) { + if (isLoading || programs === undefined) { return ; } return ( diff --git a/src/scenes/common/InitialPage/index.test.tsx b/src/scenes/common/InitialPage/index.test.tsx index 952fc9528c8e918bfe6184fe8069d42af416d95f..9d43f65059a09aaa931ba3a7fae85f220d70f51c 100644 --- a/src/scenes/common/InitialPage/index.test.tsx +++ b/src/scenes/common/InitialPage/index.test.tsx @@ -1,54 +1,46 @@ import React from 'react'; -import { fireEvent, render } from '@testing-library/react-native'; +import { render, fireEvent, waitFor } from 'utils/testing'; import * as ROUTES from 'constants/routes'; import InitialPage from '.'; -const createTestProps = (props: Object) => ({ - navigation: { - navigate: jest.fn(), - }, - ...props, -}); - describe('InitialPage', () => { - it('renders correctly', () => { - render(); + test('shows dietela cover loader when is loading', async () => { + const { queryByTestId } = render(, ROUTES.initial, { + isFirstLoading: true, + }); + + expect(queryByTestId('background')).toBeFalsy(); }); test('has background image', () => { - const { queryByTestId } = render(); + const { queryByTestId } = render(, ROUTES.initial); expect(queryByTestId('background')).toBeTruthy(); }); test('has Dietela logo', () => { - const { queryByTestId } = render(); + const { queryByTestId } = render(, ROUTES.initial); expect(queryByTestId('logo')).toBeTruthy(); }); test('has call-to-action button that navigates to Dietela Quiz', () => { - let props = createTestProps({}); - const { getByText } = render(); - - const button = getByText(/konsultasi sekarang/i); - fireEvent.press(button); + const { getByText, queryByText } = render(, ROUTES.initial); + expect(queryByText(/konsultasi sekarang/i)).toBeTruthy(); + fireEvent.press(getByText(/konsultasi sekarang/i)); - expect(button).toBeTruthy(); - expect(props.navigation.navigate).toHaveBeenCalledWith( - ROUTES.allAccessQuestionnaire, - ); + expect(queryByText(/Dietela Quiz/i)).toBeTruthy(); }); - test('has link button that navigates to Login Page', () => { - let props = createTestProps({}); - const { getByText } = render(); - - const link = getByText(/Login disini/i); - fireEvent.press(link); + test('has link button that navigates to Login Page', async () => { + const { getByText, queryByText, queryAllByText } = render( + , + ROUTES.initial, + ); + expect(queryByText(/Login disini/i)).toBeTruthy(); + await waitFor(() => fireEvent.press(getByText(/Login disini/i))); - expect(link).toBeTruthy(); - expect(props.navigation.navigate).toHaveBeenCalledWith(ROUTES.login); + expect(queryAllByText(/Login/i)).toBeTruthy(); }); }); diff --git a/src/scenes/common/InitialPage/index.tsx b/src/scenes/common/InitialPage/index.tsx index cc49653b62508f2a1ab7ba8a90603792761d69a7..58cad2d0a51243a566695621f70b43857f9a686c 100644 --- a/src/scenes/common/InitialPage/index.tsx +++ b/src/scenes/common/InitialPage/index.tsx @@ -1,46 +1,57 @@ -import React, { FC } from 'react'; - +import React, { FC, useContext } from 'react'; +import { useNavigation } from '@react-navigation/native'; import { View, Text, ImageBackground, Image } from 'react-native'; -import { BigButton, Link } from 'components/core'; +import { BigButton, Link, DietelaCoverLoader } from 'components/core'; +import { banner_girl_eating, logo_white_small } from 'assets/images'; +import { useAuthEffect } from 'hooks'; import * as ROUTES from 'constants/routes'; - +import { UserContext } from 'provider'; import { layoutStyles, typographyStyles } from 'styles'; + import { styles } from './styles'; -import { banner_girl_eating, logo_white_small } from 'assets/images'; +const InitialPage: FC = () => { + const navigation = useNavigation(); + const { isFirstLoading } = useContext(UserContext); -const InitialPage: FC = ({ navigation }) => ( - - - - - - Online Nutritionist Pertama di Indonesia - - - Hadir untuk mendefinisikan ulang kata “Diet” untuk Anda! - - - Apapun masalah diet Anda, konsultasikan bersama kami! - - - - navigation.navigate(ROUTES.allAccessQuestionnaire)} - /> - navigation.navigate(ROUTES.login)} - /> + useAuthEffect(); + + if (isFirstLoading) { + return ; + } + return ( + + + + + + Online Nutritionist Pertama di Indonesia + + + Hadir untuk mendefinisikan ulang kata “Diet” untuk Anda! + + + Apapun masalah diet Anda, konsultasikan bersama kami! + + + + navigation.navigate(ROUTES.allAccessQuestionnaire)} + /> + navigation.navigate(ROUTES.login)} + /> + - - -); + + ); +}; export default InitialPage; diff --git a/src/services/auth/index.ts b/src/services/auth/index.ts index ff598c35840338ac7452432177b9b2b1d652425a..e212d20d490fab191f68ee4829db4ae61823e40f 100644 --- a/src/services/auth/index.ts +++ b/src/services/auth/index.ts @@ -6,6 +6,9 @@ import { LoginRequest, LoginResponse, RegistrationRequest, + User, + LinkUserDataRequest, + LinkUserDataResponse, } from './models'; export const googleLoginApi = ( @@ -23,3 +26,13 @@ export const signupApi = ( export const loginApi = (body: LoginRequest): ApiResponse => { return api(RequestMethod.POST, apiUrls.login, body); }; + +export const retrieveUserApi = (): ApiResponse => { + return api(RequestMethod.GET, apiUrls.user); +}; + +export const linkUserDataApi = ( + body: LinkUserDataRequest, +): ApiResponse => { + return api(RequestMethod.POST, apiUrls.linkData, body); +}; diff --git a/src/services/auth/models.ts b/src/services/auth/models.ts index 6c24a9a0c55c5709bc2ed7aa7cd516df19b243ea..7b1edd7a5e3523d530eea1ff665395d0b941129b 100644 --- a/src/services/auth/models.ts +++ b/src/services/auth/models.ts @@ -1,3 +1,6 @@ +import { DietProfileResponse } from 'services/dietelaQuiz/models'; +import { CartResponse } from 'services/payment/models'; + export interface GoogleLoginRequest { access_token: string; } @@ -9,18 +12,23 @@ export interface RegistrationRequest { password2: string; } -export type Role = 'client' | 'nutritionist' | 'admin'; - -export interface LoginRequest { - email: string; - password: string; - role: Role; +export enum UserRole { + CLIENT = 'client', + NUTRITIONIST = 'nutritionist', + ADMIN = 'admin', } export interface User { id: number | null; email: string; name: string; + role: UserRole | null; +} + +export interface LoginRequest { + email: string; + password: string; + role: UserRole; } export interface LoginResponse { @@ -28,3 +36,15 @@ export interface LoginResponse { refresh_token: string; user: User; } + +export interface LinkUserDataRequest { + email: string; + diet_profile_id: number; + cart_id: number; +} + +export interface LinkUserDataResponse { + user: User; + diet_profile: DietProfileResponse; + cart: CartResponse; +} diff --git a/src/services/auth/urls.ts b/src/services/auth/urls.ts index 039677ed29a3fc6ab51adf850977d68f3239e87c..31f21fa0935ba5b43151740a367f3a1b70486350 100644 --- a/src/services/auth/urls.ts +++ b/src/services/auth/urls.ts @@ -3,3 +3,6 @@ const auth = 'auth/'; export const google = `${auth}google/`; export const signup = `${auth}registration/`; export const login = `${auth}user-login/`; + +export const user = `${auth}user/`; +export const linkData = `${auth}link-data/`; diff --git a/src/utils/testing.tsx b/src/utils/testing.tsx index 6d9a987472526f6ed99db1131435e76a232ec518..0fd9eaa07cdc8b36a28f2e31179070f5b0d2a2e0 100644 --- a/src/utils/testing.tsx +++ b/src/utils/testing.tsx @@ -4,8 +4,8 @@ import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import { navigation } from 'constants/navigation'; -import ContextProvider from 'provider'; -import { UserContext, useUserContext } from 'provider/UserContext'; +import { UserContext } from 'provider'; +import { UserRole } from 'services/auth/models'; const Stack = createStackNavigator(); @@ -16,34 +16,44 @@ interface TestProvider { children: ReactElement; } -const WithAuthProvider: FC = ({ children }) => { - const user = useUserContext(); - - return ( - - {children} - - ); -}; - const TestProvider: FC = ({ route, params, isAuth, children, }) => { - const Provider = isAuth ? WithAuthProvider : ContextProvider; + const user = isAuth + ? { + id: 1, + email: 'user.test@gmail.com', + name: 'User Test', + role: UserRole.CLIENT, + } + : { + id: null, + email: '', + name: '', + role: null, + }; + + const mockFn = (_?: any) => Promise.reject(); + return ( - + {navigation.map((nav, i) => ( @@ -60,7 +70,7 @@ const TestProvider: FC = ({ ))} - + ); };