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'; const TotalPayment = () => { 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('Error', `Unable to fetch price: ${errorMessage}`, [ { text: 'OK', onPress: () => { cleanupData(); router.push('/mainPage'); } } ]); } else { Alert.alert('Error', 'An unexpected error occurred while fetching the price', [ { text: '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; }; BackHandler.addEventListener('hardwareBackPress', onBackPress); 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( 'Payment Timeout', 'The payment status check has timed out. Please check your payment history.' ); } } 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('處理中'))) { // Payment successful // console.log('totalFee', totalFee); Alert.alert('付款已成功', `你已成功增值。請重新掃描去啟動充電槍。`, [ { text: '確認', onPress: async () => { cleanupData(); router.push('/mainPage'); } } ]); } else { Alert.alert('付款失敗', '請再試一次。', [ { text: '確定', onPress: () => { cleanupData(); router.push('/mainPage'); } } ]); } setIsExpectingPayment(false); setOutTradeNo(''); paymentInitiatedTime.current = null; } catch (error) { console.error('Failed to check payment status:', error); Alert.alert('Error', 'Failed to check payment status. Please check your payment history.'); } }; const showLoadingAndNavigate = async () => { setLoadingModalVisible(true); // Wait for 2 seconds await new Promise((resolve) => setTimeout(resolve, 2000)); 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('Please wait', 'Still loading price information...'); return; } //fetch car with proper try catch try { car = await chargeStationService.getUserDefaultCars(); if (!car?.data?.id) { Alert.alert('Failed to fetch UDCC', 'Please try again later'); return; } } catch (error) { console.error('Failed to fetch user default car:', error); Alert.alert('Failed to fetch UDC', 'Please try again later'); return; } //fetch user id with proper try catch try { user_id = await authenticationService.getUserInfo(); if (!user_id?.data?.id) { Alert.alert('Failed to fetch userID', 'Please try again later'); return; } } catch (error) { console.error('Failed to fetch user ID:', error); Alert.alert('Failed to fetch user ID', 'Please try again later'); 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('Failed to fetch user wallet', 'Please try again later'); 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( '未付罰款', '您有未支付的罰款。請先支付罰款後再重新掃描充電。', [ { text: '查看詳情', 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: '返回', onPress: () => { cleanupData(); if (router.canGoBack()) { router.push('/mainPage'); } else { router.push('/mainPage'); } } } ], { cancelable: false } ); return; } } } catch (error) { Alert.alert('Error', 'Failed to fetch reservation histories for penalty checking purpose'); } 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('Error', 'Unable to fetch totalPrice', [ { text: 'OK', onPress: () => { cleanupData(); router.push('/mainPage'); } } ]); return; } if (walletBalance < totalPrice) { oneTimeCharging(totalPrice); 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 ); console.log('response in new submit payyment in total payment', response); if (response.error) { console.log('bbbbbb'); // Handle error response from the service Alert.alert('掃描失敗 請稍後再試。', response.message || '未知錯誤', [ { text: '返回主頁', onPress: () => { cleanupData(); router.push('/mainPage'); } } ]); return; } if (response === 200 || response === 201) { Alert.alert('啟動成功', '請按下確認並等待頁面稍後自動跳轉至充電介面', [ { text: '確認', onPress: showLoadingAndNavigate } ]); } else { console.log('avvvvvvvvvvvvva'); Alert.alert('掃描失敗 請稍後再試。', response.error_msg || '未知錯誤', [ { text: '返回主頁', onPress: () => { cleanupData(); router.push('/mainPage'); } } ]); } } catch (error) { console.error('Payment submission failed:', error); Alert.alert('錯誤', '付款提交失敗,請稍後再試。', [ { text: 'OK', onPress: () => { cleanupData(); router.push('/mainPage'); } } ]); } } } catch (error) { console.log('I am here in this block', 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 = inputAmount * 100; const origin = 'https://openapi-hk.qfapi.com/checkstand/#/?'; const obj = { // appcode: '6937EF25DF6D4FA78BB2285441BC05E9', appcode: '636E234FB30D43598FC8F0140A1A7282', goods_name: 'Crazy Charge 錢包增值', 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]) 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('', '偵測到您錢包餘額不足,現在為您跳轉到充值頁面', [ { text: '確定', onPress: async () => { await Linking.openURL(url); } } ]); } else { Alert.alert('錯誤', '請稍後再試'); } } catch (error) { console.error('Top-up failed:', error); Alert.alert('Error', '一次性付款失敗,請稍後再試'); } } else { Alert.alert('Error', 'failed to fetch outTradeNo.'); } } catch (error) { Alert.alert('錯誤', '一次性付款失敗,請稍後再試'); } }; return ( 請稍候... { cleanupData(); if (router.canGoBack()) { router.back(); } else { router.replace('/scanQrPage'); } }} > 付款概要 充電費用 HK $ {currentPriceTotalPayment ? currentPriceTotalPayment * total_power : 'Loading...'} 結算電度數 : {total_power == 80 ? '充滿停機' : `${total_power} KWh`} 每度電價錢 : $ {currentPriceTotalPayment ? currentPriceTotalPayment : 'Loading...'} {processed_coupon_store && processed_coupon_store.length > 0 && ( 優惠劵 )} {processed_coupon_store && processed_coupon_store?.map((couponObj: any) => ( ${couponObj.coupon_detail.amount} 現金劵 有效期{' '} 至 {couponObj.coupon_detail.expire_date.slice(0, 10)} {/* x 1 */} X {' '} {couponObj.frequency} ))} {processed_coupon_store && processed_coupon_store.length > 0 && ( )} 總計 HK$ {totalPrice !== null ? totalPrice : 'Loading...'} {loading ? '處理中...' : '付款確認'} } onPress={handlePay} extendedStyle={{ padding: 24 }} /> ); }; export default TotalPayment; const styles = StyleSheet.create({ grayColor: { color: '#888888' }, greenColor: { color: '#02677D' } });