// app/(auth)/(tabs)/(home)/totalPayment.tsx import { router, useFocusEffect, useNavigation } from 'expo-router'; import { View, Text, ScrollView, Pressable, StyleSheet, Image, BackHandler, Alert, Linking, AppState, Modal, ActivityIndicator } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import NormalButton from '../../../../component/global/normal_button'; import { PreviousPageBlackSvg } from '../../../../component/global/SVG'; import { useChargingStore } from '../../../../providers/scan_qr_payload_store'; import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import useUserInfoStore from '../../../../providers/userinfo_store'; import { chargeStationService } from '../../../../service/chargeStationService'; import { authenticationService } from '../../../../service/authService'; import axios from 'axios'; import sha256 from 'crypto-js/sha256'; import { walletService } from '../../../../service/walletService'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useTranslation } from '../../../../util/hooks/useTranslation'; const TotalPayment = () => { const { t } = useTranslation(); const { promotion_code, stationID, sum_of_coupon, scanned_qr_code, coupon_detail, total_power, processed_coupon_store, setPromotionCode, setCouponDetail, setTotalPower, setProcessedCouponStore, setSumOfCoupon, setCurrentPriceStore } = useChargingStore(); const [currentPriceTotalPayment, setCurrentPriceTotalPayment] = useState(null); const [walletBalance, setWalletBalance] = useState(null); const [loading, setLoading] = useState(false); const [outTradeNo, setOutTradeNo] = useState(''); const [isExpectingPayment, setIsExpectingPayment] = useState(false); const paymentInitiatedTime = useRef(null); const PAYMENT_CHECK_TIMEOUT = 5 * 60 * 1000; // 5 minutes in milliseconds const appState = useRef(AppState.currentState); const [totalPrice, setTotalPrice] = useState(null); const [paymentStatus, setPaymentStatus] = useState(null); const [loadingModalVisible, setLoadingModalVisible] = useState(false); //fetch current price based on using coupon or not. use coupon = $3.5 useEffect(() => { const fetchCurrentPrice = async () => { try { //if promotion_code.length > 0, fetch original price, otherwise fetch current price //then calculate total price for display purpose if (promotion_code.length > 0) { const response = await chargeStationService.getOriginalPriceInPay(stationID); setCurrentPriceTotalPayment(response); let totalPrice = Number(total_power) * Number(response) - Number(sum_of_coupon); if (totalPrice < 0) { totalPrice = 0; } else { totalPrice = totalPrice; } setTotalPrice(totalPrice); } else { const response = await chargeStationService.getCurrentPriceInPay(stationID); setCurrentPriceTotalPayment(response); let totalPrice = Number(total_power) * Number(response); setTotalPrice(totalPrice); } } catch (error) { // More specific error handling if (axios.isAxiosError(error)) { const errorMessage = error.response?.data?.message || 'Network error occurred'; Alert.alert(t('common.error'), `${t('wallet.error_fetching_balance')}: ${errorMessage}`, [ { text: t('common.ok'), onPress: () => { cleanupData(); router.push('/mainPage'); } } ]); } else { Alert.alert(t('common.error'), t('wallet.error_fetching_balance'), [ { text: t('common.ok'), onPress: () => { cleanupData(); router.push('/mainPage'); } } ]); } } }; fetchCurrentPrice(); }, []); // Add this effect to handle Android back button useFocusEffect( useCallback(() => { const onBackPress = () => { cleanupData(); if (router.canGoBack()) { router.back(); } else { router.replace('/scanQrPage'); } return true; }; const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress); return () => subscription.remove() // return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress); }, []) ); //check payment status useEffect(() => { const subscription = AppState.addEventListener('change', (nextAppState: any) => { if ( appState.current.match(/inactive|background/) && nextAppState === 'active' && isExpectingPayment && // outTradeNo && paymentInitiatedTime.current ) { const currentTime = new Date().getTime(); if (currentTime - paymentInitiatedTime.current < PAYMENT_CHECK_TIMEOUT) { checkPaymentStatus(); } else { // Payment check timeout reached setIsExpectingPayment(false); setOutTradeNo(''); paymentInitiatedTime.current = null; Alert.alert( t('payment.payment_timeout_title'), t('payment.payment_timeout_message') ); } } appState.current = nextAppState; }); return () => { subscription.remove(); }; }, [outTradeNo, isExpectingPayment]); const navigation = useNavigation(); useLayoutEffect(() => { navigation.setOptions({ gestureEnabled: false }); }, [navigation]); const checkPaymentStatus = async () => { try { // console.log('outTradeNo in scanQR Page checkpaymentstatus ', outTradeNo); const result = await walletService.checkPaymentStatus(outTradeNo); setPaymentStatus(result); // console.log('checkPaymentStatus from scan QR checkpaymentstatus', result); if (result && !result.some((item) => item.errmsg?.includes(t('payment.processing')))) { // Payment successful // console.log('totalFee', totalFee); Alert.alert(t('payment.payment_success_title'), t('payment.payment_success_message', { amount: totalPrice }), [ { text: t('payment.confirm'), onPress: async () => { cleanupData(); router.push('/mainPage'); } } ]); } else { Alert.alert(t('payment.payment_failed_title'), t('payment.payment_failed_message'), [ { text: t('payment.ok'), onPress: () => { cleanupData(); router.push('/mainPage'); } } ]); } setIsExpectingPayment(false); setOutTradeNo(''); paymentInitiatedTime.current = null; } catch (error) { console.error('Failed to check payment status:', error); Alert.alert(t('common.error'), t('payment.payment_status_check_failed')); } }; const showLoadingAndNavigate = async () => { setLoadingModalVisible(true); // Wait for 2 seconds await new Promise((resolve) => setTimeout(resolve, 2000)); cleanupData(); setLoadingModalVisible(false); router.navigate('(auth)/(tabs)/(home)/mainPage'); router.push('(auth)/(tabs)/(charging)/chargingPage'); }; const cleanupData = () => { setPromotionCode([]); setCouponDetail([]); setProcessedCouponStore([]); setSumOfCoupon(0); setTotalPower(null); }; const handlePay = async () => { try { let car, user_id, walletBalance, price_for_pay; setLoading(true); if (currentPriceTotalPayment === null) { Alert.alert(t('payment.processing'), t('payment.wait_loading_price')); return; } //fetch car with proper try catch try { car = await chargeStationService.getUserDefaultCars(); if (!car?.data?.id) { Alert.alert(t('payment.error_title'), t('payment.failed_fetch_udcc')); return; } } catch (error) { console.error('Failed to fetch user default car:', error); Alert.alert(t('payment.error_title'), t('payment.failed_fetch_udc')); return; } //fetch user id with proper try catch try { user_id = await authenticationService.getUserInfo(); if (!user_id?.data?.id) { Alert.alert(t('payment.error_title'), t('payment.failed_fetch_userid')); return; } } catch (error) { console.error('Failed to fetch user ID:', error); Alert.alert(t('payment.error_title'), t('payment.failed_fetch_userid')); return; } // fetch user wallet with proper try catch try { walletBalance = await walletService.getWalletBalance(); } catch (error) { console.error('Failed to fetch user wallet:', error); Alert.alert(t('payment.error_title'), t('payment.failed_fetch_wallet')); return; } //now i have all information ready, i check penalty reservation //by first fetching all history, then check if penalty_fee > 0 and penalty_paid_status is false //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. try { const reservationHistories = await chargeStationService.fetchReservationHistories(); // console.log('reservationHistories', reservationHistories); //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 if (reservationHistories || Array.isArray(reservationHistories)) { const unpaidPenalties = reservationHistories.filter( (reservation: any) => reservation.penalty_fee > 0 && reservation.penalty_paid_status === false ); const mostRecentUnpaidReservation = unpaidPenalties.reduce((mostRecent: any, current: any) => { return new Date(mostRecent.created_at) > new Date(current.created_at) ? mostRecent : current; }, unpaidPenalties[0]); if (unpaidPenalties.length > 0) { Alert.alert( t('payment.unpaid_penalty_title'), t('payment.unpaid_penalty_message'), [ { text: t('payment.view_details'), onPress: () => { // Navigate to a page showing penalty details cleanupData(); router.push({ pathname: '(auth)/(tabs)/(home)/penaltyPaymentPage', params: { book_time: mostRecentUnpaidReservation.book_time, end_time: mostRecentUnpaidReservation.end_time, actual_end_time: mostRecentUnpaidReservation.actual_end_time, penalty_fee: mostRecentUnpaidReservation.penalty_fee, format_order_id: mostRecentUnpaidReservation.format_order_id, id: mostRecentUnpaidReservation.id } }); } }, { text: t('scanQr.back'), onPress: () => { cleanupData(); if (router.canGoBack()) { router.push('/mainPage'); } else { router.push('/mainPage'); } } } ], { cancelable: false } ); return; } } } catch (error) { Alert.alert(t('common.error'), t('scanQr.failed_fetch_reservations')); } const now = new Date(); const end_time_map: { [key: number]: number; } = { 20: 25, 25: 30, 30: 40, 40: 45, 80: 120 }; const end_time = new Date(now.getTime() + end_time_map[total_power] * 60000); const payloadForPay = { stationID: stationID, connector: scanned_qr_code, user: user_id.data.id, book_time: now.toISOString(), end_time: end_time.toISOString(), total_power: total_power, total_fee: total_power * currentPriceTotalPayment, promotion_code: promotion_code, with_coupon: promotion_code.length > 0 ? true : false, car: car.data.id, type: 'walking', is_ic_call: false }; // check if user has enough wallet, if not, link to qf pay page if (totalPrice === null) { Alert.alert(t('common.error'), t('scanQr.failed_fetch_totalprice'), [ { text: t('common.ok'), onPress: () => { cleanupData(); router.push('/mainPage'); } } ]); return; } if (walletBalance < totalPrice) { const needToPay = totalPrice - walletBalance; oneTimeCharging(needToPay); return; } else { // if user has enough wallet, proceed to payment try { const response = await walletService.newSubmitPayment( payloadForPay.stationID, payloadForPay.connector, payloadForPay.user, payloadForPay.book_time, payloadForPay.end_time, payloadForPay.total_power, payloadForPay.total_fee, payloadForPay.promotion_code, payloadForPay.with_coupon, payloadForPay.car, payloadForPay.type, payloadForPay.is_ic_call ); if (response.error) { console.log('Error1', response.error); // Handle error response from the service Alert.alert(t('payment.scan_failed'), response.message || t('payment.unknown_error'), [ { text: t('payment.back_main'), onPress: () => { cleanupData(); router.push('/mainPage'); } } ]); return; } if (response === 200 || response === 201) { Alert.alert(t('payment.charging_started_title'), t('payment.charging_started_message'), [ { text: t('payment.confirm'), onPress: showLoadingAndNavigate } ]); } else { console.log('Error111', response, payloadForPay.connector); Alert.alert(t('payment.scan_failed'), response.error_msg || t('payment.unknown_error'), [ { text: t('payment.back_main'), onPress: () => { cleanupData(); router.push('/mainPage'); } } ]); } } catch (error) { console.error('Payment submission failed:', error); Alert.alert(t('common.error'), t('payment.payment_submit_failed'), [ { text: t('common.ok'), onPress: () => { cleanupData(); router.push('/mainPage'); } } ]); } } } catch (error) { } finally { setLoading(false); } }; function formatTime(utcTimeString: any) { // Parse the UTC time string const date = new Date(utcTimeString); // Add 8 hours date.setHours(date.getHours()); // Format the date const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); // Return the formatted string return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } const oneTimeCharging = async (inputAmount: number) => { try { const response = await walletService.getOutTradeNo(); if (response) { setOutTradeNo(response); setIsExpectingPayment(true); paymentInitiatedTime.current = new Date().getTime(); const now = new Date(); const formattedTime = formatTime(now); let amount = Math.round(inputAmount * 100); const origin = 'https://openapi-hk.qfapi.com/checkstand/#/?'; const obj = { // appcode: '6937EF25DF6D4FA78BB2285441BC05E9', appcode: '636E234FB30D43598FC8F0140A1A7282', goods_name: t('payment.wallet_top_up'), out_trade_no: response, paysource: 'crazycharge_checkout', return_url: 'https://crazycharge.com.hk/completed', failed_url: 'https://crazycharge.com.hk/failed', notify_url: 'https://api.crazycharge.com.hk/api/v1/clients/qfpay/webhook', sign_type: 'sha256', txamt: amount, txcurrcd: 'HKD', txdtm: formattedTime }; const paramStringify = (json, flag?) => { let str = ''; let keysArr = Object.keys(json); keysArr.sort().forEach((val) => { if (json[val] === undefined || json[val] === null) return; str += `${val}=${flag ? encodeURIComponent(json[val]) : json[val]}&`; }); return str.slice(0, -1); }; // const api_key = '8F59E31F6ADF4D2894365F2BB6D2FF2C'; const api_key = '3E2727FBA2DA403EA325E73F36B07824'; const params = paramStringify(obj); const sign = sha256(`${params}${api_key}`).toString(); const url = `${origin}${paramStringify(obj, true)}&sign=${sign}`; try { const supported = await Linking.canOpenURL(url); if (supported) { Alert.alert('', t('payment.insufficient_balance_redirect'), [ { text: t('payment.ok'), onPress: async () => { await Linking.openURL(url); } } ]); } else { Alert.alert(t('common.error'), t('payment.try_again_later')); } } catch (error) { console.error('Top-up failed:', error); Alert.alert(t('common.error'), t('payment.one_time_payment_failed')); } } else { Alert.alert(t('common.error'), t('payment.failed_fetch_outtrade')); } } catch (error) { Alert.alert(t('common.error'), t('payment.one_time_payment_failed')); } }; return ( {t('common.please_wait')} { if (router.canGoBack()) { router.back(); } else { cleanupData(); router.replace('/scanQrPage'); } }} > {t('payment.summary_title')} {t('payment.charging_fee')} HK $ {currentPriceTotalPayment ? currentPriceTotalPayment * total_power : t('common.loading')} {t('payment.settled_kwh')}: {total_power == 80 ? t('payment.full_charge') : `${total_power} KWh`} {t('payment.price_per_kwh')}: $ {currentPriceTotalPayment ? currentPriceTotalPayment : t('common.loading')} {t('payment.note')} {processed_coupon_store && processed_coupon_store.length > 0 && ( {t('payment.coupon')} )} {processed_coupon_store && processed_coupon_store?.map((couponObj: any) => ( ${couponObj.coupon_detail.amount} {t('wallet.coupon.cash_voucher')} {t('wallet.coupon.valid_until')}{' '} {t('common.to_date', { date: couponObj.coupon_detail.expire_date.slice(0, 10) })} {/* x 1 */} X {' '} {couponObj.frequency} ))} {processed_coupon_store && processed_coupon_store.length > 0 && ( )} {t('payment.total')} HK$ {totalPrice !== null ? totalPrice : t('common.loading')} {loading ? t('common.processing') : t('payment.confirm_payment')} } onPress={handlePay} extendedStyle={{ padding: 24 }} /> ); }; export default TotalPayment; const styles = StyleSheet.create({ grayColor: { color: '#888888' }, greenColor: { color: '#02677D' } });