Browse Source

update scan qr for one time

Ian Fung 1 year ago
parent
commit
032f4c4b2e

+ 2 - 2
app.json

@@ -2,7 +2,7 @@
     "expo": {
         "name": "Crazycharge",
         "slug": "template",
-        "version": "1.1.18",
+        "version": "1.1.20",
         "orientation": "portrait",
         "icon": "./assets/CC_Logo.png",
         "userInterfaceStyle": "light",
@@ -24,7 +24,7 @@
             },
             "permissions": ["android.permission.CAMERA", "android.permission.RECORD_AUDIO"],
             "package": "hk.com.crazycharge",
-            "versionCode": 10
+            "versionCode": 12
         },
         "web": {
             "favicon": "./assets/favicon.png"

+ 497 - 124
app/(auth)/(tabs)/(home)/scanQrPage.tsx

@@ -3,7 +3,9 @@ import { useEffect, useRef, useState } from 'react';
 import {
     ActivityIndicator,
     Alert,
+    AppState,
     Dimensions,
+    Linking,
     Pressable,
     ScrollView,
     StyleSheet,
@@ -11,7 +13,7 @@ import {
     Vibration,
     View
 } from 'react-native';
-
+import sha256 from 'crypto-js/sha256';
 import ChooseCarForChargingRow from '../../../../component/global/chooseCarForChargingRow';
 import { CrossLogoWhiteSvg, QuestionSvg } from '../../../../component/global/SVG';
 import { router } from 'expo-router';
@@ -26,103 +28,102 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
 
 const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
 
-const ChooseCar = ({ carData, loading, selectedCar, setSelectedCar }) => {
-    const isLargeScreen = screenHeight >= 800;
-    const defaultImageUrl = require('../../../../assets/car1.png');
+// const ChooseCar = ({ carData, loading, selectedCar, setSelectedCar }) => {
+//     const isLargeScreen = screenHeight >= 800;
+//     const defaultImageUrl = require('../../../../assets/car1.png');
 
-    return (
-        <View
-            style={{
-                ...(isLargeScreen
-                    ? {
-                          marginTop: '10%',
-                          marginBottom: '12%',
-                          paddingBottom: 12
-                      }
-                    : {
-                          flex: 1,
-                          alignItems: 'center',
-                          justifyContent: 'center'
-                      })
-            }}
-        >
-            <View className="justify-center items-center flex-1 ">
-                <View
-                    style={{
-                        ...(isLargeScreen
-                            ? {}
-                            : {
-                                  backgroundColor: 'rgba(0,0,0,0.7)'
-                              })
-                    }}
-                >
-                    {loading ? (
-                        <View className="w-full">
-                            <ActivityIndicator color="#34657b" />
-                        </View>
-                    ) : (
-                        <View className="w-screen  bg-[#000000B3]">
-                            <View className="flex-row items-center justify-between mx-[5%] ">
-                                <Pressable
-                                    className="pt-4 "
-                                    onPress={() => {
-                                        if (router.canGoBack()) {
-                                            router.back();
-                                        } else {
-                                            router.replace('mainPage');
-                                        }
-                                    }}
-                                >
-                                    <CrossLogoWhiteSvg />
-                                </Pressable>
+//     return (
+//         <View
+//             style={{
+//                 ...(isLargeScreen
+//                     ? {
+//                           marginTop: '10%',
+//                           marginBottom: '12%',
+//                           paddingBottom: 12
+//                       }
+//                     : {
+//                           flex: 1,
+//                           alignItems: 'center',
+//                           justifyContent: 'center'
+//                       })
+//             }}
+//         >
+//             <View className="justify-center items-center flex-1 ">
+//                 <View
+//                     style={{
+//                         ...(isLargeScreen
+//                             ? {}
+//                             : {
+//                                   backgroundColor: 'rgba(0,0,0,0.7)'
+//                               })
+//                     }}
+//                 >
+//                     {loading ? (
+//                         <View className="w-full">
+//                             <ActivityIndicator color="#34657b" />
+//                         </View>
+//                     ) : (
+//                         <View className="w-screen  bg-[#000000B3]">
+//                             <View className="flex-row items-center justify-between mx-[5%] ">
+//                                 <Pressable
+//                                     className="pt-4 "
+//                                     onPress={() => {
+//                                         if (router.canGoBack()) {
+//                                             router.back();
+//                                         } else {
+//                                             router.replace('mainPage');
+//                                         }
+//                                     }}
+//                                 >
+//                                     <CrossLogoWhiteSvg />
+//                                 </Pressable>
 
-                                <Text className="text-base text-white pt-2">選擇充電車輛</Text>
-                                <Text className="text-xl text-white pt-2"></Text>
-                            </View>
+//                                 <Text className="text-base text-white pt-2">選擇充電車輛</Text>
+//                                 <Text className="text-xl text-white pt-2"></Text>
+//                             </View>
 
-                            <ScrollView
-                                horizontal={true}
-                                showsHorizontalScrollIndicator={false}
-                                contentContainerStyle={{
-                                    alignItems: 'center',
-                                    flexDirection: 'row',
-                                    marginVertical: 12
-                                }}
-                                className="space-x-2 mx-[5%]"
-                            >
-                                {carData.map((car, index) => (
-                                    <ChooseCarForChargingRow
-                                        key={`${car.name}+${index}`}
-                                        image={car.image}
-                                        onPress={() => {
-                                            setSelectedCar(car.id);
-                                            console.log(car.id);
-                                        }}
-                                        isSelected={selectedCar === car.id}
-                                        // imageUrl={image}
-                                        VehicleName={car.name}
-                                        isDefault={car.isDefault}
-                                    />
-                                ))}
-                            </ScrollView>
-                        </View>
-                    )}
-                </View>
-            </View>
-        </View>
-    );
-};
+//                             <ScrollView
+//                                 horizontal={true}
+//                                 showsHorizontalScrollIndicator={false}
+//                                 contentContainerStyle={{
+//                                     alignItems: 'center',
+//                                     flexDirection: 'row',
+//                                     marginVertical: 12
+//                                 }}
+//                                 className="space-x-2 mx-[5%]"
+//                             >
+//                                 {carData.map((car, index) => (
+//                                     <ChooseCarForChargingRow
+//                                         key={`${car.name}+${index}`}
+//                                         image={car.image}
+//                                         onPress={() => {
+//                                             setSelectedCar(car.id);
+//                                             console.log(car.id);
+//                                         }}
+//                                         isSelected={selectedCar === car.id}
+//                                         // imageUrl={image}
+//                                         VehicleName={car.name}
+//                                         isDefault={car.isDefault}
+//                                     />
+//                                 ))}
+//                             </ScrollView>
+//                         </View>
+//                     )}
+//                 </View>
+//             </View>
+//         </View>
+//     );
+// };
 
 //reminder: scan qr code page, ic call should be false
 const ScanQrPage = () => {
-    const { userID, setUserID } = useUserInfoStore();
-    // State declarations
+    const { userID, currentPrice } = useUserInfoStore();
+    const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
     const [permission, requestPermission] = useCameraPermissions();
     const [scanned, setScanned] = useState(false);
     const viewRef = useRef(null);
     const [scannedResult, setScannedResult] = useState('');
     const [selectedCar, setSelectedCar] = useState('');
-
     const now = new Date();
     const [loading, setLoading] = useState(true);
     const [loading2, setLoading2] = useState(false);
@@ -138,15 +139,13 @@ const ScanQrPage = () => {
         full: false
     });
     const [selectedDuration, setSelectedDuration] = useState(null);
-
-    const planMap = {
-        25: { duration: 40, fee: 70, kWh: 20, displayDuration: 25 },
-        30: { duration: 45, fee: 87.5, kWh: 25, displayDuration: 30 },
-        40: { duration: 55, fee: 105, kWh: 30, displayDuration: 40 },
-        45: { duration: 60, fee: 140, kWh: 40, displayDuration: 45 },
-        full: { duration: 120, fee: 280, displayDuration: '充滿停機' } // Assuming 2 hours for "充滿停機"
-    };
-
+    const appState = useRef(AppState.currentState);
+    const [paymentStatus, setPaymentStatus] = useState(null);
+    const [isExpectingPayment, setIsExpectingPayment] = useState(false);
+    const paymentInitiatedTime = useRef(null);
+    const PAYMENT_CHECK_TIMEOUT = 5 * 60 * 1000; // 5 minutes in milliseconds
+    const [outTradeNo, setOutTradeNo] = useState('');
+    const [totalFee, setTotalFee] = useState(0);
     // Effect for requesting camera permissions
     useEffect(() => {
         (async () => {
@@ -212,24 +211,16 @@ const ScanQrPage = () => {
         fetchDefaultCar();
     }, []);
 
-    if (!permission) {
-        return <View />;
-    }
-
-    if (!permission.granted) {
-        return (
-            <View className="flex-1 justify-center items-center">
-                <Text style={{ textAlign: 'center' }}>
-                    我們需要相機權限來掃描機器上的二維碼,以便識別並啟動充電機器。我們不會儲存或共享任何掃描到的資訊。
-                    請前往設定開啟相機權限
-                </Text>
-            </View>
-        );
-    }
+    const planMap = {
+        25: { duration: 40, kWh: 20, displayDuration: 25, fee: 20 * currentPrice },
+        30: { duration: 45, kWh: 25, displayDuration: 30, fee: 25 * currentPrice },
+        40: { duration: 55, kWh: 30, displayDuration: 40, fee: 30 * currentPrice },
+        45: { duration: 60, kWh: 40, displayDuration: 45, fee: 40 * currentPrice },
+        full: { duration: 120, displayDuration: '充滿停機', fee: 80 * currentPrice }
+    };
 
     // Function to handle barcode scanning
     const handleBarCodeScanned = async ({ bounds, data, type }: { bounds?: any; data: any; type: any }) => {
-        // Check if bounds exists and has the expected properties
         if (
             !bounds ||
             typeof bounds.origin?.x !== 'number' ||
@@ -244,7 +235,6 @@ const ScanQrPage = () => {
             Vibration.vibrate(100);
             console.log(`type: ${type}   data: ${data}  typeofData ${typeof data}`);
 
-            // Continue with the rest of your scanning logic...
             try {
                 const response = await chargeStationService.getTodayReservation();
                 if (response) {
@@ -357,6 +347,7 @@ const ScanQrPage = () => {
 
     const handleDurationSelect = (duration) => {
         setSelectedDuration(duration);
+        console.log(duration);
     };
     const handleCancel = () => {
         setSelectedDuration(null);
@@ -376,15 +367,16 @@ const ScanQrPage = () => {
 
             if (selectedDuration === 'full') {
                 endTime = new Date(now.getTime() + 2 * 60 * 60 * 1000); // 2 hours for "充滿停機"
-                fee = 280;
+                fee = planMap.full.fee;
                 totalPower = 0; // Set to 0 for "充滿停機"
             } else {
                 const durationInMinutes = parseInt(selectedDuration);
                 endTime = new Date(now.getTime() + durationInMinutes * 60 * 1000);
+                console.log('endTime', endTime);
                 fee = planMap[selectedDuration].fee;
                 totalPower = durationInMinutes; // Use the actual duration for other cases
             }
-
+            setTotalFee(fee);
             const dataForSubmission = {
                 stationID: '2405311022116801000',
                 connector: scannedResult,
@@ -403,9 +395,264 @@ const ScanQrPage = () => {
         }
     };
 
+    //below commented is the original WORKING startCharging, if i fucked up, return back to using this!!!
+
+    // const startCharging = async (dataForSubmission) => {
+    //     try {
+    //         const wallet = await walletService.getWalletBalance();
+    //         console.log('wallet in startCharging in scanQrPage', wallet);
+
+    //         const response = await walletService.submitPayment(
+    //             dataForSubmission.stationID,
+    //             dataForSubmission.connector,
+    //             dataForSubmission.user,
+    //             dataForSubmission.book_time,
+    //             dataForSubmission.end_time,
+    //             dataForSubmission.total_power,
+    //             dataForSubmission.total_fee,
+    //             dataForSubmission.promotion_code,
+    //             dataForSubmission.car,
+    //             dataForSubmission.type,
+    //             dataForSubmission.is_ic_call
+    //         );
+    //         if (response.status === 200 || response.status === 201) {
+    //             setSelectedDuration(null);
+    //             console.log('Charging started from startCharging', response);
+    //             setIsConfirmLoading(false);
+    //             // Set a flag in AsyncStorage to indicate charging has started
+
+    //             await AsyncStorage.setItem('chargingStarted', 'true');
+
+    //             Alert.alert('啟動成功', '請稍後等待頁面自動跳轉至充電介面', [
+    //                 {
+    //                     text: 'OK',
+    //                     onPress: async () => {
+    //                         setModalVisible(false);
+    //                         setLoading(true);
+
+    //                         // Wait for 2 seconds
+    //                         await new Promise((resolve) => setTimeout(resolve, 2000));
+
+    //                         // Hide loading spinner and navigate
+    //                         setLoading(false);
+
+    //                         router.push('(auth)/(tabs)/(charging)/chargingPage');
+    //                     }
+    //                 }
+    //             ]);
+
+    //             // Start the navigation attempt loop
+    //             // startNavigationAttempts();
+    //         } else if (response.status === 400) {
+    //             console.log('400 error in paymentSummaryPageComponent');
+    //             Alert.alert('餘額不足', '您的餘額不足,請充值後再試。');
+    //         } else {
+    //             console.log('Failed to start charging:', response);
+    //             Alert.alert('掃描失敗 請稍後再試。', response);
+    //         }
+    //     } catch (error) {
+    //         console.log('Failed to start chasssssssrging:', error);
+    //     }
+    // };
+
+    //below is the new flow for startCharging.
+
+    // useEffect(() => {
+    //     const subscription = AppState.addEventListener('change', (nextAppState) => {
+    //         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]);
+
+    //check payment status
+
+    useEffect(() => {
+        const subscription = AppState.addEventListener('change', (nextAppState) => {
+            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 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('付款已成功', `你已成功增值HKD $${totalFee}。請重新掃描去啟動充電槍。`, [
+                    {
+                        text: '確認',
+                        onPress: async () => {
+                            setModalVisible(false);
+                            router.dismiss();
+                        }
+                    }
+                ]);
+            } else {
+                Alert.alert('付款失敗', '請再試一次。', [
+                    {
+                        text: '確定',
+                        onPress: () => {
+                            setModalVisible(false);
+                            router.dismiss();
+                        }
+                    }
+                ]);
+            }
+            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.');
+        }
+    };
+    function formatTime(utcTimeString) {
+        // 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) => {
+        try {
+            const response = await walletService.getOutTradeNo();
+            console.log('outtradeno in oneTimeCharging', response);
+            if (response) {
+                setOutTradeNo(response);
+                setIsExpectingPayment(true);
+                paymentInitiatedTime.current = new Date().getTime();
+                const now = new Date();
+                const formattedTime = formatTime(now);
+                console.log('formattedTime in oneTimeCharging', formattedTime);
+
+                let amount = inputAmount * 100;
+                console.log('amount in scanqr oneTimeCharging', amount);
+                const origin = 'https://openapi-hk.qfapi.com/checkstand/#/?';
+                const obj = {
+                    appcode: '6937EF25DF6D4FA78BB2285441BC05E9',
+                    goods_name: 'Crazy Charge 錢包增值',
+                    out_trade_no: response,
+                    paysource: 'crazycharge_checkout',
+                    return_url: 'https://www.google.com',
+                    failed_url: 'https://www.google.com',
+                    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 params = paramStringify(obj);
+                const sign = sha256(`${params}${api_key}`).toString();
+                const url = `${origin}${paramStringify(obj, true)}&sign=${sign}`;
+
+                try {
+                    console.log(url);
+                    const supported = await Linking.canOpenURL(url);
+                    if (supported) {
+                        await Linking.openURL(url);
+                    } else {
+                        Alert.alert('錯誤', '請稍後再試');
+                    }
+                } catch (error) {
+                    console.error('Top-up failed:', error);
+                    Alert.alert('Error', '一次性付款失敗,請稍後再試');
+                }
+            } else {
+                console.log('nasdasdasdsdfgo');
+            }
+        } catch (error) {
+            console.log(error);
+        }
+    };
+
     const startCharging = async (dataForSubmission) => {
         try {
-            console.log('dataForSubmission', dataForSubmission);
+            const wallet = await walletService.getWalletBalance();
+            console.log('wallet in startCharging in scanQrPage', wallet);
+            oneTimeCharging(dataForSubmission.total_fee);
+
+            if (wallet < dataForSubmission.total_fee) {
+                oneTimeCharging(dataForSubmission.total_fee);
+                return;
+            }
+
             const response = await walletService.submitPayment(
                 dataForSubmission.stationID,
                 dataForSubmission.connector,
@@ -419,7 +666,8 @@ const ScanQrPage = () => {
                 dataForSubmission.type,
                 dataForSubmission.is_ic_call
             );
-            if (response) {
+            if (response.status === 200 || response.status === 201) {
+                setSelectedDuration(null);
                 console.log('Charging started from startCharging', response);
                 setIsConfirmLoading(false);
                 // Set a flag in AsyncStorage to indicate charging has started
@@ -446,6 +694,9 @@ const ScanQrPage = () => {
 
                 // Start the navigation attempt loop
                 // startNavigationAttempts();
+            } else if (response.status === 400) {
+                console.log('400 error in paymentSummaryPageComponent');
+                Alert.alert('餘額不足', '您的餘額不足,請充值後再試。');
             } else {
                 console.log('Failed to start charging:', response);
                 Alert.alert('掃描失敗 請稍後再試。', response);
@@ -489,9 +740,138 @@ const ScanQrPage = () => {
     //     setTimeout(attemptNavigation, 15000);
     // };
 
+    // return (
+    //     <View style={styles.container} ref={viewRef}>
+    //         {loading ? (
+    //             <View className="flex-1 items-center justify-center">
+    //                 <ActivityIndicator />
+    //             </View>
+    //         ) : (
+    //             <CameraView
+    //                 style={styles.camera}
+    //                 facing="back"
+    //                 barcodeScannerSettings={{
+    //                     barcodeTypes: ['qr']
+    //                 }}
+    //                 onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
+    //                 responsiveOrientationWhenOrientationLocked={true}
+    //             >
+    //                 <View style={styles.overlay}>
+    //                     <View style={styles.topOverlay}>
+    //                         {/* <ChooseCar
+    //                             carData={carData}
+    //                             loading={loading}
+    //                             selectedCar={selectedCar}
+    //                             setSelectedCar={setSelectedCar}
+    //                         /> */}
+    //                         <Pressable
+    //                             // style={styles.closeButton}
+    //                             className="absolute top-20 left-10 z-10 "
+    //                             onPress={() => {
+    //                                 if (router.canGoBack()) {
+    //                                     router.back();
+    //                                 } else {
+    //                                     router.push('/mainPage');
+    //                                 }
+    //                             }}
+    //                         >
+    //                             <CrossLogoWhiteSvg />
+    //                         </Pressable>
+    //                     </View>
+    //                     <View style={styles.centerRow}>
+    //                         <View style={styles.leftOverlay}></View>
+    //                         <View style={styles.transparentArea}></View>
+    //                         <View style={styles.rightOverlay} />
+    //                     </View>
+    //                     <View className="items-center justify-between" style={styles.bottomOverlay}>
+    //                         <View>
+    //                             <Text className="text-white text-lg font-bold mt-2 text-center">
+    //                                 請掃瞄充電座上的二維碼
+    //                             </Text>
+    //                         </View>
+    //                         <View className="flex-row space-x-2 items-center ">
+    //                             <QuestionSvg />
+
+    //                             <Pressable onPress={() => router.push('assistancePage')}>
+    //                                 <Text className="text-white text-base">需要協助?</Text>
+    //                             </Pressable>
+    //                         </View>
+    //                         <View />
+    //                     </View>
+    //                 </View>
+    //             </CameraView>
+    //         )}
+    //         <Modal isVisible={isModalVisible} backdropOpacity={0.5} animationIn="fadeIn" animationOut="fadeOut">
+    //             <View style={styles.modalContent} className="flex flex-col">
+    //                 <Text className="text-xl font-bold mt-2 text-center">請選擇充電時間</Text>
+    //                 <Text className="text-base  m-2 mb-4 text-center">按鈕呈紅色代表該時段已被他人預約</Text>
+    //                 <View className="flex flex-row flex-wrap  ">
+    //                     {Object.entries(availableSlots).map(([duration, available]) => (
+    //                         <NormalButton
+    //                             key={duration}
+    //                             title={
+    //                                 duration === 'full' ? (
+    //                                     <Text className={selectedDuration === duration ? 'text-white' : ''}>
+    //                                         充滿停機
+    //                                     </Text>
+    //                                 ) : (
+    //                                     <Text
+    //                                         className={selectedDuration === duration ? 'text-white' : ''}
+    //                                     >{`${planMap[duration].kWh} 度電 - ${planMap[duration].displayDuration} 分鐘`}</Text>
+    //                                 )
+    //                             }
+    //                             onPress={() => handleDurationSelect(duration)}
+    //                             extendedStyle={[
+    //                                 styles.durationButton,
+    //                                 {
+    //                                     backgroundColor: available
+    //                                         ? selectedDuration === duration
+    //                                             ? '#02677d'
+    //                                             : 'white'
+    //                                         : 'red',
+    //                                     borderColor: available ? 'black' : 'red',
+    //                                     borderWidth: 1
+    //                                 }
+    //                             ]}
+    //                             disabled={!available}
+    //                         />
+    //                     ))}
+    //                 </View>
+    //                 {selectedDuration && (
+    //                     <NormalButton
+    //                         title={
+    //                             isConfirmLoading ? (
+    //                                 <ActivityIndicator color="white" />
+    //                             ) : (
+    //                                 <Text className="text-white">確認</Text>
+    //                             )
+    //                         }
+    //                         onPress={handleConfirm}
+    //                         extendedStyle={styles.confirmButton}
+    //                     />
+    //                 )}
+    //                 <NormalButton
+    //                     title={<Text className="">取消</Text>}
+    //                     onPress={handleCancel}
+    //                     extendedStyle={styles.cancelButton}
+    //                 />
+    //             </View>
+    //         </Modal>
+    //     </View>
+    // );
+
     return (
         <View style={styles.container} ref={viewRef}>
-            {loading ? (
+            {!permission ? (
+                <View />
+            ) : !permission.granted ? (
+                <View className="flex-1 justify-center items-center">
+                    <Text style={{ textAlign: 'center' }}>
+                        我們需要相機權限來掃描機器上的二維碼,以便識別並啟動充電機器。我們不會儲存或共享任何掃描到的資訊。
+                        請前往設定開啟相機權限
+                    </Text>
+                </View>
+            ) : loading ? (
                 <View className="flex-1 items-center justify-center">
                     <ActivityIndicator />
                 </View>
@@ -507,14 +887,7 @@ const ScanQrPage = () => {
                 >
                     <View style={styles.overlay}>
                         <View style={styles.topOverlay}>
-                            {/* <ChooseCar
-                                carData={carData}
-                                loading={loading}
-                                selectedCar={selectedCar}
-                                setSelectedCar={setSelectedCar}
-                            /> */}
                             <Pressable
-                                // style={styles.closeButton}
                                 className="absolute top-20 left-10 z-10 "
                                 onPress={() => {
                                     if (router.canGoBack()) {

+ 11 - 0
app/_layout.tsx

@@ -5,11 +5,22 @@ import * as SecureStore from 'expo-secure-store';
 import { GestureHandlerRootView } from 'react-native-gesture-handler';
 import { useEffect, useState } from 'react';
 import { ActivityIndicator, View } from 'react-native';
+import { checkVersion } from '../component/checkVersion';
+import { authenticationService } from '../service/authService';
 
 export default function RootLayout() {
     const [isLoading, setIsLoading] = useState(true);
     const { user } = useAuth();
 
+    // useEffect(() => {
+    //     const fetchVersion = async () => {
+    //         const response = await authenticationService.getVersion();
+    //         console.log('response', response);
+    //         checkVersion(response);
+    //     };
+
+    //     fetchVersion();
+    // }, []);
     return (
         <GestureHandlerRootView style={{ flex: 1 }}>
             <AuthProvider>

+ 15 - 6
component/accountPages/uberUploadPageComponent.tsx

@@ -1,4 +1,4 @@
-import { View, Text, Pressable, Image, ScrollView, Alert } from 'react-native';
+import { View, Text, Pressable, Image, ScrollView, Alert, ActivityIndicator } from 'react-native';
 import { SafeAreaView } from 'react-native-safe-area-context';
 import { router } from 'expo-router';
 import { PreviousPageBlackSvg } from '../global/SVG';
@@ -11,6 +11,7 @@ import { authenticationService } from '../../service/authService';
 
 const UberUploadPageComponent = () => {
     const [image, setImage] = useState('');
+    const [isLoading, setIsLoading] = useState(false);
 
     const MAX_FILE_SIZE = 3 * 1024 * 1024;
 
@@ -46,6 +47,8 @@ const UberUploadPageComponent = () => {
             return;
         }
 
+        setIsLoading(true);
+
         try {
             const fileInfo = await FileSystem.getInfoAsync(image);
             if (!fileInfo.exists) {
@@ -78,6 +81,8 @@ const UberUploadPageComponent = () => {
             console.error('Headers:', error.response?.headers);
             Alert.alert('錯誤', '上傳時發生錯誤,請稍後再試');
             throw error;
+        } finally {
+            setIsLoading(false);
         }
     };
 
@@ -127,7 +132,7 @@ const UberUploadPageComponent = () => {
                                     style={{ borderWidth: 1 }}
                                     className="border-[#EEEEEE] rounded-2xl items-center justify-center  p-4 "
                                 >
-                                    <Text className="text-xl text-[#02677d] mb-6 text-center ">上傳成功</Text>
+                                    <Text className="text-xl text-[#02677d]  text-center ">上傳成功</Text>
                                 </View>
                             </Pressable>
                         ) : (
@@ -147,11 +152,15 @@ const UberUploadPageComponent = () => {
 
                         <View className="mt-4">
                             <NormalButton
-                                title={<Text className="text-xl text-white">提交</Text>}
-                                onPress={
-                                    uploadImage
-                                    // router.push('uberUploadCompletePage')
+                                title={
+                                    isLoading ? (
+                                        <ActivityIndicator color="white" />
+                                    ) : (
+                                        <Text className="text-xl text-white">提交</Text>
+                                    )
                                 }
+                                onPress={uploadImage}
+                                disabled={isLoading}
                             />
                         </View>
                     </View>

+ 37 - 32
component/accountPages/walletPageComponent.tsx

@@ -519,15 +519,15 @@ import { router } from 'expo-router';
 import { CrossLogoSvg } from '../global/SVG';
 import { useEffect, useRef, useState } from 'react';
 import { walletService } from '../../service/walletService';
-import UnionPayImage from '../../assets/unionpay.png';
-import PayMeImage from '../../assets/payme.png';
+// import UnionPayImage from '../../assets/unionpay.png';
+// import PayMeImage from '../../assets/payme.png';
 import { formatCouponDate, formatDate } from '../../util/lib';
 import { set } from 'date-fns';
 import { reloadAppAsync } from 'expo';
 import sha256 from 'crypto-js/sha256';
 
 const AmountInputModal = ({ visible, onClose, onConfirm }) => {
-    const [inputAmount, setInputAmount] = useState('');
+    const amounts = [200, 500, 1000, 2000];
 
     return (
         <Modal animationType="fade" transparent={true} visible={visible} onRequestClose={onClose}>
@@ -547,32 +547,32 @@ const AmountInputModal = ({ visible, onClose, onConfirm }) => {
                         width: '80%'
                     }}
                 >
-                    <Text style={{ fontSize: 20, marginBottom: 20 }}>輸入增值金額</Text>
-                    <TextInput
-                        style={{
-                            borderWidth: 1,
-                            borderColor: '#ccc',
-                            borderRadius: 5,
-                            padding: 10,
-                            marginBottom: 20,
-                            fontSize: 18
-                        }}
-                        keyboardType="numeric"
-                        placeholder="輸入金額"
-                        value={inputAmount}
-                        onChangeText={setInputAmount}
-                    />
-                    <Pressable
-                        onPress={() => onConfirm(inputAmount)}
+                    <Text style={{ fontSize: 20, marginBottom: 20 }}>選擇增值金額</Text>
+                    <View
                         style={{
-                            backgroundColor: '#02677D',
-                            padding: 10,
-                            borderRadius: 5,
-                            alignItems: 'center'
+                            flexDirection: 'row',
+                            flexWrap: 'wrap',
+                            justifyContent: 'space-between',
+                            marginBottom: 20
                         }}
                     >
-                        <Text style={{ color: 'white', fontSize: 18 }}>確認</Text>
-                    </Pressable>
+                        {amounts.map((amount) => (
+                            <Pressable
+                                key={amount}
+                                onPress={() => onConfirm(amount)}
+                                style={{
+                                    backgroundColor: '#02677D',
+                                    padding: 10,
+                                    borderRadius: 5,
+                                    width: '48%',
+                                    alignItems: 'center',
+                                    marginBottom: 10
+                                }}
+                            >
+                                <Text style={{ color: 'white', fontSize: 18 }}>${amount}</Text>
+                            </Pressable>
+                        ))}
+                    </View>
                     <Pressable onPress={onClose} style={{ padding: 10, alignItems: 'center', marginTop: 10 }}>
                         <Text style={{ color: 'red' }}>取消</Text>
                     </Pressable>
@@ -655,6 +655,7 @@ const WalletPageComponent = () => {
     const appState = useRef(AppState.currentState);
     const paymentInitiatedTime = useRef(null);
 
+    //monitor app state
     useEffect(() => {
         const subscription = AppState.addEventListener('change', (nextAppState) => {
             if (
@@ -685,15 +686,17 @@ const WalletPageComponent = () => {
             subscription.remove();
         };
     }, [outTradeNo, isExpectingPayment]);
-
+    //check payment status
     const checkPaymentStatus = async () => {
         try {
             console.log('what is the outTradeNo?? ', outTradeNo);
             const result = await walletService.checkPaymentStatus(outTradeNo);
             setPaymentStatus(result);
             console.log('checkPaymentStatus from walletPageComponent', result);
-            if (result) {
+
+            if (result && !result.some((item) => item.errmsg?.includes('處理中'))) {
                 // Payment successful
+
                 Alert.alert('Success', 'Payment was successful!', [
                     {
                         text: '成功',
@@ -716,14 +719,14 @@ const WalletPageComponent = () => {
         }
     };
 
+    //fetch customer wallet balance
     useEffect(() => {
         const fetchData = async () => {
             try {
                 setLoading(true);
                 const info = await walletService.getCustomerInfo();
-
                 const wallet = await walletService.getWalletBalance();
-                console.log(wallet);
+                console.log('wallet', wallet);
                 setUserID(info.id);
                 setWalletBalance(wallet);
                 setCoupons(coupon);
@@ -787,10 +790,12 @@ const WalletPageComponent = () => {
                 paymentInitiatedTime.current = new Date().getTime();
                 const now = new Date();
                 const formattedTime = formatTime(now);
-                console.log(formattedTime);
+                console.log('formattedTime in walletPageComponent', formattedTime);
                 const out_trade_no = response;
+                console.log('inputAmount in walletPageComponent', inputAmount);
 
-                let amount = parseInt(inputAmount, 10) * 100;
+                let amount = inputAmount * 100;
+                console.log('amount in walletPageComponent', amount);
                 const origin = 'https://openapi-hk.qfapi.com/checkstand/#/?';
                 const obj = {
                     appcode: '6937EF25DF6D4FA78BB2285441BC05E9',

+ 13 - 8
component/bookingMenuPage/makingBookingPageComponent.tsx

@@ -24,6 +24,7 @@ import { authenticationService } from '../../service/authService';
 import * as Location from 'expo-location';
 import { calculateDistance } from '../global/distanceCalculator';
 import Modal from 'react-native-modal';
+import useUserInfoStore from '../../providers/userinfo_store';
 interface AccordionItemProps {
     title: string;
     children: React.ReactNode;
@@ -71,7 +72,8 @@ const MakingBookingPageComponent = () => {
     const [price, setPrice] = useState(0);
     const layoutWidth = screenWidth;
     const layoutHeight = screenHeight * 0.32;
-    const [userID, setUserID] = useState('');
+    const { userID, setUserID } = useUserInfoStore();
+
     const params = useLocalSearchParams();
     const chargeStationID = params.chargeStationID as string;
     const chargeStationName = params.chargeStationName as string;
@@ -241,6 +243,7 @@ const MakingBookingPageComponent = () => {
 
     // useEffect(() => {
     //     setCarLoadingState(true);
+
     //     const fetchUserInfoAndCarData = async () => {
     //         try {
     //             const fetchedUserInfo = await authenticationService.getUserInfo();
@@ -378,7 +381,9 @@ const MakingBookingPageComponent = () => {
         '101708240502475001': '1',
         '101708240502476001': '2',
         '101708240502477001': '3',
-        '101708240502478001': '4'
+        '101708240502478001': '4',
+        '101708240502474001': '5',
+        '101708240502474002': '6'
     };
 
     console.log('availableConnectorDropdownOptions', availableConnectorDropdownOptions);
@@ -468,13 +473,13 @@ const MakingBookingPageComponent = () => {
     };
 
     const handleModalConfirm = () => {
-        Alert.alert('提示', '此功能正在準備中,敬請期待', [{ text: '確定', onPress: () => router.push('/mainPage') }], {
-            cancelable: false
-        });
+        // Alert.alert('提示', '此功能正在準備中,敬請期待', [{ text: '確定', onPress: () => router.push('/mainPage') }], {
+        //     cancelable: false
+        // });
         setModalVisible(false);
-        // if (routerParams) {
-        //     router.push(routerParams);
-        // }
+        if (routerParams) {
+            router.push(routerParams);
+        }
     };
 
     return (

+ 6 - 1
component/chargingPage/chargingPageComponent.tsx

@@ -100,6 +100,7 @@ const ChargingPageComponent = ({ data }) => {
 
     //用來計充電歷時    //用來計充電歷時    //用來計充電歷時    //用來計充電歷時    //用來計充電歷時
     const [timeSince, setTimeSince] = useState<string>('');
+
     useEffect(() => {
         const updateTimeSince = () => {
             if (reservationData && reservationData.actual_start_time) {
@@ -117,11 +118,15 @@ const ChargingPageComponent = ({ data }) => {
 
     function timeSinceBooking(timeString) {
         if (timeString) {
+            console.log('timeString in timeSinceBooking', timeString);
             const startTime = new Date(timeString);
             const now = new Date();
+            console.log('now in timeSinceBooking', now);
+            console.log('startTime in timeSinceBooking', startTime);
+            console.log('now - startTime', now - startTime);
             const diffInMilliseconds = now - startTime;
             const diffInMinutes = Math.floor(diffInMilliseconds / (1000 * 60));
-
+            console.log('diffInMinutes in timeSinceBooking', diffInMinutes);
             if (diffInMinutes < 1) {
                 return '< 1 minute';
             } else {

+ 29 - 0
component/checkVersion.tsx

@@ -0,0 +1,29 @@
+import Constants from 'expo-constants';
+import * as Linking from 'expo-linking';
+import { Alert, Platform } from 'react-native';
+
+export function checkVersion(fetchedVersion: string) {
+    const currentVersion = Constants.expoConfig?.version;
+    console.log('currentVersion', currentVersion);
+    console.log('fetchedVersion', fetchedVersion);
+
+    if (currentVersion && currentVersion !== fetchedVersion) {
+        Alert.alert(
+            '請更新到最新版本',
+            'Please update to the latest version of the app to continue.',
+            [
+                {
+                    text: '現在更新',
+                    onPress: () => {
+                        const url =
+                            Platform.OS === 'ios'
+                                ? 'https://apps.apple.com/app/crazy-charge/id6639612402'
+                                : 'https://play.google.com/store/apps/details?id=hk.com.crazycharge';
+                        Linking.openURL(url);
+                    }
+                }
+            ],
+            { cancelable: false }
+        );
+    }
+}

+ 15 - 1
component/homePage/homePage.tsx

@@ -23,7 +23,7 @@ const HomePage: React.FC<HomePageProps> = () => {
     const now = new Date();
 
     const { user } = useContext(AuthContext);
-    const { userID, setUserID } = useUserInfoStore();
+    const { userID, currentPrice, setUserID, setCurrentPrice } = useUserInfoStore();
 
     // Effect for fetching user ID
     useEffect(() => {
@@ -42,6 +42,20 @@ const HomePage: React.FC<HomePageProps> = () => {
         fetchID();
     }, []);
 
+    useEffect(() => {
+        const fetchCurrentPrice = async () => {
+            try {
+                const response = await chargeStationService.getCurrentPrice();
+                if (response) {
+                    setCurrentPrice(response);
+                }
+            } catch (error) {
+                console.log('main page fetch current price error', error);
+            }
+        };
+        fetchCurrentPrice();
+    }, []);
+
     return (
         <SafeAreaView edges={['top', 'left', 'right']} className="flex-1 bg-white">
             <ScrollView showsVerticalScrollIndicator={false} className="flex-1 mx-[5%] ">

+ 17 - 1
component/resultDetailPage/resultDetailPageComponent.tsx

@@ -108,6 +108,7 @@ const ResultDetailPageComponent = () => {
     const [distance, setDistance] = useState<string | null>(null);
     const [coordinates, setCoordinates] = useState<StationCoordinates | null>(null);
     const [price, setPrice] = useState('');
+    const [availableConnectors, setAvailableConnectors] = useState<number | null>(null);
 
     useEffect(() => {
         const getCurrentLocation = async () => {
@@ -200,6 +201,21 @@ const ResultDetailPageComponent = () => {
         fetchCoordinates();
     }, [chargeStationID, memoizedCoordinates]);
 
+    useEffect(() => {
+        const fetchAvailableConnectors = async () => {
+            try {
+                const connectors = await chargeStationService.fetchAvailableConnectors(chargeStationID);
+                console.log('connectors number from resultDetailPage', connectors);
+                setAvailableConnectors(connectors);
+            } catch (error) {
+                console.error('Error fetching available connectors:', error);
+                setAvailableConnectors(null);
+            }
+        };
+
+        fetchAvailableConnectors();
+    }, [chargeStationID]);
+
     const formatDistance = (distanceInMeters: number): string => {
         if (distanceInMeters < 1000) {
             return `${Math.round(distanceInMeters)}米`;
@@ -403,7 +419,7 @@ const ResultDetailPageComponent = () => {
                                     </Text>
 
                                     <View className="flex-row items-center space-x-2">
-                                        <Text className="text-3xl text-[#02677D]">4</Text>
+                                        <Text className="text-3xl text-[#02677D]">{availableConnectors}</Text>
                                     </View>
                                 </View>
                             </View>

+ 6 - 5
package-lock.json

@@ -22,7 +22,7 @@
         "expo": "~51.0.7",
         "expo-camera": "~15.0.13",
         "expo-checkbox": "~3.0.0",
-        "expo-constants": "~16.0.1",
+        "expo-constants": "~16.0.2",
         "expo-env": "^1.1.1",
         "expo-file-system": "~17.0.1",
         "expo-image-picker": "~15.0.7",
@@ -8577,11 +8577,12 @@
       "integrity": "sha512-ZfNUawE0Bp/Xa5Gwtn04yfg6rCnKdqdmBXvEGbYg5U+IfRfLh+ocLiiBAcx760DfdYpzMGQOGpUtWQeEVmJwNw=="
     },
     "node_modules/expo-constants": {
-      "version": "16.0.1",
-      "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-16.0.1.tgz",
-      "integrity": "sha512-s6aTHtglp926EsugWtxN7KnpSsE9FCEjb7CgEjQQ78Gpu4btj4wB+IXot2tlqNwqv+x7xFe5veoPGfJDGF/kVg==",
+      "version": "16.0.2",
+      "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-16.0.2.tgz",
+      "integrity": "sha512-9tNY3OVO0jfiMzl7ngb6IOyR5VFzNoN5OOazUWoeGfmMqVB5kltTemRvKraK9JRbBKIw+SOYLEmF0sEqgFZ6OQ==",
       "dependencies": {
-        "@expo/config": "~9.0.0-beta.0"
+        "@expo/config": "~9.0.0",
+        "@expo/env": "~0.3.0"
       },
       "peerDependencies": {
         "expo": "*"

+ 1 - 1
package.json

@@ -23,7 +23,7 @@
     "expo": "~51.0.7",
     "expo-camera": "~15.0.13",
     "expo-checkbox": "~3.0.0",
-    "expo-constants": "~16.0.1",
+    "expo-constants": "~16.0.2",
     "expo-env": "^1.1.1",
     "expo-file-system": "~17.0.1",
     "expo-image-picker": "~15.0.7",

+ 5 - 2
providers/userinfo_store.tsx

@@ -2,13 +2,16 @@ import { create } from 'zustand';
 
 interface UserInfoState {
     userID: string;
+    currentPrice: number | string;
     setUserID: (id: string) => void;
+    setCurrentPrice: (price: number | string) => void;
 }
 
 const useUserInfoStore = create<UserInfoState>((set) => ({
     userID: '',
-
-    setUserID: (id: string) => set({ userID: id })
+    currentPrice: 0,
+    setUserID: (id: string) => set({ userID: id }),
+    setCurrentPrice: (price: number | string) => set({ currentPrice: price })
 }));
 
 export default useUserInfoStore;

+ 10 - 0
service/authService.tsx

@@ -227,6 +227,16 @@ class AuthenticationService {
         }
     }
 
+    async getVersion() {
+        try {
+            const response = await axios.get(`${this.apiUrl}/public/client/app/version`);
+            return response.data.data.version;
+        } catch (error) {
+            console.error('Error getting version:', error);
+            return null;
+        }
+    }
+
     async verifyingOtpForgetPassword(
         email: string,
         otp: string,

+ 43 - 0
service/chargeStationService.tsx

@@ -172,6 +172,49 @@ class ChargeStationService {
         }
     }
 
+    async getCurrentPrice() {
+        try {
+            const response = await axios.get(`${this.apiUrl}/clients/promotion/price?id=2405311022116801000`);
+            if (response.status === 200 || response.status === 201) {
+                return response.data.originalPrice;
+            } else {
+                throw new Error(`Unexpected response status: ${response.status}`);
+            }
+        } catch (error) {
+            console.error('Error getting current price:', error);
+            if (axios.isAxiosError(error)) {
+                console.error('Response data:', error.response?.data);
+                console.error('Response status:', error.response?.status);
+            }
+            throw error; // Re-throw the error for the caller to handle
+        }
+    }
+
+    async fetchAvailableConnectors(stationID: string) {
+        try {
+            const response = await axios.get(
+                `${this.apiUrl}/clients/chargestations/resources/status?StationIDs=${stationID}`,
+                {
+                    headers: {
+                        Authorization: `Bearer ${await SecureStore.getItemAsync('accessToken')}`
+                    }
+                }
+            );
+            if (response.status === 200 || response.status === 201) {
+                const stationStatusInfos = response.data.data.StationStatusInfos;
+                if (stationStatusInfos && stationStatusInfos.length > 0) {
+                    const availableConnectors = stationStatusInfos[0].ConnectorStatusInfos.filter(
+                        (connector) => connector.Status === 2
+                    ).length;
+                    return availableConnectors;
+                }
+                return 0;
+            }
+        } catch (error) {
+            console.log(error);
+            return 0;
+        }
+    }
     async fetchChargeStations() {
         try {
             const response = await axios.get(`${this.apiUrl}/clients/chargestations/resources/info`, {