Fakultas Ilmu Komputer UI

Commit 5b4711f9 authored by Wulan Mantiri's avatar Wulan Mantiri
Browse files

Integrate payment integration

parent 7f321890
......@@ -23,6 +23,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:host="mobile.dietela.id" android:scheme="http" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
......
import React from 'react';
import { render, fireEvent, waitFor } from 'utils/testing';
import { withAuthRender, fireEvent, waitFor } from 'utils/testing';
import * as ROUTES from 'constants/routes';
import LogoutButton from '.';
......@@ -30,11 +30,11 @@ describe('LogoutButton', () => {
};
it('renders correctly', () => {
render(<LogoutButton />, ROUTES.checkout);
withAuthRender(<LogoutButton />, ROUTES.checkout);
});
it('calls logout and redirects to initial page when clicked', async () => {
const { getByTestId } = render(
const { getByTestId } = withAuthRender(
<UserContext.Provider value={userContextMock}>
<LogoutButton />
</UserContext.Provider>,
......
......@@ -2,26 +2,17 @@ import React, { FC, useContext } from 'react';
import { UserContext } from 'provider';
import { Button, Icon } from 'react-native-elements';
import { StyleSheet, View } from 'react-native';
import { useNavigation } from '@react-navigation/core';
import * as ROUTES from 'constants/routes';
const LogoutButton: FC = () => {
const LogoutButton: FC<{
tintColor?: string | undefined;
}> = () => {
const { logout, isAuthenticated } = useContext(UserContext);
const navigation = useNavigation();
const handlePress = async () => {
await logout();
navigation.reset({
index: 0,
routes: [{ name: ROUTES.initial }],
});
};
return isAuthenticated ? (
<Button
icon={<Icon name="logout" type="material" />}
buttonStyle={styles.button}
onPress={handlePress}
onPress={logout}
testID="logoutButton"
/>
) : (
......
import React, { FC } from 'react';
import React, { FC, useContext } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { ThemeProvider } from 'react-native-elements';
import Toast from 'react-native-toast-message';
import * as ROUTES from 'constants/routes';
import { navigation } from 'constants/navigation';
import ContextProvider from 'provider';
import ContextProvider, { UserContext } from 'provider';
import { theme } from 'styles/theme';
import { screenOptions, toastConfig } from './styles';
import LogoutButton from './LogoutButton';
import { getNavigation } from './schema';
const Stack = createStackNavigator();
const NavigationStack: FC = () => {
const { isAuthenticated, isUnpaidClient } = useContext(UserContext);
const { initialRoute, navigation } = getNavigation(
isAuthenticated,
isUnpaidClient,
);
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName={initialRoute}
screenOptions={screenOptions}>
<>
{navigation.map((nav, i) => (
<Stack.Screen
key={`nav${i}`}
name={nav.name}
component={nav.component}
options={{
title: nav.header,
headerShown: Boolean(nav.header),
headerRight: LogoutButton,
}}
/>
))}
</>
</Stack.Navigator>
</NavigationContainer>
);
};
const App: FC = () => {
return (
<ThemeProvider theme={theme}>
<ContextProvider>
<NavigationContainer>
<Stack.Navigator
initialRouteName={ROUTES.initial}
screenOptions={screenOptions}>
{navigation.map((nav, i) => (
<Stack.Screen
key={`nav${i}`}
name={nav.name}
component={nav.component}
options={{
title: nav.header,
headerShown: Boolean(nav.header),
headerRight: LogoutButton,
}}
/>
))}
</Stack.Navigator>
</NavigationContainer>
<NavigationStack />
<Toast config={toastConfig} ref={Toast.setRef} />
</ContextProvider>
</ThemeProvider>
......
import * as ROUTES from 'constants/routes';
import {
publicNavigation,
privateNavigation,
unpaidClientNavigation,
} from 'constants/navigation';
export const getNavigation = (
isAuthenticated: boolean,
isUnpaidClient: boolean,
) => {
if (isAuthenticated) {
return isUnpaidClient
? {
initialRoute: ROUTES.checkout,
navigation: unpaidClientNavigation,
}
: {
initialRoute: ROUTES.profile,
navigation: privateNavigation,
};
}
return {
initialRoute: ROUTES.initial,
navigation: publicNavigation,
};
};
......@@ -10,6 +10,7 @@ import {
LoginPage,
ProgramDetail,
NutritionistDetail,
PaymentResult,
} from 'scenes';
import { FC } from 'react';
......@@ -19,11 +20,7 @@ export interface NavRoute {
header?: string;
}
export const navigation: NavRoute[] = [
{
name: ROUTES.initial,
component: InitialPage,
},
const navigation: NavRoute[] = [
{
name: ROUTES.allAccessQuestionnaire,
component: AllAccessQuestionnaire,
......@@ -39,11 +36,6 @@ export const navigation: NavRoute[] = [
component: ChoosePlan,
header: 'Choose Plan',
},
{
name: ROUTES.checkout,
component: Checkout,
header: 'Checkout',
},
{
name: ROUTES.programDetail,
component: ProgramDetail,
......@@ -54,6 +46,14 @@ export const navigation: NavRoute[] = [
component: NutritionistDetail,
header: 'Nutrisionis',
},
];
export const publicNavigation: NavRoute[] = [
{
name: ROUTES.initial,
component: InitialPage,
},
...navigation,
{
name: ROUTES.registration,
component: ManualRegistrationPage,
......@@ -64,6 +64,23 @@ export const navigation: NavRoute[] = [
component: LoginPage,
header: 'Login',
},
];
export const unpaidClientNavigation: NavRoute[] = [
{
name: ROUTES.checkout,
component: Checkout,
header: 'Checkout',
},
{
name: ROUTES.paymentResult,
component: PaymentResult,
header: 'Pembayaran',
},
...navigation,
];
export const privateNavigation: NavRoute[] = [
{
name: ROUTES.profile,
component: ComingSoonPage,
......
......@@ -14,3 +14,6 @@ export const registration = 'registration';
export const login = 'login';
export const profile = 'profile';
const payment = 'payment';
export const paymentResult = `${payment}/result`;
export { default as useApi } from './useApi';
export { default as useAuthEffect } from './useAuthEffect';
export { default as useAuthGuardEffect } from './useAuthGuardEffect';
export { default as useForm } from './useForm';
export { default as useLinkingEffect } from './useLinkingEffect';
......@@ -5,10 +5,8 @@ import * as ROUTES from 'constants/routes';
import CACHE_KEYS from 'constants/cacheKeys';
import { getCache } from 'utils/cache';
const useAuthEffect = (isLogin?: boolean) => {
const { isAuthenticated, isUnpaidClient, isFirstLoading } = useContext(
UserContext,
);
const useAuthEffect = () => {
const { isFirstLoading } = useContext(UserContext);
const navigation = useNavigation();
const [isLoading, setIsLoading] = useState(false);
......@@ -34,26 +32,8 @@ const useAuthEffect = (isLogin?: boolean) => {
}, [navigation]);
useEffect(() => {
if (isAuthenticated) {
if (isUnpaidClient) {
navigation.reset({
index: 0,
routes: [{ name: ROUTES.checkout }],
});
} else {
navigation.reset({
index: 0,
routes: [{ name: ROUTES.profile }],
});
}
} else if (isLogin) {
checkCart();
}
return () => {
setIsLoading(false);
};
}, [checkCart, isLogin, isAuthenticated, isUnpaidClient, navigation]);
checkCart();
}, [checkCart]);
return isFirstLoading || isLoading;
};
......
import { useContext, useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';
import { UserContext } from 'provider';
import * as ROUTES from 'constants/routes';
const useAuthGuardEffect = (signupFallback?: boolean) => {
const { isAuthenticated, firstAuthenticated } = useContext(UserContext);
const navigation = useNavigation();
useEffect(() => {
if (!isAuthenticated && firstAuthenticated) {
navigation.navigate(signupFallback ? ROUTES.registration : ROUTES.login);
}
}, [isAuthenticated, signupFallback, navigation, firstAuthenticated]);
};
export default useAuthGuardEffect;
import { useEffect } from 'react';
import { Linking } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import * as ROUTES from 'constants/routes';
const useLinkingEffect = () => {
const navigation = useNavigation();
const getQueryParams = (path: string | null) => {
return path
? JSON.parse(
'{"' +
decodeURI(path.replace(/&/g, '","').replace(/[=]/g, '":"')) +
'"}',
)
: {};
};
useEffect(() => {
const navigate = (url: string | null) => {
if (url) {
const splitUrl = url.split('?');
const path = splitUrl[0].split('/').slice(3).join('/');
const queryParams = getQueryParams(splitUrl[1]);
if (path === 'payment') {
navigation.reset({
index: 0,
routes: [{ name: ROUTES.paymentResult, params: queryParams }],
});
}
}
};
const handleOpenURL = (event: { url: string }) => {
navigate(event.url);
};
Linking.addEventListener('url', handleOpenURL);
return () => {
Linking.removeEventListener('url', handleOpenURL);
};
}, [navigation]);
};
export default useLinkingEffect;
......@@ -22,6 +22,7 @@ import {
import { set401Callback, setAuthHeader, resetAuthHeader } from 'services/api';
import { iUserContext } from './types';
import { TransactionStatus } from 'services/payment/models';
const initialUser = {
id: null,
......@@ -32,7 +33,6 @@ const initialUser = {
export const UserContext = createContext<iUserContext>({
user: initialUser,
firstAuthenticated: true,
isAuthenticated: false,
isUnpaidClient: false,
isPaidClient: false,
......@@ -50,8 +50,9 @@ export const useUserContext = (): iUserContext => {
const [user, setUser] = useState<User>(initialUser);
const [isLoading, setIsLoading] = useState(false);
const [isFirstLoading, setIsFirstLoading] = useState(false);
const [firstAuth, setFirstAuth] = useState(true);
const [clientHasPaid] = useState(false);
const [transactionStatus, setTransactionStatus] = useState(
TransactionStatus.UNPAID,
);
const logout = useCallback(async () => {
await GoogleSignin.signOut();
......@@ -59,7 +60,6 @@ export const useUserContext = (): iUserContext => {
await removeCache(CACHE_KEYS.refreshToken);
setUser(initialUser);
resetAuthHeader();
setFirstAuth(false);
}, []);
const getUser = useCallback(async () => {
......@@ -69,7 +69,9 @@ export const useUserContext = (): iUserContext => {
setAuthHeader(token);
const response = await retrieveUserApi();
if (response.success && response.data) {
setUser(response.data);
const { cart, ...userData } = response.data;
setUser(userData);
setTransactionStatus(cart.transaction_status);
} else {
await logout();
Toast.show({
......@@ -167,10 +169,11 @@ export const useUserContext = (): iUserContext => {
return {
user,
firstAuthenticated: firstAuth,
isAuthenticated: user.id !== null,
isUnpaidClient: user.role === UserRole.CLIENT,
isPaidClient: user.role === UserRole.CLIENT && clientHasPaid,
isUnpaidClient:
user.role === UserRole.CLIENT &&
transactionStatus === TransactionStatus.UNPAID,
isPaidClient: user.role === UserRole.CLIENT,
isNutritionist: user.role === UserRole.NUTRITIONIST,
isAdmin: user.role === UserRole.ADMIN,
isLoading,
......
......@@ -9,7 +9,6 @@ import {
export interface iUserContext {
user: User;
firstAuthenticated: boolean;
isAuthenticated: boolean;
isUnpaidClient: boolean;
isPaidClient: boolean;
......
......@@ -45,7 +45,7 @@ const Login: FC = () => {
},
});
const isProcessing = useAuthEffect(true);
const isProcessing = useAuthEffect();
if (isProcessing) {
return <Loader />;
......
import React, { FC, useContext } from 'react';
import { useAuthEffect, useForm } from 'hooks';
import { useForm } from 'hooks';
import { ScrollView } from 'react-native-gesture-handler';
import { useNavigation } from '@react-navigation/core';
......@@ -50,8 +50,6 @@ const ManualRegistrationPage: FC = () => {
const signupWithGoogle = () => loginWithGoogle(false);
useAuthEffect();
return (
<ScrollView contentContainerStyle={layoutStyles}>
{textField.map((fieldProps, i) => (
......
import React, { FC, useCallback } from 'react';
import { View } from 'react-native';
import React, { FC, useCallback, useState } from 'react';
import { View, Linking } from 'react-native';
import { Text, Button } from 'react-native-elements';
import { useNavigation } from '@react-navigation/native';
import { Loader, BigButton } from 'components/core';
import { Loader, BigButton, Toast } 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 { useApi, useLinkingEffect } from 'hooks';
import { retrieveCartApi, payWithMidtransApi } from 'services/payment';
import { getCache } from 'utils/cache';
import { typographyStyles } from 'styles';
......@@ -19,14 +19,30 @@ import { CheckoutCard } from './components';
const Checkout: FC = () => {
const navigation = useNavigation();
const [cartId, setCartId] = useState<string | null>(null);
const fetchCart = useCallback(async () => {
const cartId = await getCache(CACHE_KEYS.cartId);
return await retrieveCartApi(cartId);
}, []);
const cachedCartId = await getCache(CACHE_KEYS.cartId);
setCartId(cachedCartId);
return await retrieveCartApi(cachedCartId);
}, [setCartId]);
const { isLoading, data } = useApi(fetchCart);
useAuthGuardEffect(true);
const pay = async () => {
const response = await payWithMidtransApi(cartId);
if (response.success && response.data) {
await Linking.openURL(response.data.redirect_url);
} else {
Toast.show({
type: 'error',
text1: 'Gagal melakukan transaksi pembayaran.',
text2: 'Terjadi kesalahan pada sisi kami. Silakan coba lagi',
});
}
};
useLinkingEffect();
if (isLoading) {
return <Loader />;
......@@ -70,12 +86,21 @@ const Checkout: FC = () => {
<Button
title="ganti pilihan"
type="outline"
onPress={() => navigation.navigate(ROUTES.choosePlan)}
onPress={() =>
navigation.reset({
index: 0,
routes: [{ name: ROUTES.choosePlan }],
})
}
buttonStyle={styles.buttonStyle}
titleStyle={[typographyStyles.overlineBig, styles.titleStyle]}
/>
<Section>
<BigButton title="bayar dengan midtrans" onPress={console.log} />
<BigButton
title="bayar dengan midtrans"
onPress={pay}
disabled={cartId ? undefined : true}
/>
</Section>
</View>
</View>
......
import React, { FC, useState, useCallback, useEffect } from 'react';
import React, { FC, useState, useCallback, useEffect, useContext } from 'react';
import { ScrollView } from 'react-native';
import { useNavigation } from '@react-navigation/native';
......@@ -18,9 +18,11 @@ import { setCache, getCache } from 'utils/cache';
import { PricingList } from './components';
import { initialValues, getRecommendedPrograms } from './schema';
import { UserContext } from 'provider';
const ChoosePlan: FC = () => {
const navigation = useNavigation();
const { user, isAuthenticated } = useContext(UserContext);
const [currentPage, setCurrentPage] = useState(1);
const [isSubmitting, setIsSubmitting] = useState(false);
......@@ -29,12 +31,25 @@ const ChoosePlan: FC = () => {
const handleSubmit = async () => {
setIsSubmitting(true);
const response = await createCartApi(values);
const payload = user.id
? {
...values,
user: user.id,
}
: values;
const response = await createCartApi(payload);
setIsSubmitting(false);
console.log(response);
if (response.success) {
await setCache(CACHE_KEYS.cartId, response.data?.id);
navigation.navigate(ROUTES.checkout, response.data);
if (isAuthenticated) {
navigation.navigate(ROUTES.checkout);
} else {
navigation.reset({
index: 0,
routes: [{ name: ROUTES.registration }],
});
}
} else {
Toast.show({
type: 'error',
......
......@@ -4,7 +4,6 @@ import { View, Text, ImageBackground, Image } from 'react-native';
import { BigButton, Link, DietelaCoverLoader } from 'components/core';
import { banner_girl_eating, logo_white_small } from 'assets/images';
import { useAuthEffect } from 'hooks';