Fakultas Ilmu Komputer UI
Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
ppl-fasilkom-ui
2021
Kelas D
PT Gizi Sehat - Dietela
Dietela Mobile
Commits
5ac1834d
Commit
5ac1834d
authored
May 18, 2021
by
Wulan Mantiri
Browse files
Add payment webview integration, fix auth logic
parent
212ca2c9
Changes
24
Hide whitespace changes
Inline
Side-by-side
.gitlab-ci.yml
View file @
5ac1834d
...
...
@@ -39,6 +39,7 @@ test:
-
yarn install
-
yarn test --silent
artifacts
:
expire_in
:
1 week
paths
:
-
coverage
...
...
@@ -84,9 +85,10 @@ android:
-
yarn install
-
export ANDROID_SDK_ROOT=/usr/lib/android-sdk
-
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
artifacts
:
expire_in
:
1 week
name
:
'
$CI_PROJECT_NAME-$CI_COMMIT_REF_NAME'
paths
:
-
$CI_PROJECT_NAME-$CI_COMMIT_REF_NAME.apk
...
...
src/app/index.tsx
View file @
5ac1834d
...
...
@@ -30,20 +30,18 @@ const NavigationStack: FC = () => {
<
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
,
}
}
/>
))
}
</>
{
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
>
);
...
...
src/app/schema.ts
View file @
5ac1834d
import
*
as
ROUTES
from
'
constants/routes
'
;
import
{
publicNavigation
,
unpaidClientNavigation
,
clientNavigation
,
nutritionistNavigation
,
}
from
'
constants/navigation
'
;
...
...
@@ -14,19 +13,20 @@ export const getNavigation = (
)
=>
{
if
(
isAuthenticated
)
{
if
(
user
.
role
===
UserRole
.
CLIENT
)
{
if
(
user
.
transaction_status
===
TransactionStatus
.
UNPAID
)
{
return
{
initialRoute
:
ROUTES
.
checkout
,
navigation
:
unpaidClientNavigation
,
};
let
initialRoute
=
ROUTES
.
clientProfile
;
if
([
TransactionStatus
.
UNPAID
,
null
].
includes
(
user
.
transaction_status
))
{
initialRoute
=
ROUTES
.
checkout
;
}
else
if
(
user
.
transaction_status
===
TransactionStatus
.
PENDING
)
{
initialRoute
=
ROUTES
.
paymentResult
;
}
else
if
(
!
user
.
is_finished_onboarding
)
{
initialRoute
=
ROUTES
.
extendedQuestionnaire
;
}
return
{
initialRoute
:
user
.
is_finished_onboarding
?
ROUTES
.
clientProfile
:
ROUTES
.
extendedQuestionnaire
,
initialRoute
,
navigation
:
clientNavigation
,
};
}
if
(
user
.
role
===
UserRole
.
NUTRITIONIST
)
{
return
{
initialRoute
:
ROUTES
.
clientListForNutritionist
,
...
...
@@ -34,6 +34,7 @@ export const getNavigation = (
};
}
}
return
{
initialRoute
:
ROUTES
.
initial
,
navigation
:
publicNavigation
,
...
...
src/constants/dietelaProgram.ts
View file @
5ac1834d
...
...
@@ -10,10 +10,10 @@ import {
}
from
'
./programDetail
'
;
const
prices
=
{
oneWeek
:
'
239
,
900
'
,
oneMonth
:
'
609
,
900
'
,
threeMonths
:
'
1
,
659
,
900
'
,
sixMonths
:
'
3
,
449
,
000
'
,
oneWeek
:
'
239
.
900
'
,
oneMonth
:
'
609
.
900
'
,
threeMonths
:
'
1
.
659
.
900
'
,
sixMonths
:
'
3
.
449
.
000
'
,
};
export
const
dietPrograms
=
{
...
...
src/constants/navigation.ts
View file @
5ac1834d
...
...
@@ -28,6 +28,7 @@ import {
ComingSoonPage
,
ClientProfile
,
ClientProfileForAdmin
,
PaymentWebView
,
}
from
'
scenes
'
;
import
{
FC
}
from
'
react
'
;
...
...
@@ -88,26 +89,22 @@ export const publicNavigation: NavRoute[] = [
},
];
export
const
unpaidC
lientNavigation
:
NavRoute
[]
=
[
export
const
c
lientNavigation
:
NavRoute
[]
=
[
{
name
:
ROUTES
.
checkout
,
component
:
Checkout
,
header
:
'
Checkout
'
,
},
{
name
:
ROUTES
.
payment
Result
,
component
:
Payment
Result
,
name
:
ROUTES
.
payment
,
component
:
Payment
WebView
,
header
:
'
Pembayaran
'
,
},
...
navigation
,
];
export
const
clientNavigation
:
NavRoute
[]
=
[
{
name
:
ROUTES
.
clientProfile
,
component
:
ClientProfile
,
header
:
'
Profil Saya
'
,
name
:
ROUTES
.
paymentResult
,
component
:
PaymentResult
,
},
...
navigation
,
{
name
:
ROUTES
.
extendedQuestionnaire
,
component
:
ExtendedQuestionnaire
,
...
...
@@ -142,6 +139,11 @@ export const clientNavigation: NavRoute[] = [
...
nav
,
name
:
ROUTES
.
extendedQuestionnaireById
(
id
),
})),
{
name
:
ROUTES
.
clientProfile
,
component
:
ClientProfile
,
header
:
'
Profil Saya
'
,
},
];
export
const
nutritionistNavigation
:
NavRoute
[]
=
[
...
...
@@ -176,7 +178,6 @@ export const adminNavigation: NavRoute[] = [
];
export
const
testNavigation
:
NavRoute
[]
=
[
...
navigation
,
...
clientNavigation
,
...
nutritionistNavigation
,
{
...
...
@@ -198,14 +199,4 @@ export const testNavigation: NavRoute[] = [
component
:
NutritionistAdminLogin
,
header
:
'
Login Tim Dietela
'
,
},
{
name
:
ROUTES
.
checkout
,
component
:
Checkout
,
header
:
'
Checkout
'
,
},
{
name
:
ROUTES
.
paymentResult
,
component
:
PaymentResult
,
header
:
'
Pembayaran
'
,
},
];
src/constants/routes.ts
View file @
5ac1834d
...
...
@@ -21,7 +21,7 @@ export const nutritionistAdminLogin = 'nutritionist-admin-login';
const
profile
=
'
profile
'
;
export
const
clientProfile
=
`
${
profile
}
/client`
;
const
payment
=
'
payment
'
;
export
const
payment
=
'
payment
'
;
export
const
paymentResult
=
`
${
payment
}
/result`
;
const
nutritionist
=
'
nutritionist
'
;
...
...
src/hooks/index.ts
View file @
5ac1834d
export
{
default
as
useApi
}
from
'
./useApi
'
;
export
{
default
as
useAuthEffect
}
from
'
./useAuthEffect
'
;
export
{
default
as
useDownloadFiles
}
from
'
./useDownloadFiles
'
;
export
{
default
as
useForm
}
from
'
./useForm
'
;
export
{
default
as
useLinkingEffect
}
from
'
./useLinkingEffect
'
;
export
{
default
as
useSignupEffect
}
from
'
./useSignupEffect
'
;
src/hooks/use
Auth
Effect/index.ts
→
src/hooks/use
Signup
Effect/index.ts
View file @
5ac1834d
import
{
useContext
,
useEffect
,
useCallback
,
useState
}
from
'
react
'
;
import
{
useEffect
,
useCallback
}
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
{
isFirstLoading
}
=
useContext
(
UserContext
);
const
useSignupEffect
=
()
=>
{
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
,
...
...
@@ -22,20 +17,12 @@ const useAuthEffect = () => {
{
name
:
ROUTES
.
allAccessQuestionnaire
},
],
});
}
else
if
(
!
cartId
)
{
navigation
.
reset
({
index
:
0
,
routes
:
[{
name
:
ROUTES
.
initial
},
{
name
:
ROUTES
.
choosePlan
}],
});
}
setIsLoading
(
false
);
},
[
navigation
]);
useEffect
(()
=>
{
checkCart
();
},
[
checkCart
]);
return
isFirstLoading
||
isLoading
;
};
export
default
use
Auth
Effect
;
export
default
use
Signup
Effect
;
src/provider/UserContext/index.ts
View file @
5ac1834d
...
...
@@ -21,14 +21,13 @@ import {
import
{
setAuthHeader
,
resetAuthHeader
}
from
'
services/api
'
;
import
{
iUserContext
}
from
'
./types
'
;
import
{
TransactionStatus
}
from
'
services/payment/models
'
;
export
const
initialUser
=
{
id
:
null
,
email
:
''
,
name
:
''
,
role
:
null
,
transaction_status
:
TransactionStatus
.
UNPAID
,
transaction_status
:
null
,
is_finished_onboarding
:
false
,
};
...
...
@@ -53,8 +52,8 @@ export const useUserContext = (): iUserContext => {
await
GoogleSignin
.
signOut
();
await
removeCache
(
CACHE_KEYS
.
authToken
);
await
removeCache
(
CACHE_KEYS
.
refreshToken
);
setUser
(
initialUser
);
resetAuthHeader
();
setUser
(
initialUser
);
},
[]);
const
getUser
=
useCallback
(
async
()
=>
{
...
...
@@ -81,8 +80,8 @@ export const useUserContext = (): iUserContext => {
const
accessToken
=
data
.
access_token
;
await
setCache
(
CACHE_KEYS
.
authToken
,
accessToken
);
await
setCache
(
CACHE_KEYS
.
refreshToken
,
data
.
refresh_token
);
setUser
(
data
.
user
);
setAuthHeader
(
accessToken
);
setUser
(
data
.
user
);
};
const
linkUserData
=
async
(
email
:
string
)
=>
{
...
...
@@ -104,8 +103,8 @@ export const useUserContext = (): iUserContext => {
const
signup
=
async
(
registerData
:
RegistrationRequest
)
=>
{
const
response
=
await
signupApi
(
registerData
);
if
(
response
.
success
&&
response
.
data
)
{
await
linkUserData
(
response
.
data
.
user
.
email
);
await
authSuccess
(
response
.
data
);
return
await
linkUserData
(
response
.
data
.
user
.
email
);
}
return
response
;
};
...
...
@@ -127,8 +126,7 @@ export const useUserContext = (): iUserContext => {
access_token
:
tokens
.
accessToken
,
});
if
(
response
.
success
&&
response
.
data
)
{
await
authSuccess
(
response
.
data
);
// If signup, link user to cart and diet profile
if
(
!
isLogin
)
{
const
linkResponse
=
await
linkUserData
(
response
.
data
.
user
.
email
);
if
(
!
linkResponse
.
success
)
{
...
...
@@ -140,6 +138,8 @@ export const useUserContext = (): iUserContext => {
});
}
}
await
authSuccess
(
response
.
data
);
}
else
{
await
logout
();
}
...
...
src/scenes/auth/ManualRegistrationPage/index.tsx
View file @
5ac1834d
import
React
,
{
FC
,
useContext
}
from
'
react
'
;
import
{
useForm
,
use
Auth
Effect
}
from
'
hooks
'
;
import
{
useForm
,
use
Signup
Effect
}
from
'
hooks
'
;
import
{
ScrollView
}
from
'
react-native-gesture-handler
'
;
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
{
TextField
}
from
'
components/form
'
;
import
{
GoogleLoginButton
}
from
'
../components
'
;
...
...
@@ -50,11 +50,8 @@ const ManualRegistrationPage: FC = () => {
const
signupWithGoogle
=
()
=>
loginWithGoogle
(
false
);
const
isProcessing
=
useAuth
Effect
();
useSignup
Effect
();
if
(
isProcessing
)
{
return
<
Loader
/>;
}
return
(
<
ScrollView
contentContainerStyle
=
{
layoutStyles
}
>
{
textField
.
map
((
fieldProps
,
i
)
=>
(
...
...
src/scenes/cart/Checkout/index.test.tsx
View file @
5ac1834d
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
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
'
;
import
{
Linking
}
from
'
react-native
'
;
jest
.
mock
(
'
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
'
,
()
=>
{
const
nutritionist
=
{
id
:
1
,
...
...
@@ -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
()
=>
{
await
setCache
(
CACHE_KEYS
.
cartId
,
1
);
mockAxios
.
request
.
mockImplementationOnce
(
retrieveCartApi
);
const
{
getAllByText
,
getByText
}
=
render
(<
Checkout
/>,
ROUTES
.
checkout
,
{
userContext
:
userContextMock
,
});
const
{
getAllByText
,
getByText
}
=
render
(<
Checkout
/>);
await
waitFor
(()
=>
expect
(
mockAxios
.
request
).
toBeCalled
());
const
chosenProgram
=
getByText
(
/One Week Trial/i
);
...
...
@@ -59,16 +61,13 @@ describe('Checkout', () => {
expect
(
readMoreButton
).
toBeTruthy
();
fireEvent
.
press
(
readMoreButton
);
const
programDetailPage
=
getByText
(
/Program Dietela/i
);
expect
(
programDetailPage
).
toBeTruthy
();
expect
(
mockedNavigate
).
toHaveBeenCalled
();
});
it
(
'
redirects to nutritionist detail page when user clicks Baca selengkapnya button for nutritionist
'
,
async
()
=>
{
mockAxios
.
request
.
mockImplementationOnce
(
retrieveCartApi
);
const
{
getAllByText
,
getByText
}
=
render
(<
Checkout
/>,
ROUTES
.
checkout
,
{
userContext
:
userContextMock
,
});
const
{
getAllByText
,
getByText
}
=
render
(<
Checkout
/>);
await
waitFor
(()
=>
expect
(
mockAxios
.
request
).
toBeCalled
());
const
chosenNutritionist
=
getByText
(
/Wendy/i
);
...
...
@@ -77,6 +76,8 @@ describe('Checkout', () => {
const
readMoreButton
=
getAllByText
(
/Baca selengkapnya/i
)[
1
];
expect
(
readMoreButton
).
toBeTruthy
();
fireEvent
.
press
(
readMoreButton
);
expect
(
mockedNavigate
).
toHaveBeenCalled
();
});
it
(
'
redirects to choose plan page when user clicks Ganti Pilihan button
'
,
async
()
=>
{
...
...
@@ -86,25 +87,20 @@ describe('Checkout', () => {
);
mockAxios
.
request
.
mockImplementationOnce
(
retrieveCartApi
);
const
{
getByText
}
=
render
(<
Checkout
/>,
ROUTES
.
checkout
,
{
userContext
:
userContextMock
,
});
const
{
getByText
}
=
render
(<
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
();
expect
(
mockedNavigate
).
toHaveBeenCalled
();
});
it
(
'
call Linking open url
when user clicks Bayar button and submit successful
'
,
async
()
=>
{
it
(
'
redirect to payment webview
when user clicks Bayar button and submit successful
'
,
async
()
=>
{
mockAxios
.
request
.
mockImplementationOnce
(
retrieveCartApi
);
const
{
getByText
}
=
render
(<
Checkout
/>,
ROUTES
.
checkout
,
{
userContext
:
userContextMock
,
});
const
{
getByText
}
=
render
(<
Checkout
/>);
await
waitFor
(()
=>
expect
(
mockAxios
.
request
).
toBeCalled
());
const
payWithMidtransApi
=
()
=>
...
...
@@ -115,22 +111,18 @@ describe('Checkout', () => {
},
});
mockAxios
.
request
.
mockImplementationOnce
(
payWithMidtransApi
);
const
spy
=
jest
.
spyOn
(
Linking
,
'
openURL
'
);
const
payButton
=
getByText
(
/bayar dengan midtrans/i
);
expect
(
payButton
).
toBeTruthy
();
await
waitFor
(()
=>
fireEvent
.
press
(
payButton
));
expect
(
spy
).
toHaveBeenCalled
();
spy
.
mockReset
();
expect
(
mockedNavigate
).
toHaveBeenCalled
();
});
it
(
'
does not
call Linking open url
when user clicks Bayar button but submit fails
'
,
async
()
=>
{
it
(
'
does not
redirect to payment
when user clicks Bayar button but submit fails
'
,
async
()
=>
{
mockAxios
.
request
.
mockImplementationOnce
(
retrieveCartApi
);
const
{
getByText
}
=
render
(<
Checkout
/>,
ROUTES
.
checkout
,
{
userContext
:
userContextMock
,
});
const
{
getByText
}
=
render
(<
Checkout
/>);
await
waitFor
(()
=>
expect
(
mockAxios
.
request
).
toBeCalled
());
const
payWithMidtransApi
=
()
=>
...
...
@@ -141,14 +133,27 @@ describe('Checkout', () => {
},
});
mockAxios
.
request
.
mockImplementationOnce
(
payWithMidtransApi
);
const
spy
=
jest
.
spyOn
(
Linking
,
'
openURL
'
);
const
payButton
=
getByText
(
/bayar dengan midtrans/i
);
expect
(
payButton
).
toBeTruthy
();
await
waitFor
(()
=>
fireEvent
.
press
(
payButton
));
});
it
(
'
shows empty data page when fetch cart fails
'
,
async
()
=>
{
mockAxios
.
request
.
mockImplementationOnce
(()
=>
Promise
.
resolve
({
status
:
400
,
response
:
{
data
:
undefined
,
},
}),
);
const
{
getByText
}
=
render
(<
Checkout
/>);
await
waitFor
(()
=>
expect
(
mockAxios
.
request
).
toBeCalled
());
expect
(
spy
).
not
.
toHaveBeenCalled
(
);
spy
.
mockReset
();
const
emptyDataPage
=
getByText
(
/Anda belum memilih program diet/i
);
expect
(
emptyDataPage
).
toBeTruthy
();
});
afterAll
(()
=>
{
...
...
src/scenes/cart/Checkout/index.tsx
View file @
5ac1834d
import
React
,
{
FC
,
useCallback
,
useState
}
from
'
react
'
;
import
{
View
,
Linking
}
from
'
react-native
'
;
import
React
,
{
FC
}
from
'
react
'
;
import
{
View
}
from
'
react-native
'
;
import
{
Text
,
Button
}
from
'
react-native-elements
'
;
import
{
useNavigation
}
from
'
@react-navigation/native
'
;
...
...
@@ -8,79 +8,75 @@ import { Section } from 'components/layout';
import
CACHE_KEYS
from
'
constants/cacheKeys
'
;
import
*
as
ROUTES
from
'
constants/routes
'
;
import
{
dietPrograms
}
from
'
constants/dietelaProgram
'
;
import
{
useApi
,
useLinkingEffect
}
from
'
hooks
'
;
import
{
useApi
}
from
'
hooks
'
;
import
{
retrieveCartApi
,
payWithMidtransApi
}
from
'
services/payment
'
;
import
{
getCache
}
from
'
utils/cache
'
;
import
{
typographyStyles
}
from
'
styles
'
;
import
{
styles
}
from
'
./styles
'
;
import
{
CheckoutCard
}
from
'
./components
'
;
import
EmptyDataPage
from
'
components/core/EmptyDataPage
'
;
const
Checkout
:
FC
=
()
=>
{
const
navigation
=
useNavigation
();
const
[
cartId
,
setCartId
]
=
useState
<
string
|
null
>
(
null
);
const
fetchCart
=
useCallback
(
async
()
=>
{
const
fetchCart
=
async
()
=>
{
const
cachedCartId
=
await
getCache
(
CACHE_KEYS
.
cartId
);
setCartId
(
cachedCartId
);
return
await
retrieveCartApi
(
cachedCartId
);
}
,
[
setCartId
])
;
};
const
{
isLoading
,
data
}
=
useApi
(
fetchCart
);
const
pay
=
async
()
=>
{
const
response
=
await
payWithMidtransApi
(
cartI
d
);
const
response
=
await
payWithMidtransApi
(
data
?.
i
d
);
if
(
response
.
success
&&
response
.
data
)
{
await
Linking
.
openURL
(
response
.
data
.
redirect_url
);
navigation
.
navigate
(
ROUTES
.
payment
,
{
url
:
response
.
data
.
redirect_url
}
);
}
else
{
Toast
.
show
({
type
:
'
error
'
,
text1
:
'
Gagal melakukan transaksi pembayaran.
'
,
text2
:
'
Terjadi kesalahan pada sisi kami. Silakan coba lagi
'
,
text1
:
'
Anda sudah membayar tagihan ini.
'
,
text2
:
'
Mohon restart aplikasi Dietela untuk memulai perjalanan diet Anda.
'
,
});
}
};
useLinkingEffect
();
if
(
isLoading
)
{
return
<
Loader
/>;
}
if
(
!
data
)
{
return
<
EmptyDataPage
text
=
"Anda belum memilih program diet"
/>;
}
return
(
<
View
style
=
{
styles
.
container
}
>
<
View
>
<
CheckoutCard
content
=
{
data
?
dietPrograms
[
data
.
program
.
unique_code
].
title
:
'
-
'
}
content
=
{
dietPrograms
[
data
.
program
.
unique_code
].
title
}
type
=
"program"
onReadMore
=
{
()
=>
navigation
.
navigate
(
ROUTES
.
programDetail
,
{
id
:
data
?
.
program
.
unique_code
,
id
:
data
.
program
.
unique_code
,
})
}
/>
<
CheckoutCard
content
=
{
data
?
data
.
nutritionist
.
full_name_and_degree
:
'
-
'
}
content
=
{
data
.
nutritionist
.
full_name_and_degree
}
type
=
"nutritionist"
onReadMore
=
{
()
=>
navigation
.
navigate
(
ROUTES
.
nutritionistDetail
,
{
ntr
:
data
?
.
nutritionist
,
ntr
:
data
.
nutritionist
,
})
}
/>
</
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
>