scanQrPage.tsx 48 KB

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