| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617 |
- import { CameraView, useCameraPermissions } from 'expo-camera';
- import { useCallback, useEffect, useRef, useState } from 'react';
- import {
- ActivityIndicator,
- Alert,
- AppState,
- Dimensions,
- Linking,
- Pressable,
- ScrollView,
- StyleSheet,
- Text,
- Vibration,
- View,
- Platform
- } from 'react-native';
- import sha256 from 'crypto-js/sha256';
- import ChooseCarForChargingRow from '../../../../component/global/chooseCarForChargingRow';
- import { CrossLogoWhiteSvg, QuestionSvg } from '../../../../component/global/SVG';
- import { router, useFocusEffect } from 'expo-router';
- import { chargeStationService } from '../../../../service/chargeStationService';
- import { authenticationService } from '../../../../service/authService';
- import { walletService } from '../../../../service/walletService';
- import useUserInfoStore from '../../../../providers/userinfo_store';
- import Modal from 'react-native-modal';
- import NormalButton from '../../../../component/global/normal_button';
- import { ceil } from 'lodash';
- import AsyncStorage from '@react-native-async-storage/async-storage';
- import { useChargingStore } from '../../../../providers/scan_qr_payload_store';
- const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
- //reminder: scan qr code page, ic call should be false
- const ScanQrPage = () => {
- const { userID, currentPrice, setCurrentPrice } = useUserInfoStore();
- const [currentPriceFetchedWhenScanQr, setCurrentPriceFetchedWhenScanQr] = useState(0);
- const { scanned_qr_code, setScannedQrCode, stationID, setStationId } = useChargingStore();
- 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);
- const [loading3, setLoading3] = useState(false);
- const [carData, setCarData] = useState([]);
- const [isModalVisible, setModalVisible] = useState(false);
- const [isConfirmLoading, setIsConfirmLoading] = useState(false);
- const [availableSlots, setAvailableSlots] = useState({
- // 3: false,
- 25: false,
- 30: false,
- 40: false,
- 45: false,
- full: false
- });
- const [selectedDuration, setSelectedDuration] = useState(null);
- 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);
- const [walletBalance, setWalletBalance] = useState(0);
- // Effect for requesting camera permissions
- useEffect(() => {
- (async () => {
- const { status } = await requestPermission();
- if (status !== 'granted') {
- alert(
- '我們需要相機權限來掃描機器上的二維碼,以便識別並啟動充電機器。我們不會儲存或共享任何掃描到的資訊。 請前往設定開啟相機權限'
- );
- }
- })();
- }, []);
- useFocusEffect(
- useCallback(() => {
- // When screen comes into focus, enable scanning
- setScanned(false);
- return () => {
- // When screen loses focus, disable scanning
- setScanned(true);
- };
- }, [])
- );
- useEffect(() => {
- const fetchDefaultCar = async () => {
- try {
- const response = await chargeStationService.getUserDefaultCars();
- if (response) {
- // console.log('default car', response.data.id);
- setSelectedCar(response.data.id);
- }
- } catch (error) {
- } finally {
- setLoading(false);
- }
- };
- fetchDefaultCar();
- }, []);
- useEffect(() => {
- const getWalletBalance = async () => {
- try {
- const response = await walletService.getWalletBalance();
- if (response) {
- // console.log('walletBalance setting up', response);
- setWalletBalance(response);
- }
- } catch (error) {
- console.log(error);
- }
- };
- getWalletBalance();
- }, []);
- // Function to handle barcode scanning
- const handleBarCodeScanned = async ({ bounds, data, type }: { bounds?: any; data: any; type: any }) => {
- if (
- !bounds ||
- typeof bounds.origin?.x !== 'number' ||
- typeof bounds.origin?.y !== 'number' ||
- typeof bounds.size?.width !== 'number' ||
- typeof bounds.size?.height !== 'number'
- ) {
- setScanned(true);
- setScannedQrCode(data);
- Vibration.vibrate(100);
- //after scanning, immediately fetch the correct station id and push to optionPage
- try {
- const stationId = await chargeStationService.noImagefetchChargeStationIdByScannedConnectorId(data);
- if (!stationId) {
- Alert.alert('錯誤', '無法找到充電站,請稍後再嘗試');
- setTimeout(() => {
- setScanned(false);
- }, 2000);
- return;
- }
- setStationId(stationId);
- router.push('/optionPage');
- } catch (error) {
- console.error('Error fetching station ID:', error);
- Alert.alert('錯誤', '無法找到充電站,請稍後再試');
- setTimeout(() => {
- setScanned(false);
- }, 2000);
- return;
- }
- return;
- }
- // -----------------------------------------------------------------------------------------------------
- const { origin, size } = bounds;
- // Calculate the size of the square transparent area
- const transparentAreaSize = Math.min(screenWidth * 0.6, screenHeight * 0.3);
- const transparentAreaX = (screenWidth - transparentAreaSize) / 2;
- const transparentAreaY = (screenHeight - transparentAreaSize) / 2;
- // Check if the barcode is within the transparent area
- // 在iOS上检查二维码是否在扫描框内,在安卓上跳过位置检查
- const isIOS = Platform.OS === 'ios';
- const isWithinScanArea = isIOS
- ? origin.x >= transparentAreaX &&
- origin.y >= transparentAreaY &&
- origin.x + size.width <= transparentAreaX + transparentAreaSize &&
- origin.y + size.height <= transparentAreaY + transparentAreaSize
- : true;
- if (isWithinScanArea) {
- setScanned(true);
- setScannedQrCode(data);
- Vibration.vibrate(100);
- //after scanning, immediately fetch the correct station id and push to optionPage
- try {
- const stationId = await chargeStationService.noImagefetchChargeStationIdByScannedConnectorId(data);
- if (!stationId) {
- Alert.alert('錯誤', '無法找到充電站,請稍後再嘗試');
- setTimeout(() => {
- setScanned(false);
- }, 2000);
- return;
- }
- setStationId(stationId);
- router.push('/optionPage');
- } catch (error) {
- console.error('Error fetching station ID:', error);
- Alert.alert('錯誤', '無法找到充電站,請稍後再試');
- setTimeout(() => {
- setScanned(false);
- }, 2000);
- return;
- }
- return;
- }
- };
- 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 $${
- Number.isInteger(totalFee) ? totalFee : totalFee.toFixed(1)
- }。請重新掃描去啟動充電槍。`,
- [
- {
- 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);
- 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 {
- // 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 {
- }
- } catch (error) {}
- };
- const startCharging = async (dataForSubmission) => {
- try {
- //before i start below logic, i need to check if the user has penalty unpaid.
- //i will call fetchReservationHistories. and the api will return an array of object, within the object there is a field called "penalty_fee".
- //if any reservation has penalty_fee > 0, 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.
- const reservationHistories = await chargeStationService.fetchReservationHistories();
- const unpaidPenalties = reservationHistories.filter(
- (reservation) => reservation.penalty_fee > 0 && reservation.penalty_paid_status === false
- );
- const mostRecentUnpaidReservation = unpaidPenalties.reduce((mostRecent, current) => {
- 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
- setModalVisible(false);
- setLoading(false);
- 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,
- stationName:
- mostRecentUnpaidReservation.connector.EquipmentID.StationID.snapshot
- .StationName,
- address:
- mostRecentUnpaidReservation.connector.EquipmentID.StationID.snapshot.Address
- }
- });
- }
- },
- {
- text: '返回',
- onPress: () => {
- setModalVisible(false);
- if (router.canGoBack()) {
- router.back();
- } else {
- router.push('/mainPage');
- }
- }
- }
- ],
- { cancelable: false }
- );
- return;
- }
- const wallet = await walletService.getWalletBalance();
- if (wallet < dataForSubmission.total_fee) {
- oneTimeCharging(dataForSubmission.total_fee);
- // const remainingAmount = dataForSubmission.total_fee - wallet;
- // oneTimeCharging(remainingAmount);
- return;
- }
- 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);
- setIsConfirmLoading(false);
- 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.navigate('(auth)/(tabs)/(home)/mainPage');
- router.push('(auth)/(tabs)/(charging)/chargingPage');
- }
- }
- ]);
- } else if (response.status === 400) {
- Alert.alert('餘額不足', '掃描失敗 請稍後再試。');
- } else {
- Alert.alert('掃描失敗 請稍後再試。', response);
- }
- } catch (error) {}
- };
- return (
- <View style={styles.container} ref={viewRef}>
- {!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>
- ) : (
- <View className="flex-1 position-relative">
- <CameraView
- style={styles.camera}
- facing="back"
- barcodeScannerSettings={{
- barcodeTypes: ['qr']
- }}
- onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
- responsiveOrientationWhenOrientationLocked={true}
- ></CameraView>
- <View style={styles.overlay}>
- <View style={styles.topOverlay}>
- <Pressable
- className="absolute top-20 left-10 z-10 p-4"
- hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }} // Added hitSlop
- onPress={() => {
- if (router.canGoBack()) {
- router.back();
- } else {
- router.push('/mainPage');
- }
- }}
- >
- <View style={{ transform: [{ scale: 1.5 }] }}>
- <CrossLogoWhiteSvg />
- </View>
- </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>
- </View>
- )}
- </View>
- );
- };
- const styles = StyleSheet.create({
- container: {
- flex: 1
- },
- camera: {
- flex: 1
- },
- overlay: {
- flex: 1,
- width: '100%',
- height: '100%',
- position: 'absolute'
- },
- topOverlay: {
- flex: 35,
- alignItems: 'center',
- backgroundColor: 'rgba(0,0,0,0.5)'
- },
- centerRow: {
- flex: 30,
- flexDirection: 'row'
- },
- leftOverlay: {
- flex: 20,
- backgroundColor: 'rgba(0,0,0,0.5)'
- },
- transparentArea: {
- flex: 60,
- aspectRatio: 1,
- position: 'relative'
- },
- rightOverlay: {
- flex: 20,
- backgroundColor: 'rgba(0,0,0,0.5)'
- },
- bottomOverlay: {
- flex: 35,
- backgroundColor: 'rgba(0,0,0,0.5)'
- },
- closeButton: {
- position: 'absolute',
- top: 40,
- left: 20,
- zIndex: 1
- },
- modalContent: {
- backgroundColor: 'white',
- padding: 22,
- alignItems: 'center',
- borderRadius: 4,
- borderColor: 'rgba(0, 0, 0, 0.1)'
- },
- durationButton: { margin: 5 },
- confirmButton: {
- marginTop: 20,
- width: '100%'
- },
- cancelButton: {
- marginTop: 20,
- width: '100%',
- backgroundColor: 'white',
- borderColor: 'black',
- borderWidth: 1,
- color: 'black'
- }
- });
- export default ScanQrPage;
|