Fakultas Ilmu Komputer UI

Commit 48cdfb25 authored by Wulan Mantiri's avatar Wulan Mantiri Committed by Muzaki Azami
Browse files

Implement Checkout page layout and integrate with API

parent 16ed516f
...@@ -3,4 +3,5 @@ export default { ...@@ -3,4 +3,5 @@ export default {
cartId: 'CART_ID', cartId: 'CART_ID',
authToken: 'AUTH_TOKEN', authToken: 'AUTH_TOKEN',
refreshToken: 'REFRESH_TOKEN', refreshToken: 'REFRESH_TOKEN',
programRecommendations: 'PROGRAM_RECOMMENDATIONS',
}; };
import * as ROUTES from 'constants/routes'; import * as ROUTES from 'constants/routes';
import { import {
AllAccessQuestionnaire, AllAccessQuestionnaire,
Checkout,
ChoosePlan, ChoosePlan,
ComingSoonPage, ComingSoonPage,
DietelaQuizResult, DietelaQuizResult,
...@@ -38,9 +39,9 @@ export const navigation: NavRoute[] = [ ...@@ -38,9 +39,9 @@ export const navigation: NavRoute[] = [
header: 'Choose Plan', header: 'Choose Plan',
}, },
{ {
name: ROUTES.cart, name: ROUTES.checkout,
component: ComingSoonPage, component: Checkout,
header: 'Cart', header: 'Checkout',
}, },
{ {
name: ROUTES.programDetail, name: ROUTES.programDetail,
......
...@@ -5,10 +5,10 @@ const questionnaire = 'questionnaire'; ...@@ -5,10 +5,10 @@ const questionnaire = 'questionnaire';
export const allAccessQuestionnaire = `${questionnaire}/all-access`; export const allAccessQuestionnaire = `${questionnaire}/all-access`;
export const dietelaQuizResult = `${questionnaire}/dietela-quiz-result`; export const dietelaQuizResult = `${questionnaire}/dietela-quiz-result`;
export const cart = 'cart'; export const checkout = 'checkout';
export const choosePlan = `${cart}/choose-plan`; export const choosePlan = `${checkout}/choose-plan`;
export const programDetail = 'dietela-program'; export const programDetail = `${checkout}/dietela-program`;
export const nutritionistDetail = 'nutritionist'; export const nutritionistDetail = `${checkout}/nutritionist`;
export const registration = 'registration'; export const registration = 'registration';
export const login = 'login'; export const login = 'login';
......
export { default as useApi } from './useApi'; export { default as useApi } from './useApi';
export { default as useAuthEffect } from './useAuthEffect'; export { default as useAuthEffect } from './useAuthEffect';
export { default as useAuthGuardEffect } from './useAuthGuardEffect';
export { default as useForm } from './useForm'; export { default as useForm } from './useForm';
import { useContext, useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';
import { UserContext } from 'provider';
import * as ROUTES from 'constants/routes';
const useAuthGuardEffect = () => {
const { isAuthenticated } = useContext(UserContext);
const navigation = useNavigation();
useEffect(() => {
if (!isAuthenticated) {
navigation.navigate(ROUTES.login);
}
}, [isAuthenticated, navigation]);
};
export default useAuthGuardEffect;
import React from 'react';
import { render } from '@testing-library/react-native';
import CheckoutCard from '.';
describe('CheckoutCard component', () => {
it('for program renders correctly', () => {
render(
<CheckoutCard type="program" content="yes" onReadMore={jest.fn()} />,
);
});
it('for nutritionist renders correctly', () => {
render(
<CheckoutCard type="nutritionist" content="yes" onReadMore={jest.fn()} />,
);
});
});
import React, { FC } from 'react';
import { View } from 'react-native';
import { Text, Button } from 'react-native-elements';
import { Props } from './types';
import { styles } from './styles';
import { typographyStyles, colors } from 'styles';
const CheckoutCard: FC<Props> = ({ content, type, onReadMore }) => (
<View style={styles.container}>
<Text style={typographyStyles.overlineBig}>
Pilihan {type === 'program' ? 'Program' : 'Nutrisionis'} Anda
</Text>
<Text style={styles.title}>
{type === 'program' ? '📝 ' : '👩🏻‍⚕️ '}
{content}
</Text>
<Button
title="Baca selengkapnya"
type="clear"
icon={{
name: 'arrow-forward',
size: 22,
color: colors.primary,
}}
onPress={onReadMore}
iconRight
titleStyle={styles.readMore}
buttonStyle={styles.buttonStyle}
/>
</View>
);
export default CheckoutCard;
import { StyleSheet } from 'react-native';
import { colors, typography } from 'styles';
export const styles = StyleSheet.create({
container: {
borderWidth: 2,
borderRadius: 6,
padding: 20,
paddingBottom: 15,
marginTop: 20,
backgroundColor: colors.primaryYellow,
borderColor: colors.primaryVariant,
},
title: {
...typography.headingMedium,
marginVertical: 15,
},
readMore: {
color: colors.primary,
},
buttonStyle: {
margin: 0,
padding: 0,
},
});
export interface Props {
type: 'program' | 'nutritionist';
content: string;
onReadMore: () => void;
}
export { default as CheckoutCard } from './CheckoutCard';
import React from 'react';
import { withAuthRender, waitFor, fireEvent } from 'utils/testing';
import axios from 'axios';
import Checkout from '.';
import * as ROUTES from 'constants/routes';
import CACHE_KEYS from 'constants/cacheKeys';
import { DietelaProgram } from 'services/dietelaQuiz/quizResult';
import { setCache } from 'utils/cache';
import { mockProgramRecommendations } from '__mocks__/quizResult';
jest.mock('axios');
const mockAxios = axios as jest.Mocked<typeof axios>;
describe('Checkout', () => {
const nutritionist = {
id: 1,
full_name_and_degree: 'Wendy',
registration_certificate_no: '123',
university: 'UI',
mastered_nutritional_problems: 'diet',
handled_age_group: '18',
another_practice_place: '-',
languages: 'English',
};
const retrieveCartApi = () =>
Promise.resolve({
status: 200,
data: {
id: 1,
program: {
id: 1,
unique_code: DietelaProgram.TRIAL,
},
nutritionist,
},
});
it('redirects to program detail page when user clicks Baca selengkapnya button for program', async () => {
await setCache(CACHE_KEYS.cartId, 1);
mockAxios.request.mockImplementationOnce(retrieveCartApi);
const { getAllByText, getByText } = withAuthRender(
<Checkout />,
ROUTES.checkout,
);
await waitFor(() => expect(mockAxios.request).toBeCalled());
const chosenProgram = getByText(/One Week Trial/i);
expect(chosenProgram).toBeTruthy();
const readMoreButton = getAllByText(/Baca selengkapnya/i)[0];
expect(readMoreButton).toBeTruthy();
fireEvent.press(readMoreButton);
const programDetailPage = getByText(/Program Dietela/i);
expect(programDetailPage).toBeTruthy();
});
it('redirects to nutritionist detail page when user clicks Baca selengkapnya button for nutritionist', async () => {
mockAxios.request.mockImplementationOnce(retrieveCartApi);
const { getAllByText, getByText } = withAuthRender(
<Checkout />,
ROUTES.checkout,
);
await waitFor(() => expect(mockAxios.request).toBeCalled());
const chosenNutritionist = getByText(/Wendy/i);
expect(chosenNutritionist).toBeTruthy();
const readMoreButton = getAllByText(/Baca selengkapnya/i)[1];
expect(readMoreButton).toBeTruthy();
fireEvent.press(readMoreButton);
const nutritionistDetailPage = getByText(/Coming Soon/i);
expect(nutritionistDetailPage).toBeTruthy();
});
it('redirects to choose plan page when user clicks Ganti Pilihan button', async () => {
await setCache(
CACHE_KEYS.programRecommendations,
JSON.stringify(mockProgramRecommendations),
);
mockAxios.request.mockImplementationOnce(retrieveCartApi);
const { getByText } = withAuthRender(<Checkout />, ROUTES.checkout);
await waitFor(() => expect(mockAxios.request).toBeCalled());
const changePlanButton = getByText(/ganti pilihan/i);
expect(changePlanButton).toBeTruthy();
await waitFor(() => fireEvent.press(changePlanButton));
const choosePlanPage = getByText(/Choose Plan/i);
expect(choosePlanPage).toBeTruthy();
});
afterAll(() => {
jest.clearAllMocks();
});
});
import React, { FC, useCallback } from 'react';
import { View } from 'react-native';
import { Text, Button } from 'react-native-elements';
import { useNavigation } from '@react-navigation/native';
import { Loader, BigButton } from 'components/core';
import { Section } from 'components/layout';
import CACHE_KEYS from 'constants/cacheKeys';
import * as ROUTES from 'constants/routes';
import { dietPrograms } from 'constants/dietelaProgram';
import { useApi, useAuthGuardEffect } from 'hooks';
import { retrieveCartApi } from 'services/payment';
import { getCache } from 'utils/cache';
import { typographyStyles } from 'styles';
import { styles } from './styles';
import { CheckoutCard } from './components';
const Checkout: FC = () => {
const navigation = useNavigation();
const fetchCart = useCallback(async () => {
const cartId = await getCache(CACHE_KEYS.cartId);
return await retrieveCartApi(cartId);
}, []);
const { isLoading, data } = useApi(fetchCart);
useAuthGuardEffect();
if (isLoading) {
return <Loader />;
}
return (
<View style={styles.container}>
<View>
<CheckoutCard
content={data ? dietPrograms[data.program.unique_code].title : '-'}
type="program"
onReadMore={() =>
navigation.navigate(ROUTES.programDetail, {
id: data?.program.unique_code,
})
}
/>
<CheckoutCard
content={data ? data.nutritionist.full_name_and_degree : '-'}
type="nutritionist"
onReadMore={() =>
navigation.navigate(ROUTES.nutritionistDetail, {
id: data?.nutritionist.id,
})
}
/>
</View>
<View style={styles.priceContainer}>
<Text style={typographyStyles.headingMedium}>Harga:</Text>
{data ? (
<View style={styles.currencyContainer}>
<Text style={styles.currency}>Rp</Text>
<Text style={styles.basePrice}>
{dietPrograms[data.program.unique_code].price}
</Text>
</View>
) : (
<Text>-</Text>
)}
</View>
<View>
<Button
title="ganti pilihan"
type="outline"
onPress={() => navigation.navigate(ROUTES.choosePlan)}
buttonStyle={styles.buttonStyle}
titleStyle={[typographyStyles.overlineBig, styles.titleStyle]}
/>
<Section>
<BigButton title="bayar dengan midtrans" onPress={console.log} />
</Section>
</View>
</View>
);
};
export default Checkout;
import { StyleSheet } from 'react-native';
import { layoutStyles, typography, colors } from 'styles';
export const styles = StyleSheet.create({
container: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
flexGrow: 1,
...layoutStyles,
marginBottom: 10,
},
topContainer: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
},
priceContainer: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
currencyContainer: {
display: 'flex',
flexDirection: 'row',
},
currency: {
...typography.bodyMedium,
color: colors.formLabel,
marginRight: 8,
marginTop: 8,
},
basePrice: {
...typography.displayMediumMontserrat,
color: colors.primary,
},
buttonStyle: {
borderColor: colors.buttonYellow,
borderWidth: 1,
},
titleStyle: {
color: colors.textBlack,
},
});
...@@ -59,7 +59,7 @@ const PricingCard: FC<Props> = ({ ...@@ -59,7 +59,7 @@ const PricingCard: FC<Props> = ({
title="Baca selengkapnya" title="Baca selengkapnya"
type="clear" type="clear"
icon={{ icon={{
name: 'keyboard-arrow-right', name: 'arrow-forward',
size: 25, size: 25,
color: colors.primaryVariant, color: colors.primaryVariant,
}} }}
......
...@@ -2,11 +2,13 @@ import React from 'react'; ...@@ -2,11 +2,13 @@ import React from 'react';
import { render, fireEvent, waitFor } from 'utils/testing'; import { render, fireEvent, waitFor } from 'utils/testing';
import axios from 'axios'; import axios from 'axios';
import * as ROUTES from 'constants/routes';
import ChoosePlan from '.'; import ChoosePlan from '.';
import * as ROUTES from 'constants/routes';
import CACHE_KEYS from 'constants/cacheKeys';
import { mockProgramRecommendations } from '__mocks__/quizResult'; import { mockProgramRecommendations } from '__mocks__/quizResult';
import { dietPrograms } from 'constants/dietelaProgram'; import { dietPrograms } from 'constants/dietelaProgram';
import { defaultProgramRecommendations } from 'constants/dietelaProgram'; import { setCache } from 'utils/cache';
jest.mock('axios'); jest.mock('axios');
const mockAxios = axios as jest.Mocked<typeof axios>; const mockAxios = axios as jest.Mocked<typeof axios>;
...@@ -30,7 +32,7 @@ describe('ChoosePlan', () => { ...@@ -30,7 +32,7 @@ describe('ChoosePlan', () => {
data: nutritionists, data: nutritionists,
}); });
it('provides default program recommendations if not passed', async () => { it('provides default program recommendations if not in cache', async () => {
mockAxios.request.mockImplementationOnce(retrieveNutritionistsApi); mockAxios.request.mockImplementationOnce(retrieveNutritionistsApi);
const { getAllByText } = render(<ChoosePlan />, ROUTES.choosePlan); const { getAllByText } = render(<ChoosePlan />, ROUTES.choosePlan);
...@@ -41,6 +43,10 @@ describe('ChoosePlan', () => { ...@@ -41,6 +43,10 @@ describe('ChoosePlan', () => {
}); });
it('redirects to cart page when user clicks Bayar button', async () => { it('redirects to cart page when user clicks Bayar button', async () => {
await setCache(
CACHE_KEYS.programRecommendations,
JSON.stringify(mockProgramRecommendations),
);
const createCartApi = () => const createCartApi = () =>
Promise.resolve({ Promise.resolve({
status: 200, status: 200,
...@@ -53,10 +59,9 @@ describe('ChoosePlan', () => { ...@@ -53,10 +59,9 @@ describe('ChoosePlan', () => {
mockAxios.request.mockImplementationOnce(retrieveNutritionistsApi); mockAxios.request.mockImplementationOnce(retrieveNutritionistsApi);
const { getByText, queryByText } = render( const { getByText, queryAllByText } = render(
<ChoosePlan />, <ChoosePlan />,
ROUTES.choosePlan, ROUTES.choosePlan,
defaultProgramRecommendations,
); );
await waitFor(() => expect(mockAxios.request).toBeCalled()); await waitFor(() => expect(mockAxios.request).toBeCalled());
...@@ -78,15 +83,17 @@ describe('ChoosePlan', () => { ...@@ -78,15 +83,17 @@ describe('ChoosePlan', () => {
expect(payButton).toBeTruthy(); expect(payButton).toBeTruthy();
await waitFor(() => fireEvent.press(payButton)); await waitFor(() => fireEvent.press(payButton));
const cartPage = queryByText(/Coming Soon/i); const checkoutPage = queryAllByText(/Checkout/i);
expect(cartPage).toBeTruthy(); expect(checkoutPage).toBeTruthy();
}); });
it('does not redirect to cart page when submit fails', async () => { it('does not redirect to cart page when submit fails', async () => {
const createCartApi = () => const createCartApi = () =>
Promise.reject({ Promise.reject({
status: 400, status: 400,
data: 'error', response: {
data: 'error',
},
}); });
mockAxios.request.mockImplementationOnce(retrieveNutritionistsApi); mockAxios.request.mockImplementationOnce(retrieveNutritionistsApi);
...@@ -94,7 +101,6 @@ describe('ChoosePlan', () => { ...@@ -94,7 +101,6 @@ describe('ChoosePlan', () => {
const { getByText, queryByText } = render( const { getByText, queryByText } = render(
<ChoosePlan />, <ChoosePlan />,
ROUTES.choosePlan, ROUTES.choosePlan,
defaultProgramRecommendations,
); );
await waitFor(() => expect(mockAxios.request).toBeCalled()); await waitFor(() => expect(mockAxios.request).toBeCalled());
...@@ -116,8 +122,8 @@ describe('ChoosePlan', () => { ...@@ -116,8 +122,8 @@ describe('ChoosePlan', () => {
expect(payButton).toBeTruthy(); expect(payButton).toBeTruthy();
await waitFor(() => fireEvent.press(payButton)); await waitFor(() => fireEvent.press(payButton));
const cartPage = queryByText(/Coming Soon/i); const checkoutPage = queryByText(/Checkout/i);
expect(cartPage).toBeFalsy(); expect(checkoutPage).toBeFalsy();
}); });
it('redirects to program detail page when user clicks Baca selengkapnya button for program', async () => { it('redirects to program detail page when user clicks Baca selengkapnya button for program', async () => {
...@@ -126,7 +132,6 @@ describe('ChoosePlan', () => { ...@@ -126,7 +132,6 @@ describe('ChoosePlan', () => {
const { getAllByText, getByText } = render( const { getAllByText, getByText } = render(
<ChoosePlan />, <ChoosePlan />,
ROUTES.choosePlan, ROUTES.choosePlan,
defaultProgramRecommendations,
); );
await waitFor(() => expect(mockAxios.request).toBeCalled()); await waitFor(() => expect(mockAxios.request).toBeCalled());
...@@ -147,7 +152,6 @@ describe('ChoosePlan', () => { ...@@ -147,7 +152,6 @@ describe('ChoosePlan', () => {
const { getAllByText, getByText } = render( const { getAllByText, getByText } = render(
<ChoosePlan />, <ChoosePlan />,
ROUTES.choosePlan, ROUTES.choosePlan,
defaultProgramRecommendations,
); );
await waitFor(() => expect(mockAxios.request).toBeCalled()); await waitFor(() => expect(mockAxios.request).toBeCalled());
......
import React, { FC, useState } from 'react'; import React, { FC, useState, useCallback, useEffect } from 'react';
import { ScrollView } from 'react-native'; import { ScrollView } from 'react-native';
import { useNavigation, useRoute } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';