Fakultas Ilmu Komputer UI
Skip to content
GitLab
Menu
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
d401fdd6
Commit
d401fdd6
authored
May 18, 2021
by
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
Changes
24
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
.gitlab-ci.yml
View file @
d401fdd6
...
...
@@ -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 @
d401fdd6
...
...
@@ -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 @
d401fdd6
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 @
d401fdd6
...
...
@@ -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 @
d401fdd6
...
...
@@ -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 @
d401fdd6
...
...
@@ -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 @
d401fdd6
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 @
d401fdd6
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 @
d401fdd6
...
...
@@ -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 @
d401fdd6
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 @
d401fdd6
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 @
d401fdd6
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
?
(