totalPayment.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. // app/(auth)/(tabs)/(home)/totalPayment.tsx
  2. import { router, useFocusEffect, useNavigation } from 'expo-router';
  3. import {
  4. View,
  5. Text,
  6. ScrollView,
  7. Pressable,
  8. StyleSheet,
  9. Image,
  10. BackHandler,
  11. Alert,
  12. Linking,
  13. AppState,
  14. Modal,
  15. ActivityIndicator
  16. } from 'react-native';
  17. import { SafeAreaView } from 'react-native-safe-area-context';
  18. import NormalButton from '../../../../component/global/normal_button';
  19. import { PreviousPageBlackSvg } from '../../../../component/global/SVG';
  20. import { useChargingStore } from '../../../../providers/scan_qr_payload_store';
  21. import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
  22. import useUserInfoStore from '../../../../providers/userinfo_store';
  23. import { chargeStationService } from '../../../../service/chargeStationService';
  24. import { authenticationService } from '../../../../service/authService';
  25. import axios from 'axios';
  26. import sha256 from 'crypto-js/sha256';
  27. import { walletService } from '../../../../service/walletService';
  28. import AsyncStorage from '@react-native-async-storage/async-storage';
  29. import { useTranslation } from '../../../../util/hooks/useTranslation';
  30. const TotalPayment = () => {
  31. const { t } = useTranslation();
  32. const {
  33. promotion_code,
  34. stationID,
  35. sum_of_coupon,
  36. scanned_qr_code,
  37. coupon_detail,
  38. total_power,
  39. processed_coupon_store,
  40. setPromotionCode,
  41. setCouponDetail,
  42. setTotalPower,
  43. setProcessedCouponStore,
  44. setSumOfCoupon,
  45. setCurrentPriceStore
  46. } = useChargingStore();
  47. const [currentPriceTotalPayment, setCurrentPriceTotalPayment] = useState<number | null>(null);
  48. const [walletBalance, setWalletBalance] = useState<number | null>(null);
  49. const [loading, setLoading] = useState(false);
  50. const [outTradeNo, setOutTradeNo] = useState('');
  51. const [isExpectingPayment, setIsExpectingPayment] = useState(false);
  52. const paymentInitiatedTime = useRef(null);
  53. const PAYMENT_CHECK_TIMEOUT = 5 * 60 * 1000; // 5 minutes in milliseconds
  54. const appState = useRef(AppState.currentState);
  55. const [totalPrice, setTotalPrice] = useState<number | null>(null);
  56. const [paymentStatus, setPaymentStatus] = useState(null);
  57. const [loadingModalVisible, setLoadingModalVisible] = useState(false);
  58. //fetch current price based on using coupon or not. use coupon = $3.5
  59. useEffect(() => {
  60. const fetchCurrentPrice = async () => {
  61. try {
  62. //if promotion_code.length > 0, fetch original price, otherwise fetch current price
  63. //then calculate total price for display purpose
  64. if (promotion_code.length > 0) {
  65. const response = await chargeStationService.getOriginalPriceInPay(stationID);
  66. setCurrentPriceTotalPayment(response);
  67. let totalPrice = Number(total_power) * Number(response) - Number(sum_of_coupon);
  68. if (totalPrice < 0) {
  69. totalPrice = 0;
  70. } else {
  71. totalPrice = totalPrice;
  72. }
  73. setTotalPrice(totalPrice);
  74. } else {
  75. const response = await chargeStationService.getCurrentPriceInPay(stationID);
  76. setCurrentPriceTotalPayment(response);
  77. let totalPrice = Number(total_power) * Number(response);
  78. setTotalPrice(totalPrice);
  79. }
  80. } catch (error) {
  81. // More specific error handling
  82. if (axios.isAxiosError(error)) {
  83. const errorMessage = error.response?.data?.message || 'Network error occurred';
  84. Alert.alert(t('common.error'), `${t('wallet.error_fetching_balance')}: ${errorMessage}`, [
  85. {
  86. text: t('common.ok'),
  87. onPress: () => {
  88. cleanupData();
  89. router.push('/mainPage');
  90. }
  91. }
  92. ]);
  93. } else {
  94. Alert.alert(t('common.error'), t('wallet.error_fetching_balance'), [
  95. {
  96. text: t('common.ok'),
  97. onPress: () => {
  98. cleanupData();
  99. router.push('/mainPage');
  100. }
  101. }
  102. ]);
  103. }
  104. }
  105. };
  106. fetchCurrentPrice();
  107. }, []);
  108. // Add this effect to handle Android back button
  109. useFocusEffect(
  110. useCallback(() => {
  111. const onBackPress = () => {
  112. cleanupData();
  113. if (router.canGoBack()) {
  114. router.back();
  115. } else {
  116. router.replace('/scanQrPage');
  117. }
  118. return true;
  119. };
  120. const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress);
  121. return () => subscription.remove()
  122. // return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress);
  123. }, [])
  124. );
  125. //check payment status
  126. useEffect(() => {
  127. const subscription = AppState.addEventListener('change', (nextAppState: any) => {
  128. if (
  129. appState.current.match(/inactive|background/) &&
  130. nextAppState === 'active' &&
  131. isExpectingPayment &&
  132. // outTradeNo &&
  133. paymentInitiatedTime.current
  134. ) {
  135. const currentTime = new Date().getTime();
  136. if (currentTime - paymentInitiatedTime.current < PAYMENT_CHECK_TIMEOUT) {
  137. checkPaymentStatus();
  138. } else {
  139. // Payment check timeout reached
  140. setIsExpectingPayment(false);
  141. setOutTradeNo('');
  142. paymentInitiatedTime.current = null;
  143. Alert.alert(
  144. t('payment.payment_timeout_title'),
  145. t('payment.payment_timeout_message')
  146. );
  147. }
  148. }
  149. appState.current = nextAppState;
  150. });
  151. return () => {
  152. subscription.remove();
  153. };
  154. }, [outTradeNo, isExpectingPayment]);
  155. const navigation = useNavigation();
  156. useLayoutEffect(() => {
  157. navigation.setOptions({
  158. gestureEnabled: false
  159. });
  160. }, [navigation]);
  161. const checkPaymentStatus = async () => {
  162. try {
  163. // console.log('outTradeNo in scanQR Page checkpaymentstatus ', outTradeNo);
  164. const result = await walletService.checkPaymentStatus(outTradeNo);
  165. setPaymentStatus(result);
  166. // console.log('checkPaymentStatus from scan QR checkpaymentstatus', result);
  167. if (result && !result.some((item) => item.errmsg?.includes(t('payment.processing')))) {
  168. // Payment successful
  169. // console.log('totalFee', totalFee);
  170. Alert.alert(t('payment.payment_success_title'), t('payment.payment_success_message', { amount: totalPrice }), [
  171. {
  172. text: t('payment.confirm'),
  173. onPress: async () => {
  174. cleanupData();
  175. router.push('/mainPage');
  176. }
  177. }
  178. ]);
  179. } else {
  180. Alert.alert(t('payment.payment_failed_title'), t('payment.payment_failed_message'), [
  181. {
  182. text: t('payment.ok'),
  183. onPress: () => {
  184. cleanupData();
  185. router.push('/mainPage');
  186. }
  187. }
  188. ]);
  189. }
  190. setIsExpectingPayment(false);
  191. setOutTradeNo('');
  192. paymentInitiatedTime.current = null;
  193. } catch (error) {
  194. console.error('Failed to check payment status:', error);
  195. Alert.alert(t('common.error'), t('payment.payment_status_check_failed'));
  196. }
  197. };
  198. const showLoadingAndNavigate = async () => {
  199. setLoadingModalVisible(true);
  200. // Wait for 2 seconds
  201. await new Promise((resolve) => setTimeout(resolve, 2000));
  202. cleanupData();
  203. setLoadingModalVisible(false);
  204. router.navigate('(auth)/(tabs)/(home)/mainPage');
  205. router.push('(auth)/(tabs)/(charging)/chargingPage');
  206. };
  207. const cleanupData = () => {
  208. setPromotionCode([]);
  209. setCouponDetail([]);
  210. setProcessedCouponStore([]);
  211. setSumOfCoupon(0);
  212. setTotalPower(null);
  213. };
  214. const handlePay = async () => {
  215. try {
  216. let car, user_id, walletBalance, price_for_pay;
  217. setLoading(true);
  218. if (currentPriceTotalPayment === null) {
  219. Alert.alert(t('payment.processing'), t('payment.wait_loading_price'));
  220. return;
  221. }
  222. //fetch car with proper try catch
  223. try {
  224. car = await chargeStationService.getUserDefaultCars();
  225. if (!car?.data?.id) {
  226. Alert.alert(t('payment.error_title'), t('payment.failed_fetch_udcc'));
  227. return;
  228. }
  229. } catch (error) {
  230. console.error('Failed to fetch user default car:', error);
  231. Alert.alert(t('payment.error_title'), t('payment.failed_fetch_udc'));
  232. return;
  233. }
  234. //fetch user id with proper try catch
  235. try {
  236. user_id = await authenticationService.getUserInfo();
  237. if (!user_id?.data?.id) {
  238. Alert.alert(t('payment.error_title'), t('payment.failed_fetch_userid'));
  239. return;
  240. }
  241. } catch (error) {
  242. console.error('Failed to fetch user ID:', error);
  243. Alert.alert(t('payment.error_title'), t('payment.failed_fetch_userid'));
  244. return;
  245. }
  246. // fetch user wallet with proper try catch
  247. try {
  248. walletBalance = await walletService.getWalletBalance();
  249. } catch (error) {
  250. console.error('Failed to fetch user wallet:', error);
  251. Alert.alert(t('payment.error_title'), t('payment.failed_fetch_wallet'));
  252. return;
  253. }
  254. //now i have all information ready, i check penalty reservation
  255. //by first fetching all history, then check if penalty_fee > 0 and penalty_paid_status is false
  256. //if there is any, i will show an alert to the user, and once click the alert it will takes them to a page that show the detail of the reservation.
  257. try {
  258. const reservationHistories = await chargeStationService.fetchReservationHistories();
  259. // console.log('reservationHistories', reservationHistories);
  260. //here if i successfully fetch the reservationHistories, i will check if there are penalty, if i cannot fetch, i will simply continue the payment flow
  261. if (reservationHistories || Array.isArray(reservationHistories)) {
  262. const unpaidPenalties = reservationHistories.filter(
  263. (reservation: any) => reservation.penalty_fee > 0 && reservation.penalty_paid_status === false
  264. );
  265. const mostRecentUnpaidReservation = unpaidPenalties.reduce((mostRecent: any, current: any) => {
  266. return new Date(mostRecent.created_at) > new Date(current.created_at) ? mostRecent : current;
  267. }, unpaidPenalties[0]);
  268. if (unpaidPenalties.length > 0) {
  269. Alert.alert(
  270. t('payment.unpaid_penalty_title'),
  271. t('payment.unpaid_penalty_message'),
  272. [
  273. {
  274. text: t('payment.view_details'),
  275. onPress: () => {
  276. // Navigate to a page showing penalty details
  277. cleanupData();
  278. router.push({
  279. pathname: '(auth)/(tabs)/(home)/penaltyPaymentPage',
  280. params: {
  281. book_time: mostRecentUnpaidReservation.book_time,
  282. end_time: mostRecentUnpaidReservation.end_time,
  283. actual_end_time: mostRecentUnpaidReservation.actual_end_time,
  284. penalty_fee: mostRecentUnpaidReservation.penalty_fee,
  285. format_order_id: mostRecentUnpaidReservation.format_order_id,
  286. id: mostRecentUnpaidReservation.id
  287. }
  288. });
  289. }
  290. },
  291. {
  292. text: t('scanQr.back'),
  293. onPress: () => {
  294. cleanupData();
  295. if (router.canGoBack()) {
  296. router.push('/mainPage');
  297. } else {
  298. router.push('/mainPage');
  299. }
  300. }
  301. }
  302. ],
  303. { cancelable: false }
  304. );
  305. return;
  306. }
  307. }
  308. } catch (error) {
  309. Alert.alert(t('common.error'), t('scanQr.failed_fetch_reservations'));
  310. }
  311. const now = new Date();
  312. const end_time_map: {
  313. [key: number]: number;
  314. } = {
  315. 20: 25,
  316. 25: 30,
  317. 30: 40,
  318. 40: 45,
  319. 80: 120
  320. };
  321. const end_time = new Date(now.getTime() + end_time_map[total_power] * 60000);
  322. const payloadForPay = {
  323. stationID: stationID,
  324. connector: scanned_qr_code,
  325. user: user_id.data.id,
  326. book_time: now.toISOString(),
  327. end_time: end_time.toISOString(),
  328. total_power: total_power,
  329. total_fee: total_power * currentPriceTotalPayment,
  330. promotion_code: promotion_code,
  331. with_coupon: promotion_code.length > 0 ? true : false,
  332. car: car.data.id,
  333. type: 'walking',
  334. is_ic_call: false
  335. };
  336. // check if user has enough wallet, if not, link to qf pay page
  337. if (totalPrice === null) {
  338. Alert.alert(t('common.error'), t('scanQr.failed_fetch_totalprice'), [
  339. {
  340. text: t('common.ok'),
  341. onPress: () => {
  342. cleanupData();
  343. router.push('/mainPage');
  344. }
  345. }
  346. ]);
  347. return;
  348. }
  349. if (walletBalance < totalPrice) {
  350. const needToPay = totalPrice - walletBalance;
  351. oneTimeCharging(needToPay);
  352. return;
  353. } else {
  354. // if user has enough wallet, proceed to payment
  355. try {
  356. const response = await walletService.newSubmitPayment(
  357. payloadForPay.stationID,
  358. payloadForPay.connector,
  359. payloadForPay.user,
  360. payloadForPay.book_time,
  361. payloadForPay.end_time,
  362. payloadForPay.total_power,
  363. payloadForPay.total_fee,
  364. payloadForPay.promotion_code,
  365. payloadForPay.with_coupon,
  366. payloadForPay.car,
  367. payloadForPay.type,
  368. payloadForPay.is_ic_call
  369. );
  370. if (response.error) {
  371. console.log('Error1', response.error);
  372. // Handle error response from the service
  373. Alert.alert(t('payment.scan_failed'), response.message || t('payment.unknown_error'), [
  374. {
  375. text: t('payment.back_main'),
  376. onPress: () => {
  377. cleanupData();
  378. router.push('/mainPage');
  379. }
  380. }
  381. ]);
  382. return;
  383. }
  384. if (response === 200 || response === 201) {
  385. Alert.alert(t('payment.charging_started_title'), t('payment.charging_started_message'), [
  386. {
  387. text: t('payment.confirm'),
  388. onPress: showLoadingAndNavigate
  389. }
  390. ]);
  391. } else {
  392. console.log('Error111', response, payloadForPay.connector);
  393. Alert.alert(t('payment.scan_failed'), response.error_msg || t('payment.unknown_error'), [
  394. {
  395. text: t('payment.back_main'),
  396. onPress: () => {
  397. cleanupData();
  398. router.push('/mainPage');
  399. }
  400. }
  401. ]);
  402. }
  403. } catch (error) {
  404. console.error('Payment submission failed:', error);
  405. Alert.alert(t('common.error'), t('payment.payment_submit_failed'), [
  406. {
  407. text: t('common.ok'),
  408. onPress: () => {
  409. cleanupData();
  410. router.push('/mainPage');
  411. }
  412. }
  413. ]);
  414. }
  415. }
  416. } catch (error) {
  417. } finally {
  418. setLoading(false);
  419. }
  420. };
  421. function formatTime(utcTimeString: any) {
  422. // Parse the UTC time string
  423. const date = new Date(utcTimeString);
  424. // Add 8 hours
  425. date.setHours(date.getHours());
  426. // Format the date
  427. const year = date.getFullYear();
  428. const month = String(date.getMonth() + 1).padStart(2, '0');
  429. const day = String(date.getDate()).padStart(2, '0');
  430. const hours = String(date.getHours()).padStart(2, '0');
  431. const minutes = String(date.getMinutes()).padStart(2, '0');
  432. const seconds = String(date.getSeconds()).padStart(2, '0');
  433. // Return the formatted string
  434. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  435. }
  436. const oneTimeCharging = async (inputAmount: number) => {
  437. try {
  438. const response = await walletService.getOutTradeNo();
  439. if (response) {
  440. setOutTradeNo(response);
  441. setIsExpectingPayment(true);
  442. paymentInitiatedTime.current = new Date().getTime();
  443. const now = new Date();
  444. const formattedTime = formatTime(now);
  445. let amount = Math.round(inputAmount * 100);
  446. const origin = 'https://openapi-hk.qfapi.com/checkstand/#/?';
  447. const obj = {
  448. // appcode: '6937EF25DF6D4FA78BB2285441BC05E9',
  449. appcode: '636E234FB30D43598FC8F0140A1A7282',
  450. goods_name: t('payment.wallet_top_up'),
  451. out_trade_no: response,
  452. paysource: 'crazycharge_checkout',
  453. return_url: 'https://crazycharge.com.hk/completed',
  454. failed_url: 'https://crazycharge.com.hk/failed',
  455. notify_url: 'https://api.crazycharge.com.hk/api/v1/clients/qfpay/webhook',
  456. sign_type: 'sha256',
  457. txamt: amount,
  458. txcurrcd: 'HKD',
  459. txdtm: formattedTime
  460. };
  461. const paramStringify = (json, flag?) => {
  462. let str = '';
  463. let keysArr = Object.keys(json);
  464. keysArr.sort().forEach((val) => {
  465. if (json[val] === undefined || json[val] === null) return;
  466. str += `${val}=${flag ? encodeURIComponent(json[val]) : json[val]}&`;
  467. });
  468. return str.slice(0, -1);
  469. };
  470. // const api_key = '8F59E31F6ADF4D2894365F2BB6D2FF2C';
  471. const api_key = '3E2727FBA2DA403EA325E73F36B07824';
  472. const params = paramStringify(obj);
  473. const sign = sha256(`${params}${api_key}`).toString();
  474. const url = `${origin}${paramStringify(obj, true)}&sign=${sign}`;
  475. try {
  476. const supported = await Linking.canOpenURL(url);
  477. if (supported) {
  478. Alert.alert('', t('payment.insufficient_balance_redirect'), [
  479. {
  480. text: t('payment.ok'),
  481. onPress: async () => {
  482. await Linking.openURL(url);
  483. }
  484. }
  485. ]);
  486. } else {
  487. Alert.alert(t('common.error'), t('payment.try_again_later'));
  488. }
  489. } catch (error) {
  490. console.error('Top-up failed:', error);
  491. Alert.alert(t('common.error'), t('payment.one_time_payment_failed'));
  492. }
  493. } else {
  494. Alert.alert(t('common.error'), t('payment.failed_fetch_outtrade'));
  495. }
  496. } catch (error) {
  497. Alert.alert(t('common.error'), t('payment.one_time_payment_failed'));
  498. }
  499. };
  500. return (
  501. <SafeAreaView className="flex-1 bg-white" edges={['top', 'left', 'right']}>
  502. <Modal transparent={true} visible={loadingModalVisible} animationType="fade">
  503. <View className="flex-1 justify-center items-center bg-black/50">
  504. <View className="bg-white p-6 rounded-lg items-center">
  505. <ActivityIndicator size="large" color="#02677D" />
  506. <Text className="mt-3">{t('common.please_wait')}</Text>
  507. </View>
  508. </View>
  509. </Modal>
  510. <ScrollView className="flex-1 mx-[5%]" showsVerticalScrollIndicator={false}>
  511. <View style={{ marginTop: 25 }}>
  512. <Pressable
  513. onPress={() => {
  514. if (router.canGoBack()) {
  515. router.back();
  516. } else {
  517. cleanupData();
  518. router.replace('/scanQrPage');
  519. }
  520. }}
  521. >
  522. <PreviousPageBlackSvg />
  523. </Pressable>
  524. </View>
  525. <View style={{ marginTop: 25 }}>
  526. <Text style={{ fontSize: 45, paddingBottom: 12 }}>{t('payment.summary_title')}</Text>
  527. <View>
  528. <View className="flex-row justify-between">
  529. <Text className="text-base lg:text-lg ">{t('payment.charging_fee')}</Text>
  530. <Text className="text-base lg:text-lg">
  531. HK $ {currentPriceTotalPayment ? currentPriceTotalPayment * total_power : t('common.loading')}
  532. </Text>
  533. </View>
  534. <Text style={styles.grayColor} className="text-sm lg:text-base mt-4">
  535. {t('payment.settled_kwh')}: {total_power == 80 ? t('payment.full_charge') : `${total_power} KWh`}
  536. </Text>
  537. <Text style={styles.grayColor} className="text-sm lg:text-base mt-4">
  538. {t('payment.price_per_kwh')}: $ {currentPriceTotalPayment ? currentPriceTotalPayment : t('common.loading')}
  539. </Text>
  540. <Text style={styles.grayColor} className="text-sm lg:text-base mt-4">
  541. {t('payment.note')}
  542. </Text>
  543. <View className="h-0.5 my-3 bg-[#f4f4f4]" />
  544. {processed_coupon_store && processed_coupon_store.length > 0 && (
  545. <Text className="text-base lg:text-lg mb-4 lg:mb-6">{t('payment.coupon')}</Text>
  546. )}
  547. {processed_coupon_store &&
  548. processed_coupon_store?.map((couponObj: any) => (
  549. <View
  550. key={`${couponObj.coupon_detail.amount}-${couponObj.coupon_detail.expire_date}`}
  551. className="flex flex-row items-center justify-between"
  552. >
  553. <View className="flex flex-row items-start ">
  554. <Image
  555. className="w-6 lg:w-8 xl:w-10 h-6 lg:h-8 xl:h-10"
  556. source={require('../../../../assets/couponlogo.png')}
  557. />
  558. <View key={couponObj.coupon_detail.id} className="flex flex-col ml-2 lg:ml-4 ">
  559. <Text className="text-base lg:text-xl text-[#888888] ">
  560. ${couponObj.coupon_detail.amount} {t('wallet.coupon.cash_voucher')}
  561. </Text>
  562. <Text className=" text-sm lg:text-base my-1 lg:mt-2 lg:mb-4 text-[#888888]">
  563. {t('wallet.coupon.valid_until')}{' '}
  564. <Text className="font-[500] text-[#02677D]">
  565. {t('common.to_date', { date: couponObj.coupon_detail.expire_date.slice(0, 10) })}
  566. </Text>
  567. </Text>
  568. </View>
  569. </View>
  570. {/* x 1 */}
  571. <View className="flex flex-row items-center">
  572. <Text className="text-sm lg:text-base">X {' '}</Text>
  573. <View className="w-8 h-8 rounded-full bg-[#02677D] flex items-center justify-center">
  574. <Text className="text-white text-center text-lg">
  575. {couponObj.frequency}
  576. </Text>
  577. </View>
  578. </View>
  579. </View>
  580. ))}
  581. {processed_coupon_store && processed_coupon_store.length > 0 && (
  582. <View className="h-0.5 my-3 bg-[#f4f4f4]" />
  583. )}
  584. <View className="flex-row justify-between">
  585. <Text className="text-xl">{t('payment.total')}</Text>
  586. <Text className="text-3xl">HK$ {totalPrice !== null ? totalPrice : t('common.loading')}</Text>
  587. </View>
  588. <View className="mt-4 ">
  589. <NormalButton
  590. title={
  591. <Text
  592. style={{
  593. color: 'white',
  594. fontSize: 16,
  595. fontWeight: '800'
  596. }}
  597. >
  598. {loading ? t('common.processing') : t('payment.confirm_payment')}
  599. </Text>
  600. }
  601. onPress={handlePay}
  602. extendedStyle={{ padding: 24 }}
  603. />
  604. </View>
  605. <View className="h-8" />
  606. </View>
  607. </View>
  608. </ScrollView>
  609. </SafeAreaView>
  610. );
  611. };
  612. export default TotalPayment;
  613. const styles = StyleSheet.create({
  614. grayColor: {
  615. color: '#888888'
  616. },
  617. greenColor: {
  618. color: '#02677D'
  619. }
  620. });