Fakultas Ilmu Komputer UI

Commit d401fdd6 authored by Wulan Mantiri's avatar Wulan Mantiri
Browse files

Merge branch 'PBI-8-payment_inapp_logic' into 'staging'

Add payment webview integration, fix auth logic

See merge request !53
parents c73e2c4a 5ac1834d
Pipeline #77693 passed with stages
in 48 minutes and 6 seconds
...@@ -39,6 +39,7 @@ test: ...@@ -39,6 +39,7 @@ test:
- yarn install - yarn install
- yarn test --silent - yarn test --silent
artifacts: artifacts:
expire_in: 1 week
paths: paths:
- coverage - coverage
...@@ -84,9 +85,10 @@ android: ...@@ -84,9 +85,10 @@ android:
- yarn install - yarn install
- export ANDROID_SDK_ROOT=/usr/lib/android-sdk - export ANDROID_SDK_ROOT=/usr/lib/android-sdk
- cd android - cd android
- chmod +x gradlew && ./gradlew assembleRelease - chmod +x gradlew && ./gradlew clean && ./gradlew assembleRelease
- cd .. && cp android/app/build/outputs/apk/release/app-release.apk $CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.apk - cd .. && cp android/app/build/outputs/apk/release/app-release.apk $CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.apk
artifacts: artifacts:
expire_in: 1 week
name: '$CI_PROJECT_NAME-$CI_COMMIT_REF_NAME' name: '$CI_PROJECT_NAME-$CI_COMMIT_REF_NAME'
paths: paths:
- $CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.apk - $CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.apk
......
...@@ -30,20 +30,18 @@ const NavigationStack: FC = () => { ...@@ -30,20 +30,18 @@ const NavigationStack: FC = () => {
<Stack.Navigator <Stack.Navigator
initialRouteName={initialRoute} initialRouteName={initialRoute}
screenOptions={screenOptions}> screenOptions={screenOptions}>
<> {navigation.map((nav, i) => (
{navigation.map((nav, i) => ( <Stack.Screen
<Stack.Screen key={`nav${i}`}
key={`nav${i}`} name={nav.name}
name={nav.name} component={nav.component}
component={nav.component} options={{
options={{ title: nav.header,
title: nav.header, headerShown: Boolean(nav.header),
headerShown: Boolean(nav.header), headerRight: LogoutButton,
headerRight: LogoutButton, }}
}} />
/> ))}
))}
</>
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>
); );
......
import * as ROUTES from 'constants/routes'; import * as ROUTES from 'constants/routes';
import { import {
publicNavigation, publicNavigation,
unpaidClientNavigation,
clientNavigation, clientNavigation,
nutritionistNavigation, nutritionistNavigation,
} from 'constants/navigation'; } from 'constants/navigation';
...@@ -14,19 +13,20 @@ export const getNavigation = ( ...@@ -14,19 +13,20 @@ export const getNavigation = (
) => { ) => {
if (isAuthenticated) { if (isAuthenticated) {
if (user.role === UserRole.CLIENT) { if (user.role === UserRole.CLIENT) {
if (user.transaction_status === TransactionStatus.UNPAID) { let initialRoute = ROUTES.clientProfile;
return { if ([TransactionStatus.UNPAID, null].includes(user.transaction_status)) {
initialRoute: ROUTES.checkout, initialRoute = ROUTES.checkout;
navigation: unpaidClientNavigation, } else if (user.transaction_status === TransactionStatus.PENDING) {
}; initialRoute = ROUTES.paymentResult;
} else if (!user.is_finished_onboarding) {
initialRoute = ROUTES.extendedQuestionnaire;
} }
return { return {
initialRoute: user.is_finished_onboarding initialRoute,
? ROUTES.clientProfile
: ROUTES.extendedQuestionnaire,
navigation: clientNavigation, navigation: clientNavigation,
}; };
} }
if (user.role === UserRole.NUTRITIONIST) { if (user.role === UserRole.NUTRITIONIST) {
return { return {
initialRoute: ROUTES.clientListForNutritionist, initialRoute: ROUTES.clientListForNutritionist,
...@@ -34,6 +34,7 @@ export const getNavigation = ( ...@@ -34,6 +34,7 @@ export const getNavigation = (
}; };
} }
} }
return { return {
initialRoute: ROUTES.initial, initialRoute: ROUTES.initial,
navigation: publicNavigation, navigation: publicNavigation,
......
...@@ -10,10 +10,10 @@ import { ...@@ -10,10 +10,10 @@ import {
} from './programDetail'; } from './programDetail';
const prices = { const prices = {
oneWeek: '239,900', oneWeek: '239.900',
oneMonth: '609,900', oneMonth: '609.900',
threeMonths: '1,659,900', threeMonths: '1.659.900',
sixMonths: '3,449,000', sixMonths: '3.449.000',
}; };
export const dietPrograms = { export const dietPrograms = {
......
...@@ -28,6 +28,7 @@ import { ...@@ -28,6 +28,7 @@ import {
ComingSoonPage, ComingSoonPage,
ClientProfile, ClientProfile,
ClientProfileForAdmin, ClientProfileForAdmin,
PaymentWebView,
} from 'scenes'; } from 'scenes';
import { FC } from 'react'; import { FC } from 'react';
...@@ -88,26 +89,22 @@ export const publicNavigation: NavRoute[] = [ ...@@ -88,26 +89,22 @@ export const publicNavigation: NavRoute[] = [
}, },
]; ];
export const unpaidClientNavigation: NavRoute[] = [ export const clientNavigation: NavRoute[] = [
{ {
name: ROUTES.checkout, name: ROUTES.checkout,
component: Checkout, component: Checkout,
header: 'Checkout', header: 'Checkout',
}, },
{ {
name: ROUTES.paymentResult, name: ROUTES.payment,
component: PaymentResult, component: PaymentWebView,
header: 'Pembayaran', header: 'Pembayaran',
}, },
...navigation,
];
export const clientNavigation: NavRoute[] = [
{ {
name: ROUTES.clientProfile, name: ROUTES.paymentResult,
component: ClientProfile, component: PaymentResult,
header: 'Profil Saya',
}, },
...navigation,
{ {
name: ROUTES.extendedQuestionnaire, name: ROUTES.extendedQuestionnaire,
component: ExtendedQuestionnaire, component: ExtendedQuestionnaire,
...@@ -142,6 +139,11 @@ export const clientNavigation: NavRoute[] = [ ...@@ -142,6 +139,11 @@ export const clientNavigation: NavRoute[] = [
...nav, ...nav,
name: ROUTES.extendedQuestionnaireById(id), name: ROUTES.extendedQuestionnaireById(id),
})), })),
{
name: ROUTES.clientProfile,
component: ClientProfile,
header: 'Profil Saya',
},
]; ];
export const nutritionistNavigation: NavRoute[] = [ export const nutritionistNavigation: NavRoute[] = [
...@@ -176,7 +178,6 @@ export const adminNavigation: NavRoute[] = [ ...@@ -176,7 +178,6 @@ export const adminNavigation: NavRoute[] = [
]; ];
export const testNavigation: NavRoute[] = [ export const testNavigation: NavRoute[] = [
...navigation,
...clientNavigation, ...clientNavigation,
...nutritionistNavigation, ...nutritionistNavigation,
{ {
...@@ -198,14 +199,4 @@ export const testNavigation: NavRoute[] = [ ...@@ -198,14 +199,4 @@ export const testNavigation: NavRoute[] = [
component: NutritionistAdminLogin, component: NutritionistAdminLogin,
header: 'Login Tim Dietela', header: 'Login Tim Dietela',
}, },
{
name: ROUTES.checkout,
component: Checkout,
header: 'Checkout',
},
{
name: ROUTES.paymentResult,
component: PaymentResult,
header: 'Pembayaran',
},
]; ];
...@@ -21,7 +21,7 @@ export const nutritionistAdminLogin = 'nutritionist-admin-login'; ...@@ -21,7 +21,7 @@ export const nutritionistAdminLogin = 'nutritionist-admin-login';
const profile = 'profile'; const profile = 'profile';
export const clientProfile = `${profile}/client`; export const clientProfile = `${profile}/client`;
const payment = 'payment'; export const payment = 'payment';
export const paymentResult = `${payment}/result`; export const paymentResult = `${payment}/result`;
const nutritionist = 'nutritionist'; const nutritionist = 'nutritionist';
......
export { default as useApi } from './useApi'; export { default as useApi } from './useApi';
export { default as useAuthEffect } from './useAuthEffect';
export { default as useDownloadFiles } from './useDownloadFiles'; export { default as useDownloadFiles } from './useDownloadFiles';
export { default as useForm } from './useForm'; export { default as useForm } from './useForm';
export { default as useLinkingEffect } from './useLinkingEffect'; export { default as useLinkingEffect } from './useLinkingEffect';
export { default as useSignupEffect } from './useSignupEffect';
import { useContext, useEffect, useCallback, useState } from 'react'; import { useEffect, useCallback } from 'react';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { UserContext } from 'provider';
import * as ROUTES from 'constants/routes'; import * as ROUTES from 'constants/routes';
import CACHE_KEYS from 'constants/cacheKeys'; import CACHE_KEYS from 'constants/cacheKeys';
import { getCache } from 'utils/cache'; import { getCache } from 'utils/cache';
const useAuthEffect = () => { const useSignupEffect = () => {
const { isFirstLoading } = useContext(UserContext);
const navigation = useNavigation(); const navigation = useNavigation();
const [isLoading, setIsLoading] = useState(false);
const checkCart = useCallback(async () => { const checkCart = useCallback(async () => {
setIsLoading(true);
const dietProfileId = await getCache(CACHE_KEYS.dietProfileId); const dietProfileId = await getCache(CACHE_KEYS.dietProfileId);
const cartId = await getCache(CACHE_KEYS.cartId);
if (!dietProfileId) { if (!dietProfileId) {
navigation.reset({ navigation.reset({
index: 0, index: 0,
...@@ -22,20 +17,12 @@ const useAuthEffect = () => { ...@@ -22,20 +17,12 @@ const useAuthEffect = () => {
{ name: ROUTES.allAccessQuestionnaire }, { name: ROUTES.allAccessQuestionnaire },
], ],
}); });
} else if (!cartId) {
navigation.reset({
index: 0,
routes: [{ name: ROUTES.initial }, { name: ROUTES.choosePlan }],
});
} }
setIsLoading(false);
}, [navigation]); }, [navigation]);
useEffect(() => { useEffect(() => {
checkCart(); checkCart();
}, [checkCart]); }, [checkCart]);
return isFirstLoading || isLoading;
}; };
export default useAuthEffect; export default useSignupEffect;
...@@ -21,14 +21,13 @@ import { ...@@ -21,14 +21,13 @@ import {
import { setAuthHeader, resetAuthHeader } from 'services/api'; import { setAuthHeader, resetAuthHeader } from 'services/api';
import { iUserContext } from './types'; import { iUserContext } from './types';
import { TransactionStatus } from 'services/payment/models';
export const initialUser = { export const initialUser = {
id: null, id: null,
email: '', email: '',
name: '', name: '',
role: null, role: null,
transaction_status: TransactionStatus.UNPAID, transaction_status: null,
is_finished_onboarding: false, is_finished_onboarding: false,
}; };
...@@ -53,8 +52,8 @@ export const useUserContext = (): iUserContext => { ...@@ -53,8 +52,8 @@ export const useUserContext = (): iUserContext => {
await GoogleSignin.signOut(); await GoogleSignin.signOut();
await removeCache(CACHE_KEYS.authToken); await removeCache(CACHE_KEYS.authToken);
await removeCache(CACHE_KEYS.refreshToken); await removeCache(CACHE_KEYS.refreshToken);
setUser(initialUser);
resetAuthHeader(); resetAuthHeader();
setUser(initialUser);
}, []); }, []);
const getUser = useCallback(async () => { const getUser = useCallback(async () => {
...@@ -81,8 +80,8 @@ export const useUserContext = (): iUserContext => { ...@@ -81,8 +80,8 @@ export const useUserContext = (): iUserContext => {
const accessToken = data.access_token; const accessToken = data.access_token;
await setCache(CACHE_KEYS.authToken, accessToken); await setCache(CACHE_KEYS.authToken, accessToken);
await setCache(CACHE_KEYS.refreshToken, data.refresh_token); await setCache(CACHE_KEYS.refreshToken, data.refresh_token);
setUser(data.user);
setAuthHeader(accessToken); setAuthHeader(accessToken);
setUser(data.user);
}; };
const linkUserData = async (email: string) => { const linkUserData = async (email: string) => {
...@@ -104,8 +103,8 @@ export const useUserContext = (): iUserContext => { ...@@ -104,8 +103,8 @@ export const useUserContext = (): iUserContext => {
const signup = async (registerData: RegistrationRequest) => { const signup = async (registerData: RegistrationRequest) => {
const response = await signupApi(registerData); const response = await signupApi(registerData);
if (response.success && response.data) { if (response.success && response.data) {
await linkUserData(response.data.user.email);
await authSuccess(response.data); await authSuccess(response.data);
return await linkUserData(response.data.user.email);
} }
return response; return response;
}; };
...@@ -127,8 +126,7 @@ export const useUserContext = (): iUserContext => { ...@@ -127,8 +126,7 @@ export const useUserContext = (): iUserContext => {
access_token: tokens.accessToken, access_token: tokens.accessToken,
}); });
if (response.success && response.data) { if (response.success && response.data) {
await authSuccess(response.data); // If signup, link user to cart and diet profile
if (!isLogin) { if (!isLogin) {
const linkResponse = await linkUserData(response.data.user.email); const linkResponse = await linkUserData(response.data.user.email);
if (!linkResponse.success) { if (!linkResponse.success) {
...@@ -140,6 +138,8 @@ export const useUserContext = (): iUserContext => { ...@@ -140,6 +138,8 @@ export const useUserContext = (): iUserContext => {
}); });
} }
} }
await authSuccess(response.data);
} else { } else {
await logout(); await logout();
} }
......
import React, { FC, useContext } from 'react'; import React, { FC, useContext } from 'react';
import { useForm, useAuthEffect } from 'hooks'; import { useForm, useSignupEffect } from 'hooks';
import { ScrollView } from 'react-native-gesture-handler'; import { ScrollView } from 'react-native-gesture-handler';
import { useNavigation } from '@react-navigation/core'; import { useNavigation } from '@react-navigation/core';
import { BigButton, Link, Toast, Loader } from 'components/core'; import { BigButton, Link, Toast } from 'components/core';
import { Section } from 'components/layout'; import { Section } from 'components/layout';
import { TextField } from 'components/form'; import { TextField } from 'components/form';
import { GoogleLoginButton } from '../components'; import { GoogleLoginButton } from '../components';
...@@ -50,11 +50,8 @@ const ManualRegistrationPage: FC = () => { ...@@ -50,11 +50,8 @@ const ManualRegistrationPage: FC = () => {
const signupWithGoogle = () => loginWithGoogle(false); const signupWithGoogle = () => loginWithGoogle(false);
const isProcessing = useAuthEffect(); useSignupEffect();
if (isProcessing) {
return <Loader />;
}
return ( return (
<ScrollView contentContainerStyle={layoutStyles}> <ScrollView contentContainerStyle={layoutStyles}>
{textField.map((fieldProps, i) => ( {textField.map((fieldProps, i) => (
......
import React from 'react'; import React from 'react';
import { render, waitFor, fireEvent } from 'utils/testing'; import { render, waitFor, fireEvent } from '@testing-library/react-native';
import axios from 'axios'; import axios from 'axios';
import Checkout from '.'; import Checkout from '.';
import * as ROUTES from 'constants/routes';
import CACHE_KEYS from 'constants/cacheKeys'; import CACHE_KEYS from 'constants/cacheKeys';
import { DietelaProgram } from 'services/dietelaQuiz/quizResult'; import { DietelaProgram } from 'services/dietelaQuiz/quizResult';
import { setCache } from 'utils/cache'; import { setCache } from 'utils/cache';
import { mockProgramRecommendations } from '__mocks__/quizResult'; import { mockProgramRecommendations } from '__mocks__/quizResult';
import { Linking } from 'react-native';
jest.mock('axios'); jest.mock('axios');
const mockAxios = axios as jest.Mocked<typeof axios>; const mockAxios = axios as jest.Mocked<typeof axios>;
const mockedNavigate = jest.fn();
jest.mock('@react-navigation/native', () => {
return {
useNavigation: () => ({
navigate: mockedNavigate,
reset: mockedNavigate,
}),
};
});
describe('Checkout', () => { describe('Checkout', () => {
const nutritionist = { const nutritionist = {
id: 1, id: 1,
...@@ -38,18 +47,11 @@ describe('Checkout', () => { ...@@ -38,18 +47,11 @@ describe('Checkout', () => {
}, },
}); });
const userContextMock = {
isAuthenticated: true,
isUnpaidClient: true,
};
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 () => {
await setCache(CACHE_KEYS.cartId, 1); await setCache(CACHE_KEYS.cartId, 1);
mockAxios.request.mockImplementationOnce(retrieveCartApi); mockAxios.request.mockImplementationOnce(retrieveCartApi);
const { getAllByText, getByText } = render(<Checkout />, ROUTES.checkout, { const { getAllByText, getByText } = render(<Checkout />);
userContext: userContextMock,
});
await waitFor(() => expect(mockAxios.request).toBeCalled()); await waitFor(() => expect(mockAxios.request).toBeCalled());
const chosenProgram = getByText(/One Week Trial/i); const chosenProgram = getByText(/One Week Trial/i);
...@@ -59,16 +61,13 @@ describe('Checkout', () => { ...@@ -59,16 +61,13 @@ describe('Checkout', () => {
expect(readMoreButton).toBeTruthy(); expect(readMoreButton).toBeTruthy();
fireEvent.press(readMoreButton); fireEvent.press(readMoreButton);
const programDetailPage = getByText(/Program Dietela/i); expect(mockedNavigate).toHaveBeenCalled();
expect(programDetailPage).toBeTruthy();
}); });
it('redirects to nutritionist detail page when user clicks Baca selengkapnya button for nutritionist', async () => { it('redirects to nutritionist detail page when user clicks Baca selengkapnya button for nutritionist', async () => {
mockAxios.request.mockImplementationOnce(retrieveCartApi); mockAxios.request.mockImplementationOnce(retrieveCartApi);
const { getAllByText, getByText } = render(<Checkout />, ROUTES.checkout, { const { getAllByText, getByText } = render(<Checkout />);
userContext: userContextMock,
});
await waitFor(() => expect(mockAxios.request).toBeCalled()); await waitFor(() => expect(mockAxios.request).toBeCalled());
const chosenNutritionist = getByText(/Wendy/i); const chosenNutritionist = getByText(/Wendy/i);
...@@ -77,6 +76,8 @@ describe('Checkout', () => { ...@@ -77,6 +76,8 @@ describe('Checkout', () => {
const readMoreButton = getAllByText(/Baca selengkapnya/i)[1]; const readMoreButton = getAllByText(/Baca selengkapnya/i)[1];
expect(readMoreButton).toBeTruthy(); expect(readMoreButton).toBeTruthy();
fireEvent.press(readMoreButton); fireEvent.press(readMoreButton);
expect(mockedNavigate).toHaveBeenCalled();
}); });
it('redirects to choose plan page when user clicks Ganti Pilihan button', async () => { it('redirects to choose plan page when user clicks Ganti Pilihan button', async () => {
...@@ -86,25 +87,20 @@ describe('Checkout', () => { ...@@ -86,25 +87,20 @@ describe('Checkout', () => {
); );
mockAxios.request.mockImplementationOnce(retrieveCartApi); mockAxios.request.mockImplementationOnce(retrieveCartApi);