scanQrPage.tsx 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188
  1. import { CameraView, useCameraPermissions } from 'expo-camera';
  2. import { useEffect, useRef, useState } from 'react';
  3. import {
  4. ActivityIndicator,
  5. Alert,
  6. AppState,
  7. Dimensions,
  8. Linking,
  9. Pressable,
  10. ScrollView,
  11. StyleSheet,
  12. Text,
  13. Vibration,
  14. View
  15. } from 'react-native';
  16. import sha256 from 'crypto-js/sha256';
  17. import ChooseCarForChargingRow from '../../../../component/global/chooseCarForChargingRow';
  18. import { CrossLogoWhiteSvg, QuestionSvg } from '../../../../component/global/SVG';
  19. import { router } from 'expo-router';
  20. import { chargeStationService } from '../../../../service/chargeStationService';
  21. import { authenticationService } from '../../../../service/authService';
  22. import { walletService } from '../../../../service/walletService';
  23. import useUserInfoStore from '../../../../providers/userinfo_store';
  24. import Modal from 'react-native-modal';
  25. import NormalButton from '../../../../component/global/normal_button';
  26. import { ceil } from 'lodash';
  27. import AsyncStorage from '@react-native-async-storage/async-storage';
  28. const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
  29. // const ChooseCar = ({ carData, loading, selectedCar, setSelectedCar }) => {
  30. // const isLargeScreen = screenHeight >= 800;
  31. // const defaultImageUrl = require('../../../../assets/car1.png');
  32. // return (
  33. // <View
  34. // style={{
  35. // ...(isLargeScreen
  36. // ? {
  37. // marginTop: '10%',
  38. // marginBottom: '12%',
  39. // paddingBottom: 12
  40. // }
  41. // : {
  42. // flex: 1,
  43. // alignItems: 'center',
  44. // justifyContent: 'center'
  45. // })
  46. // }}
  47. // >
  48. // <View className="justify-center items-center flex-1 ">
  49. // <View
  50. // style={{
  51. // ...(isLargeScreen
  52. // ? {}
  53. // : {
  54. // backgroundColor: 'rgba(0,0,0,0.7)'
  55. // })
  56. // }}
  57. // >
  58. // {loading ? (
  59. // <View className="w-full">
  60. // <ActivityIndicator color="#34657b" />
  61. // </View>
  62. // ) : (
  63. // <View className="w-screen bg-[#000000B3]">
  64. // <View className="flex-row items-center justify-between mx-[5%] ">
  65. // <Pressable
  66. // className="pt-4 "
  67. // onPress={() => {
  68. // if (router.canGoBack()) {
  69. // router.back();
  70. // } else {
  71. // router.replace('mainPage');
  72. // }
  73. // }}
  74. // >
  75. // <CrossLogoWhiteSvg />
  76. // </Pressable>
  77. // <Text className="text-base text-white pt-2">選擇充電車輛</Text>
  78. // <Text className="text-xl text-white pt-2"></Text>
  79. // </View>
  80. // <ScrollView
  81. // horizontal={true}
  82. // showsHorizontalScrollIndicator={false}
  83. // contentContainerStyle={{
  84. // alignItems: 'center',
  85. // flexDirection: 'row',
  86. // marginVertical: 12
  87. // }}
  88. // className="space-x-2 mx-[5%]"
  89. // >
  90. // {carData.map((car, index) => (
  91. // <ChooseCarForChargingRow
  92. // key={`${car.name}+${index}`}
  93. // image={car.image}
  94. // onPress={() => {
  95. // setSelectedCar(car.id);
  96. // console.log(car.id);
  97. // }}
  98. // isSelected={selectedCar === car.id}
  99. // // imageUrl={image}
  100. // VehicleName={car.name}
  101. // isDefault={car.isDefault}
  102. // />
  103. // ))}
  104. // </ScrollView>
  105. // </View>
  106. // )}
  107. // </View>
  108. // </View>
  109. // </View>
  110. // );
  111. // };
  112. //reminder: scan qr code page, ic call should be false
  113. const ScanQrPage = () => {
  114. const { userID, currentPrice, setCurrentPrice } = useUserInfoStore();
  115. const [currentPriceFetchedWhenScanQr, setCurrentPriceFetchedWhenScanQr] = useState(0);
  116. const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
  117. const [permission, requestPermission] = useCameraPermissions();
  118. const [scanned, setScanned] = useState(false);
  119. const viewRef = useRef(null);
  120. const [scannedResult, setScannedResult] = useState('');
  121. const [selectedCar, setSelectedCar] = useState('');
  122. const now = new Date();
  123. const [loading, setLoading] = useState(true);
  124. const [loading2, setLoading2] = useState(false);
  125. const [loading3, setLoading3] = useState(false);
  126. const [carData, setCarData] = useState([]);
  127. const [isModalVisible, setModalVisible] = useState(false);
  128. const [isConfirmLoading, setIsConfirmLoading] = useState(false);
  129. const [availableSlots, setAvailableSlots] = useState({
  130. // 3: false,
  131. 25: false,
  132. 30: false,
  133. 40: false,
  134. 45: false,
  135. full: false
  136. });
  137. const [selectedDuration, setSelectedDuration] = useState(null);
  138. const appState = useRef(AppState.currentState);
  139. const [paymentStatus, setPaymentStatus] = useState(null);
  140. const [isExpectingPayment, setIsExpectingPayment] = useState(false);
  141. const paymentInitiatedTime = useRef(null);
  142. const PAYMENT_CHECK_TIMEOUT = 5 * 60 * 1000; // 5 minutes in milliseconds
  143. const [outTradeNo, setOutTradeNo] = useState('');
  144. const [totalFee, setTotalFee] = useState(0);
  145. const [walletBalance, setWalletBalance] = useState(0);
  146. // Effect for requesting camera permissions
  147. useEffect(() => {
  148. (async () => {
  149. const { status } = await requestPermission();
  150. if (status !== 'granted') {
  151. alert(
  152. '我們需要相機權限來掃描機器上的二維碼,以便識別並啟動充電機器。我們不會儲存或共享任何掃描到的資訊。 請前往設定開啟相機權限'
  153. );
  154. }
  155. })();
  156. }, []);
  157. // Effect for fetching user's cars
  158. // useEffect(() => {
  159. // const fetchAllCars = async () => {
  160. // try {
  161. // const response = await chargeStationService.getUserCars();
  162. // if (response) {
  163. // console.log('data', response.data);
  164. // const carTypes = response.data.map((item: any) => ({
  165. // id: item.id,
  166. // name: item.car_typ e.name,
  167. // image: item.car_type.type_image_url
  168. // }));
  169. // // console.log('carTypes', carTypes);
  170. // // console.log('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', carTypes);
  171. // let updatedCarTypes = [...carTypes];
  172. // for (let i = 0; i < carTypes.length; i++) {
  173. // const car = updatedCarTypes[i];
  174. // const imageUrl = await chargeStationService.getProcessedImageUrl(car.image);
  175. // updatedCarTypes[i] = {
  176. // ...car,
  177. // image: imageUrl
  178. // };
  179. // }
  180. // setCarData(updatedCarTypes);
  181. // // console.log('updatedCarTypes', updatedCarTypes);
  182. // return true;
  183. // }
  184. // } catch (error) {
  185. // console.log(error);
  186. // } finally {
  187. // setLoading(false);
  188. // }
  189. // };
  190. // fetchAllCars();
  191. // }, []);
  192. useEffect(() => {
  193. const fetchDefaultCar = async () => {
  194. try {
  195. const response = await chargeStationService.getUserDefaultCars();
  196. if (response) {
  197. // console.log('default car', response.data.id);
  198. setSelectedCar(response.data.id);
  199. }
  200. } catch (error) {
  201. console.log(error);
  202. } finally {
  203. setLoading(false);
  204. }
  205. };
  206. fetchDefaultCar();
  207. }, []);
  208. //check current wallet
  209. useEffect(() => {
  210. const getWalletBalance = async () => {
  211. try {
  212. const response = await walletService.getWalletBalance();
  213. if (response) {
  214. // console.log('walletBalance setting up', response);
  215. setWalletBalance(response);
  216. }
  217. } catch (error) {
  218. console.log(error);
  219. }
  220. };
  221. getWalletBalance();
  222. }, []);
  223. const fetchCurrentPrice = async () => {
  224. try {
  225. const response = await chargeStationService.getCurrentPrice();
  226. if (response) {
  227. setCurrentPriceFetchedWhenScanQr(response);
  228. setCurrentPrice(response);
  229. return response;
  230. }
  231. } catch (error) {
  232. console.error('Error fetching current price:', error);
  233. Alert.alert('錯誤', '無法獲取當前價格,請稍後再試');
  234. return null;
  235. }
  236. };
  237. const planMap = {
  238. // 3: { duration: 10, kWh: 3, displayDuration: 5, fee: 3 * currentPrice },
  239. 25: { duration: 40, kWh: 20, displayDuration: 25, fee: 20 * currentPrice },
  240. 30: { duration: 45, kWh: 25, displayDuration: 30, fee: 25 * currentPrice },
  241. 40: { duration: 55, kWh: 30, displayDuration: 40, fee: 30 * currentPrice },
  242. 45: { duration: 60, kWh: 40, displayDuration: 45, fee: 40 * currentPrice },
  243. full: { duration: 120, displayDuration: '充滿停機', fee: 80 * currentPrice }
  244. };
  245. // Function to handle barcode scanning
  246. const handleBarCodeScanned = async ({ bounds, data, type }: { bounds?: any; data: any; type: any }) => {
  247. if (
  248. !bounds ||
  249. typeof bounds.origin?.x !== 'number' ||
  250. typeof bounds.origin?.y !== 'number' ||
  251. typeof bounds.size?.width !== 'number' ||
  252. typeof bounds.size?.height !== 'number'
  253. ) {
  254. // console.log('Invalid or missing bounds data:', bounds);
  255. // Proceed with scanning logic without bounds checking
  256. setScanned(true);
  257. setScannedResult(data);
  258. Vibration.vibrate(100);
  259. // console.log(`type: ${type} data: ${data} typeofData ${typeof data}`);
  260. try {
  261. const price = await fetchCurrentPrice();
  262. console.log('fetchedCurrentPrice in scanQrPage', price);
  263. if (!price) {
  264. return; // Exit if price fetch failed
  265. }
  266. const response = await chargeStationService.getTodayReservation();
  267. if (response) {
  268. const now = new Date();
  269. const onlyThisConnector = response.filter(
  270. (reservation: any) => reservation.connector.ConnectorID === data
  271. );
  272. // Check availability for each duration```````````````````````````````````````````````````````````````````
  273. const availability = {
  274. // 3: checkAvailability(onlyThisConnector, now, 10) && walletBalance >= 3 * price,
  275. 25: checkAvailability(onlyThisConnector, now, 40) && walletBalance >= 20 * price,
  276. 30: checkAvailability(onlyThisConnector, now, 45) && walletBalance >= 25 * price,
  277. 40: checkAvailability(onlyThisConnector, now, 55) && walletBalance >= 30 * price,
  278. 45: checkAvailability(onlyThisConnector, now, 60) && walletBalance >= 40 * price,
  279. full: checkAvailability(onlyThisConnector, now, 120) && walletBalance >= 80 * price
  280. };
  281. setAvailableSlots(availability);
  282. setModalVisible(true);
  283. } else {
  284. Alert.alert('系統錯誤', '無法獲取預約信息。請稍後再試。');
  285. }
  286. } catch (error) {
  287. console.error("Error fetching today's reservations:", error);
  288. Alert.alert('系統錯誤', '發生未知錯誤。請稍後再試。');
  289. }
  290. setTimeout(() => {
  291. setScanned(false);
  292. }, 2000);
  293. return;
  294. }
  295. // -----------------------------------------------------------------------------------------------------
  296. const { origin, size } = bounds;
  297. // Calculate the size of the square transparent area
  298. const transparentAreaSize = Math.min(screenWidth * 0.6, screenHeight * 0.3);
  299. const transparentAreaX = (screenWidth - transparentAreaSize) / 2;
  300. const transparentAreaY = (screenHeight - transparentAreaSize) / 2;
  301. // Check if the barcode is within the transparent area
  302. if (
  303. origin.x >= transparentAreaX &&
  304. origin.y >= transparentAreaY &&
  305. origin.x + size.width <= transparentAreaX + transparentAreaSize &&
  306. origin.y + size.height <= transparentAreaY + transparentAreaSize
  307. ) {
  308. setScanned(true);
  309. setScannedResult(data);
  310. Vibration.vibrate(100);
  311. // console.log(` type: ${type} data: ${data} typeofData ${typeof data}`);
  312. try {
  313. const price = await fetchCurrentPrice();
  314. console.log('fetchedCurrentPrice in scanQrPage', price);
  315. if (!price) {
  316. return; // Exit if price fetch failed
  317. }
  318. const response = await chargeStationService.getTodayReservation();
  319. if (response) {
  320. const now = new Date();
  321. const onlyThisConnector = response.filter(
  322. (reservation: any) => reservation.connector.ConnectorID === data
  323. );
  324. console.log('onlyThisConnector', onlyThisConnector);
  325. // Check availability for each duration
  326. const availability = {
  327. // 3: checkAvailability(onlyThisConnector, now, 10) && walletBalance >= 3 * price,
  328. 25: checkAvailability(onlyThisConnector, now, 40) && walletBalance >= 20 * price,
  329. 30: checkAvailability(onlyThisConnector, now, 45) && walletBalance >= 25 * price,
  330. 40: checkAvailability(onlyThisConnector, now, 55) && walletBalance >= 30 * price,
  331. 45: checkAvailability(onlyThisConnector, now, 60) && walletBalance >= 40 * price,
  332. full: checkAvailability(onlyThisConnector, now, 120) && walletBalance >= 80 * price
  333. };
  334. // console.log('availability', availability);
  335. // console.log('walletBalance', walletBalance);
  336. // console.log('currentPricecurrentPricecurrentPricecurrentPricecurrentPrice', currentPrice);
  337. setAvailableSlots(availability);
  338. setModalVisible(true);
  339. } else {
  340. Alert.alert('系統錯誤', '無法獲取預約信息。請稍後再試。');
  341. }
  342. } catch (error) {
  343. console.error("Error fetching today's reservations:", error);
  344. Alert.alert('系統錯誤', '發生未知錯誤。請稍後再試。');
  345. }
  346. setTimeout(() => {
  347. setScanned(false);
  348. }, 2000);
  349. }
  350. };
  351. const checkAvailability = (reservations, startTime, duration) => {
  352. const endTime = new Date(startTime.getTime() + duration * 60000);
  353. // console.log('now', startTime);
  354. // console.log('endTime', endTime);
  355. // console.log('reservations', reservations);
  356. return !reservations.some((reservation) => {
  357. // Ignore reservations with status '9' (cancelled)
  358. if (reservation.status.id === '9' || reservation.status.id === '13') {
  359. return false;
  360. }
  361. // For status '8' (early finished), check actual_end_time
  362. if (reservation.status.id === '8' && reservation.actual_end_time) {
  363. const actualEndTime = new Date(reservation.actual_end_time);
  364. if (actualEndTime <= startTime) {
  365. return false; // Treat as available if actual end time is before or at start time
  366. }
  367. }
  368. const resStart = new Date(reservation.book_time);
  369. const resEnd = new Date(reservation.end_time);
  370. return startTime < resEnd && endTime > resStart;
  371. });
  372. };
  373. const handleDurationSelect = (duration) => {
  374. setSelectedDuration(duration);
  375. // console.log(duration);
  376. };
  377. const handleCancel = () => {
  378. setSelectedDuration(null);
  379. setModalVisible(false);
  380. if (router.canGoBack()) {
  381. router.back();
  382. } else {
  383. router.push('/mainPage');
  384. }
  385. };
  386. const handleConfirm = () => {
  387. if (selectedDuration !== null) {
  388. const now = new Date();
  389. let endTime;
  390. let fee;
  391. let totalPower;
  392. //i create a planMap2 because i want to move the planMap inside this component but i dont wanna move the outside one because i dont wanna make any potential disruptive changes
  393. const planMap2 = {
  394. // 3: { duration: 10, kWh: 3, displayDuration: 5, fee: 3 * currentPriceFetchedWhenScanQr },
  395. 25: { duration: 40, kWh: 20, displayDuration: 25, fee: 20 * currentPriceFetchedWhenScanQr },
  396. 30: { duration: 45, kWh: 25, displayDuration: 30, fee: 25 * currentPriceFetchedWhenScanQr },
  397. 40: { duration: 55, kWh: 30, displayDuration: 40, fee: 30 * currentPriceFetchedWhenScanQr },
  398. 45: { duration: 60, kWh: 40, displayDuration: 45, fee: 40 * currentPriceFetchedWhenScanQr },
  399. full: { duration: 120, displayDuration: '充滿停機', fee: 80 * currentPriceFetchedWhenScanQr }
  400. };
  401. if (selectedDuration === 'full') {
  402. endTime = new Date(now.getTime() + 2 * 60 * 60 * 1000); // 2 hours for "充滿停機"
  403. fee = planMap2.full.fee;
  404. totalPower = 80; // Set to 130 for "充滿停機"
  405. } else {
  406. const durationInMinutes = parseInt(selectedDuration);
  407. endTime = new Date(now.getTime() + durationInMinutes * 60 * 1000);
  408. // console.log('endTime', endTime);
  409. fee = planMap2[selectedDuration].fee;
  410. totalPower = planMap2[selectedDuration].kWh;
  411. }
  412. setTotalFee(fee);
  413. console.log('fee in scanQrPage-- this is the total_fee i send to backend', fee);
  414. const dataForSubmission = {
  415. stationID: '2405311022116801000',
  416. connector: scannedResult,
  417. user: userID,
  418. book_time: now,
  419. end_time: endTime,
  420. total_power: totalPower,
  421. total_fee: fee,
  422. // total_fee: 1,
  423. promotion_code: '',
  424. car: selectedCar,
  425. type: 'walking',
  426. is_ic_call: false
  427. };
  428. startCharging(dataForSubmission);
  429. setIsConfirmLoading(true);
  430. }
  431. };
  432. //below commented is the original WORKING startCharging, if i fucked up, return back to using this!!!
  433. // const startCharging = async (dataForSubmission) => {
  434. // try {
  435. // const wallet = await walletService.getWalletBalance();
  436. // console.log('wallet in startCharging in scanQrPage', wallet);
  437. // const response = await walletService.submitPayment(
  438. // dataForSubmission.stationID,
  439. // dataForSubmission.connector,
  440. // dataForSubmission.user,
  441. // dataForSubmission.book_time,
  442. // dataForSubmission.end_time,
  443. // dataForSubmission.total_power,
  444. // dataForSubmission.total_fee,
  445. // dataForSubmission.promotion_code,
  446. // dataForSubmission.car,
  447. // dataForSubmission.type,
  448. // dataForSubmission.is_ic_call
  449. // );
  450. // if (response.status === 200 || response.status === 201) {
  451. // setSelectedDuration(null);
  452. // console.log('Charging started from startCharging', response);
  453. // setIsConfirmLoading(false);
  454. // // Set a flag in AsyncStorage to indicate charging has started
  455. // await AsyncStorage.setItem('chargingStarted', 'true');
  456. // Alert.alert('啟動成功', '請稍後等待頁面自動跳轉至充電介面', [
  457. // {
  458. // text: 'OK',
  459. // onPress: async () => {
  460. // setModalVisible(false);
  461. // setLoading(true);
  462. // // Wait for 2 seconds
  463. // await new Promise((resolve) => setTimeout(resolve, 2000));
  464. // // Hide loading spinner and navigate
  465. // setLoading(false);
  466. // router.push('(auth)/(tabs)/(charging)/chargingPage');
  467. // }
  468. // }
  469. // ]);
  470. // // Start the navigation attempt loop
  471. // // startNavigationAttempts();
  472. // } else if (response.status === 400) {
  473. // console.log('400 error in paymentSummaryPageComponent');
  474. // Alert.alert('餘額不足', '您的餘額不足,請充值後再試。');
  475. // } else {
  476. // console.log('Failed to start charging:', response);
  477. // Alert.alert('掃描失敗 請稍後再試。', response);
  478. // }
  479. // } catch (error) {
  480. // console.log('Failed to start chasssssssrging:', error);
  481. // }
  482. // };
  483. //below is the new flow for startCharging.
  484. // useEffect(() => {
  485. // const subscription = AppState.addEventListener('change', (nextAppState) => {
  486. // if (
  487. // appState.current.match(/inactive|background/) &&
  488. // nextAppState === 'active' &&
  489. // isExpectingPayment &&
  490. // // outTradeNo &&
  491. // paymentInitiatedTime.current
  492. // ) {
  493. // const currentTime = new Date().getTime();
  494. // if (currentTime - paymentInitiatedTime.current < PAYMENT_CHECK_TIMEOUT) {
  495. // checkPaymentStatus();
  496. // } else {
  497. // // Payment check timeout reached
  498. // setIsExpectingPayment(false);
  499. // setOutTradeNo('');
  500. // paymentInitiatedTime.current = null;
  501. // Alert.alert(
  502. // 'Payment Timeout',
  503. // 'The payment status check has timed out. Please check your payment history.'
  504. // );
  505. // }
  506. // }
  507. // appState.current = nextAppState;
  508. // });
  509. // return () => {
  510. // subscription.remove();
  511. // };
  512. // }, [outTradeNo, isExpectingPayment]);
  513. //check payment status
  514. useEffect(() => {
  515. const subscription = AppState.addEventListener('change', (nextAppState) => {
  516. if (
  517. appState.current.match(/inactive|background/) &&
  518. nextAppState === 'active' &&
  519. isExpectingPayment &&
  520. // outTradeNo &&
  521. paymentInitiatedTime.current
  522. ) {
  523. const currentTime = new Date().getTime();
  524. if (currentTime - paymentInitiatedTime.current < PAYMENT_CHECK_TIMEOUT) {
  525. checkPaymentStatus();
  526. } else {
  527. // Payment check timeout reached
  528. setIsExpectingPayment(false);
  529. setOutTradeNo('');
  530. paymentInitiatedTime.current = null;
  531. Alert.alert(
  532. 'Payment Timeout',
  533. 'The payment status check has timed out. Please check your payment history.'
  534. );
  535. }
  536. }
  537. appState.current = nextAppState;
  538. });
  539. return () => {
  540. subscription.remove();
  541. };
  542. }, [outTradeNo, isExpectingPayment]);
  543. const checkPaymentStatus = async () => {
  544. try {
  545. // console.log('outTradeNo in scanQR Page checkpaymentstatus ', outTradeNo);
  546. const result = await walletService.checkPaymentStatus(outTradeNo);
  547. setPaymentStatus(result);
  548. // console.log('checkPaymentStatus from scan QR checkpaymentStatus', result);
  549. if (result && !result.some((item) => item.errmsg?.includes('處理中'))) {
  550. // Payment successful
  551. // console.log('totalFee', totalFee);
  552. Alert.alert(
  553. '付款已成功',
  554. `你已成功增值HKD $${
  555. Number.isInteger(totalFee) ? totalFee : totalFee.toFixed(1)
  556. }。請重新掃描去啟動充電槍。`,
  557. [
  558. {
  559. text: '確認',
  560. onPress: async () => {
  561. setModalVisible(false);
  562. router.dismiss();
  563. }
  564. }
  565. ]
  566. );
  567. } else {
  568. Alert.alert('付款失敗', '請再試一次。', [
  569. {
  570. text: '確定',
  571. onPress: () => {
  572. setModalVisible(false);
  573. router.dismiss();
  574. }
  575. }
  576. ]);
  577. }
  578. setIsExpectingPayment(false);
  579. setOutTradeNo('');
  580. paymentInitiatedTime.current = null;
  581. } catch (error) {
  582. console.error('Failed to check payment status:', error);
  583. Alert.alert('Error', 'Failed to check payment status. Please check your payment history.');
  584. }
  585. };
  586. function formatTime(utcTimeString) {
  587. // Parse the UTC time string
  588. const date = new Date(utcTimeString);
  589. // Add 8 hours
  590. date.setHours(date.getHours());
  591. // Format the date
  592. const year = date.getFullYear();
  593. const month = String(date.getMonth() + 1).padStart(2, '0');
  594. const day = String(date.getDate()).padStart(2, '0');
  595. const hours = String(date.getHours()).padStart(2, '0');
  596. const minutes = String(date.getMinutes()).padStart(2, '0');
  597. const seconds = String(date.getSeconds()).padStart(2, '0');
  598. // Return the formatted string
  599. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  600. }
  601. const oneTimeCharging = async (inputAmount) => {
  602. try {
  603. const response = await walletService.getOutTradeNo();
  604. // console.log('outtradeno in oneTimeCharging', response);
  605. if (response) {
  606. setOutTradeNo(response);
  607. setIsExpectingPayment(true);
  608. paymentInitiatedTime.current = new Date().getTime();
  609. const now = new Date();
  610. const formattedTime = formatTime(now);
  611. let amount = inputAmount * 100;
  612. const origin = 'https://openapi-hk.qfapi.com/checkstand/#/?';
  613. const obj = {
  614. appcode: '6937EF25DF6D4FA78BB2285441BC05E9',
  615. goods_name: 'Crazy Charge 錢包增值',
  616. out_trade_no: response,
  617. paysource: 'crazycharge_checkout',
  618. return_url: 'https://crazycharge.com.hk/completed',
  619. failed_url: 'https://crazycharge.com.hk/failed',
  620. notify_url: 'https://api.crazycharge.com.hk/api/v1/clients/qfpay/webhook',
  621. sign_type: 'sha256',
  622. txamt: amount,
  623. txcurrcd: 'HKD',
  624. txdtm: formattedTime
  625. };
  626. const paramStringify = (json, flag?) => {
  627. let str = '';
  628. let keysArr = Object.keys(json);
  629. keysArr.sort().forEach((val) => {
  630. if (!json[val]) return;
  631. str += `${val}=${flag ? encodeURIComponent(json[val]) : json[val]}&`;
  632. });
  633. return str.slice(0, -1);
  634. };
  635. const api_key = '8F59E31F6ADF4D2894365F2BB6D2FF2C';
  636. const params = paramStringify(obj);
  637. const sign = sha256(`${params}${api_key}`).toString();
  638. const url = `${origin}${paramStringify(obj, true)}&sign=${sign}`;
  639. try {
  640. // console.log(url);
  641. const supported = await Linking.canOpenURL(url);
  642. if (supported) {
  643. await Linking.openURL(url);
  644. } else {
  645. Alert.alert('錯誤', '請稍後再試');
  646. }
  647. } catch (error) {
  648. console.error('Top-up failed:', error);
  649. Alert.alert('Error', '一次性付款失敗,請稍後再試');
  650. }
  651. } else {
  652. console.log('nasdasdasdsdfgo');
  653. }
  654. } catch (error) {
  655. console.log(error);
  656. }
  657. };
  658. const startCharging = async (dataForSubmission) => {
  659. try {
  660. //before i start below logic, i need to check if the user has penalty unpaid.
  661. //i will call fetchReservationHistories. and the api will return an array of object, within the object there is a field called "penalty_fee".
  662. //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.
  663. const reservationHistories = await chargeStationService.fetchReservationHistories();
  664. const unpaidPenalties = reservationHistories.filter(
  665. (reservation) => reservation.penalty_fee > 0 && reservation.penalty_paid_status === false
  666. );
  667. const mostRecentUnpaidReservation = unpaidPenalties.reduce((mostRecent, current) => {
  668. return new Date(mostRecent.created_at) > new Date(current.created_at) ? mostRecent : current;
  669. }, unpaidPenalties[0]);
  670. if (unpaidPenalties.length > 0) {
  671. Alert.alert(
  672. '未付罰款',
  673. '您有未支付的罰款。請先支付罰款後再開始充電。',
  674. [
  675. {
  676. text: '查看詳情',
  677. onPress: () => {
  678. // Navigate to a page showing penalty details
  679. setModalVisible(false);
  680. setLoading(false);
  681. router.push({
  682. pathname: '(auth)/(tabs)/(home)/penaltyPaymentPage',
  683. params: {
  684. book_time: mostRecentUnpaidReservation.book_time,
  685. end_time: mostRecentUnpaidReservation.end_time,
  686. actual_end_time: mostRecentUnpaidReservation.actual_end_time,
  687. penalty_fee: mostRecentUnpaidReservation.penalty_fee,
  688. format_order_id: mostRecentUnpaidReservation.format_order_id,
  689. id: mostRecentUnpaidReservation.id,
  690. stationName:
  691. mostRecentUnpaidReservation.connector.EquipmentID.StationID.snapshot
  692. .StationName,
  693. address:
  694. mostRecentUnpaidReservation.connector.EquipmentID.StationID.snapshot.Address
  695. }
  696. });
  697. }
  698. },
  699. {
  700. text: '返回',
  701. onPress: () => {
  702. setModalVisible(false);
  703. if (router.canGoBack()) {
  704. router.back();
  705. } else {
  706. router.push('/mainPage');
  707. }
  708. }
  709. }
  710. ],
  711. { cancelable: false }
  712. );
  713. return;
  714. }
  715. const wallet = await walletService.getWalletBalance();
  716. if (wallet < dataForSubmission.total_fee) {
  717. oneTimeCharging(dataForSubmission.total_fee);
  718. // const remainingAmount = dataForSubmission.total_fee - wallet;
  719. // oneTimeCharging(remainingAmount);
  720. return;
  721. }
  722. const response = await walletService.submitPayment(
  723. dataForSubmission.stationID,
  724. dataForSubmission.connector,
  725. dataForSubmission.user,
  726. dataForSubmission.book_time,
  727. dataForSubmission.end_time,
  728. dataForSubmission.total_power,
  729. dataForSubmission.total_fee,
  730. dataForSubmission.promotion_code,
  731. dataForSubmission.car,
  732. dataForSubmission.type,
  733. dataForSubmission.is_ic_call
  734. );
  735. if (response.status === 200 || response.status === 201) {
  736. setSelectedDuration(null);
  737. setIsConfirmLoading(false);
  738. await AsyncStorage.setItem('chargingStarted', 'true');
  739. Alert.alert('啟動成功', '請按下確認並等待頁面稍後自動跳轉至充電介面', [
  740. {
  741. text: 'OK',
  742. onPress: async () => {
  743. setModalVisible(false);
  744. setLoading(true);
  745. // Wait for 2 seconds
  746. await new Promise((resolve) => setTimeout(resolve, 2000));
  747. // Hide loading spinner and navigate
  748. setLoading(false);
  749. router.navigate('(auth)/(tabs)/(home)/mainPage');
  750. router.push('(auth)/(tabs)/(charging)/chargingPage');
  751. }
  752. }
  753. ]);
  754. } else if (response.status === 400) {
  755. console.log('400 error in paymentSummaryPageComponent');
  756. Alert.alert('餘額不足', '掃描失敗 請稍後再試。');
  757. } else {
  758. console.log('Failed to start charging:', response);
  759. Alert.alert('掃描失敗 請稍後再試。', response);
  760. }
  761. } catch (error) {
  762. console.log('Failed to start chasssssssrging:', error);
  763. }
  764. };
  765. // const startNavigationAttempts = () => {
  766. // let attempts = 0;
  767. // const maxAttempts = 10; // Try for about 2.5 minutes (10 * 15 seconds)
  768. // const attemptNavigation = async () => {
  769. // try {
  770. // const chargingStarted = await AsyncStorage.getItem('chargingStarted');
  771. // if (chargingStarted === 'true') {
  772. // // Wait for 2 seconds before navigating
  773. // await new Promise((resolve) => setTimeout(resolve, 2000));
  774. // await AsyncStorage.removeItem('chargingStarted');
  775. // router.push('(auth)/(tabs)/(charging)/chargingPage');
  776. // // If navigation is successful, clear the flag
  777. // } else {
  778. // throw new Error('Navigation not ready');
  779. // }
  780. // } catch (error) {
  781. // attempts++;
  782. // if (attempts < maxAttempts) {
  783. // // If navigation fails, try again after 15 seconds
  784. // setTimeout(attemptNavigation, 15000);
  785. // } else {
  786. // // If all attempts fail, show an alert to the user
  787. // Alert.alert('導航失敗', '無法自動跳轉到充電頁面。請手動導航到充電頁面。', [
  788. // { text: 'OK', onPress: () => {} }
  789. // ]);
  790. // }
  791. // }
  792. // };
  793. // // Start the first attempt after 15 seconds
  794. // setTimeout(attemptNavigation, 15000);
  795. // };
  796. // return (
  797. // <View style={styles.container} ref={viewRef}>
  798. // {loading ? (
  799. // <View className="flex-1 items-center justify-center">
  800. // <ActivityIndicator />
  801. // </View>
  802. // ) : (
  803. // <CameraView
  804. // style={styles.camera}
  805. // facing="back"
  806. // barcodeScannerSettings={{
  807. // barcodeTypes: ['qr']
  808. // }}
  809. // onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
  810. // responsiveOrientationWhenOrientationLocked={true}
  811. // >
  812. // <View style={styles.overlay}>
  813. // <View style={styles.topOverlay}>
  814. // {/* <ChooseCar
  815. // carData={carData}
  816. // loading={loading}
  817. // selectedCar={selectedCar}
  818. // setSelectedCar={setSelectedCar}
  819. // /> */}
  820. // <Pressable
  821. // // style={styles.closeButton}
  822. // className="absolute top-20 left-10 z-10 "
  823. // onPress={() => {
  824. // if (router.canGoBack()) {
  825. // router.back();
  826. // } else {
  827. // router.push('/mainPage');
  828. // }
  829. // }}
  830. // >
  831. // <CrossLogoWhiteSvg />
  832. // </Pressable>
  833. // </View>
  834. // <View style={styles.centerRow}>
  835. // <View style={styles.leftOverlay}></View>
  836. // <View style={styles.transparentArea}></View>
  837. // <View style={styles.rightOverlay} />
  838. // </View>
  839. // <View className="items-center justify-between" style={styles.bottomOverlay}>
  840. // <View>
  841. // <Text className="text-white text-lg font-bold mt-2 text-center">
  842. // 請掃瞄充電座上的二維碼
  843. // </Text>
  844. // </View>
  845. // <View className="flex-row space-x-2 items-center ">
  846. // <QuestionSvg />
  847. // <Pressable onPress={() => router.push('assistancePage')}>
  848. // <Text className="text-white text-base">需要協助?</Text>
  849. // </Pressable>
  850. // </View>
  851. // <View />
  852. // </View>
  853. // </View>
  854. // </CameraView>
  855. // )}
  856. // <Modal isVisible={isModalVisible} backdropOpacity={0.5} animationIn="fadeIn" animationOut="fadeOut">
  857. // <View style={styles.modalContent} className="flex flex-col">
  858. // <Text className="text-xl font-bold mt-2 text-center">請選擇充電時間</Text>
  859. // <Text className="text-base m-2 mb-4 text-center">按鈕呈紅色代表該時段已被他人預約</Text>
  860. // <View className="flex flex-row flex-wrap ">
  861. // {Object.entries(availableSlots).map(([duration, available]) => (
  862. // <NormalButton
  863. // key={duration}
  864. // title={
  865. // duration === 'full' ? (
  866. // <Text className={selectedDuration === duration ? 'text-white' : ''}>
  867. // 充滿停機
  868. // </Text>
  869. // ) : (
  870. // <Text
  871. // className={selectedDuration === duration ? 'text-white' : ''}
  872. // >{`${planMap[duration].kWh} 度電 - ${planMap[duration].displayDuration} 分鐘`}</Text>
  873. // )
  874. // }
  875. // onPress={() => handleDurationSelect(duration)}
  876. // extendedStyle={[
  877. // styles.durationButton,
  878. // {
  879. // backgroundColor: available
  880. // ? selectedDuration === duration
  881. // ? '#02677d'
  882. // : 'white'
  883. // : 'red',
  884. // borderColor: available ? 'black' : 'red',
  885. // borderWidth: 1
  886. // }
  887. // ]}
  888. // disabled={!available}
  889. // />
  890. // ))}
  891. // </View>
  892. // {selectedDuration && (
  893. // <NormalButton
  894. // title={
  895. // isConfirmLoading ? (
  896. // <ActivityIndicator color="white" />
  897. // ) : (
  898. // <Text className="text-white">確認</Text>
  899. // )
  900. // }
  901. // onPress={handleConfirm}
  902. // extendedStyle={styles.confirmButton}
  903. // />
  904. // )}
  905. // <NormalButton
  906. // title={<Text className="">取消</Text>}
  907. // onPress={handleCancel}
  908. // extendedStyle={styles.cancelButton}
  909. // />
  910. // </View>
  911. // </Modal>
  912. // </View>
  913. // );
  914. // console.log('availableSlots', availableSlots);
  915. return (
  916. <View style={styles.container} ref={viewRef}>
  917. {!permission ? (
  918. <View />
  919. ) : !permission.granted ? (
  920. <View className="flex-1 justify-center items-center">
  921. <Text style={{ textAlign: 'center' }}>
  922. 我們需要相機權限來掃描機器上的二維碼,以便識別並啟動充電機器。我們不會儲存或共享任何掃描到的資訊。
  923. 請前往設定開啟相機權限
  924. </Text>
  925. </View>
  926. ) : loading ? (
  927. <View className="flex-1 items-center justify-center">
  928. <ActivityIndicator />
  929. </View>
  930. ) : (
  931. <CameraView
  932. style={styles.camera}
  933. facing="back"
  934. barcodeScannerSettings={{
  935. barcodeTypes: ['qr']
  936. }}
  937. onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
  938. responsiveOrientationWhenOrientationLocked={true}
  939. >
  940. <View style={styles.overlay}>
  941. <View style={styles.topOverlay}>
  942. <Pressable
  943. className="absolute top-20 left-10 z-10 p-4"
  944. hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }} // Added hitSlop
  945. onPress={() => {
  946. if (router.canGoBack()) {
  947. router.back();
  948. } else {
  949. router.push('/mainPage');
  950. }
  951. }}
  952. >
  953. <View style={{ transform: [{ scale: 1.5 }] }}>
  954. <CrossLogoWhiteSvg />
  955. </View>
  956. </Pressable>
  957. </View>
  958. <View style={styles.centerRow}>
  959. <View style={styles.leftOverlay}></View>
  960. <View style={styles.transparentArea}></View>
  961. <View style={styles.rightOverlay} />
  962. </View>
  963. <View className="items-center justify-between" style={styles.bottomOverlay}>
  964. <View>
  965. <Text className="text-white text-lg font-bold mt-2 text-center">
  966. 請掃瞄充電座上的二維碼
  967. </Text>
  968. </View>
  969. <View className="flex-row space-x-2 items-center ">
  970. <QuestionSvg />
  971. <Pressable onPress={() => router.push('assistancePage')}>
  972. <Text className="text-white text-base">需要協助?</Text>
  973. </Pressable>
  974. </View>
  975. <View />
  976. </View>
  977. </View>
  978. </CameraView>
  979. )}
  980. <Modal isVisible={isModalVisible} backdropOpacity={0.5} animationIn="fadeIn" animationOut="fadeOut">
  981. <View style={styles.modalContent} className="flex flex-col">
  982. <Text className="text-xl font-bold mt-2 text-center">請選擇充電時間</Text>
  983. {Object.values(availableSlots).some((available) => !available) && (
  984. <Text className="text-base mt-1 text-center">
  985. 按鈕呈紅色代表現時錢包餘額不足以支付選擇的充電時間,選擇紅色按鈕並按下確認鍵會進行一次性充值付費
  986. </Text>
  987. )}
  988. <Text className="text-base text-center mb-4"></Text>
  989. <View className="flex flex-row flex-wrap ">
  990. {Object.entries(availableSlots).map(([duration, available]) => (
  991. <NormalButton
  992. key={duration}
  993. title={
  994. duration === 'full' ? (
  995. <Text className={selectedDuration === duration ? 'text-white' : ''}>
  996. 充滿停機 (最多80度電)
  997. </Text>
  998. ) : (
  999. <Text
  1000. className={selectedDuration === duration ? 'text-white' : ''}
  1001. >{`${planMap[duration].kWh} 度電 - ${planMap[duration].displayDuration} 分鐘`}</Text>
  1002. )
  1003. }
  1004. onPress={() => handleDurationSelect(duration)}
  1005. extendedStyle={[
  1006. styles.durationButton,
  1007. {
  1008. backgroundColor: available
  1009. ? selectedDuration === duration
  1010. ? '#02677d'
  1011. : 'white'
  1012. : selectedDuration === duration
  1013. ? '#8B0000' // Darker red when selected
  1014. : 'red', // Normal red when not selected
  1015. borderColor: available ? 'black' : 'red',
  1016. borderWidth: 1
  1017. }
  1018. ]}
  1019. />
  1020. ))}
  1021. </View>
  1022. {selectedDuration && (
  1023. <NormalButton
  1024. title={
  1025. isConfirmLoading ? (
  1026. <ActivityIndicator color="white" />
  1027. ) : (
  1028. <Text className="text-white">確認</Text>
  1029. )
  1030. }
  1031. onPress={handleConfirm}
  1032. extendedStyle={styles.confirmButton}
  1033. />
  1034. )}
  1035. <NormalButton
  1036. title={<Text className="">取消</Text>}
  1037. onPress={handleCancel}
  1038. extendedStyle={styles.cancelButton}
  1039. />
  1040. </View>
  1041. </Modal>
  1042. </View>
  1043. );
  1044. };
  1045. const styles = StyleSheet.create({
  1046. container: {
  1047. flex: 1
  1048. },
  1049. camera: {
  1050. flex: 1
  1051. },
  1052. overlay: {
  1053. flex: 1
  1054. },
  1055. topOverlay: {
  1056. flex: 35,
  1057. alignItems: 'center',
  1058. backgroundColor: 'rgba(0,0,0,0.5)'
  1059. },
  1060. centerRow: {
  1061. flex: 30,
  1062. flexDirection: 'row'
  1063. },
  1064. leftOverlay: {
  1065. flex: 20,
  1066. backgroundColor: 'rgba(0,0,0,0.5)'
  1067. },
  1068. transparentArea: {
  1069. flex: 60,
  1070. aspectRatio: 1,
  1071. position: 'relative'
  1072. },
  1073. rightOverlay: {
  1074. flex: 20,
  1075. backgroundColor: 'rgba(0,0,0,0.5)'
  1076. },
  1077. bottomOverlay: {
  1078. flex: 35,
  1079. backgroundColor: 'rgba(0,0,0,0.5)'
  1080. },
  1081. closeButton: {
  1082. position: 'absolute',
  1083. top: 40,
  1084. left: 20,
  1085. zIndex: 1
  1086. },
  1087. modalContent: {
  1088. backgroundColor: 'white',
  1089. padding: 22,
  1090. alignItems: 'center',
  1091. borderRadius: 4,
  1092. borderColor: 'rgba(0, 0, 0, 0.1)'
  1093. },
  1094. durationButton: { margin: 5 },
  1095. confirmButton: {
  1096. marginTop: 20,
  1097. width: '100%'
  1098. },
  1099. cancelButton: {
  1100. marginTop: 20,
  1101. width: '100%',
  1102. backgroundColor: 'white',
  1103. borderColor: 'black',
  1104. borderWidth: 1,
  1105. color: 'black'
  1106. }
  1107. });
  1108. export default ScanQrPage;