| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648 |
- 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<number | null>(null);
- const [walletBalance, setWalletBalance] = useState<number | null>(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<number | null>(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;
- };
- 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(
- '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));
- 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('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) {
- 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('掃描失敗 請稍後再試。', response.message || '未知錯誤', [
- {
- text: '返回主頁',
- onPress: () => {
- cleanupData();
- router.push('/mainPage');
- }
- }
- ]);
- return;
- }
- if (response === 200 || response === 201) {
- Alert.alert('啟動成功', '請按下確認並等待頁面稍後自動跳轉至充電介面', [
- {
- text: '確認',
- onPress: showLoadingAndNavigate
- }
- ]);
- } else {
- console.log('Error111', response, payloadForPay.connector);
- 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) {
- } 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: '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] === 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('', '偵測到您錢包餘額不足,現在為您跳轉到充值頁面', [
- {
- 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 (
- <SafeAreaView className="flex-1 bg-white" edges={['top', 'left', 'right']}>
- <Modal transparent={true} visible={loadingModalVisible} animationType="fade">
- <View className="flex-1 justify-center items-center bg-black/50">
- <View className="bg-white p-6 rounded-lg items-center">
- <ActivityIndicator size="large" color="#02677D" />
- <Text className="mt-3">請稍候...</Text>
- </View>
- </View>
- </Modal>
- <ScrollView className="flex-1 mx-[5%]" showsVerticalScrollIndicator={false}>
- <View style={{ marginTop: 25 }}>
- <Pressable
- onPress={() => {
- if (router.canGoBack()) {
- router.back();
- } else {
- cleanupData();
- router.replace('/scanQrPage');
- }
- }}
- >
- <PreviousPageBlackSvg />
- </Pressable>
- </View>
- <View style={{ marginTop: 25 }}>
- <Text style={{ fontSize: 45, paddingBottom: 12 }}>付款概要</Text>
- <View>
- <View className="flex-row justify-between">
- <Text className="text-base lg:text-lg ">充電費用</Text>
- <Text className="text-base lg:text-lg">
- HK $ {currentPriceTotalPayment ? currentPriceTotalPayment * total_power : 'Loading...'}
- </Text>
- </View>
- <Text style={styles.grayColor} className="text-sm lg:text-base mt-4">
- 結算電度數 : {total_power == 80 ? '充滿停機' : `${total_power} KWh`}
- </Text>
- <Text style={styles.grayColor} className="text-sm lg:text-base mt-4">
- 每度電價錢 : $ {currentPriceTotalPayment ? currentPriceTotalPayment : 'Loading...'}
- </Text>
- <View className="h-0.5 my-3 bg-[#f4f4f4]" />
- {processed_coupon_store && processed_coupon_store.length > 0 && (
- <Text className="text-base lg:text-lg mb-4 lg:mb-6">優惠劵</Text>
- )}
- {processed_coupon_store &&
- processed_coupon_store?.map((couponObj: any) => (
- <View
- key={`${couponObj.coupon_detail.amount}-${couponObj.coupon_detail.expire_date}`}
- className="flex flex-row items-center justify-between"
- >
- <View className="flex flex-row items-start ">
- <Image
- className="w-6 lg:w-8 xl:w-10 h-6 lg:h-8 xl:h-10"
- source={require('../../../../assets/couponlogo.png')}
- />
- <View key={couponObj.coupon_detail.id} className="flex flex-col ml-2 lg:ml-4 ">
- <Text className="text-base lg:text-xl text-[#888888] ">
- ${couponObj.coupon_detail.amount} 現金劵
- </Text>
- <Text className=" text-sm lg:text-base my-1 lg:mt-2 lg:mb-4 text-[#888888]">
- 有效期{' '}
- <Text className="font-[500] text-[#02677D]">
- 至 {couponObj.coupon_detail.expire_date.slice(0, 10)}
- </Text>
- </Text>
- </View>
- </View>
- {/* x 1 */}
- <View className="flex flex-row items-center">
- <Text className="text-sm lg:text-base">X {' '}</Text>
- <View className="w-8 h-8 rounded-full bg-[#02677D] flex items-center justify-center">
- <Text className="text-white text-center text-lg">
- {couponObj.frequency}
- </Text>
- </View>
- </View>
- </View>
- ))}
- {processed_coupon_store && processed_coupon_store.length > 0 && (
- <View className="h-0.5 my-3 bg-[#f4f4f4]" />
- )}
- <View className="flex-row justify-between ">
- <Text className="text-xl">總計</Text>
- <Text className="text-3xl">HK$ {totalPrice !== null ? totalPrice : 'Loading...'}</Text>
- </View>
- <View className="mt-4 ">
- <NormalButton
- title={
- <Text
- style={{
- color: 'white',
- fontSize: 16,
- fontWeight: '800'
- }}
- >
- {loading ? '處理中...' : '付款確認'}
- </Text>
- }
- onPress={handlePay}
- extendedStyle={{ padding: 24 }}
- />
- </View>
- <View className="h-8" />
- </View>
- </View>
- </ScrollView>
- </SafeAreaView>
- );
- };
- export default TotalPayment;
- const styles = StyleSheet.create({
- grayColor: {
- color: '#888888'
- },
- greenColor: {
- color: '#02677D'
- }
- });
|