scanQrPage.tsx 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052
  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. const wallet = await walletService.getWalletBalance();
  597. console.log('wallet in startCharging in scanQrPage', wallet);
  598. oneTimeCharging(dataForSubmission.total_fee);
  599. if (wallet < dataForSubmission.total_fee) {
  600. oneTimeCharging(dataForSubmission.total_fee);
  601. return;
  602. }
  603. const response = await walletService.submitPayment(
  604. dataForSubmission.stationID,
  605. dataForSubmission.connector,
  606. dataForSubmission.user,
  607. dataForSubmission.book_time,
  608. dataForSubmission.end_time,
  609. dataForSubmission.total_power,
  610. dataForSubmission.total_fee,
  611. dataForSubmission.promotion_code,
  612. dataForSubmission.car,
  613. dataForSubmission.type,
  614. dataForSubmission.is_ic_call
  615. );
  616. if (response.status === 200 || response.status === 201) {
  617. setSelectedDuration(null);
  618. console.log('Charging started from startCharging', response);
  619. setIsConfirmLoading(false);
  620. // Set a flag in AsyncStorage to indicate charging has started
  621. await AsyncStorage.setItem('chargingStarted', 'true');
  622. Alert.alert('啟動成功', '請稍後等待頁面自動跳轉至充電介面', [
  623. {
  624. text: 'OK',
  625. onPress: async () => {
  626. setModalVisible(false);
  627. setLoading(true);
  628. // Wait for 2 seconds
  629. await new Promise((resolve) => setTimeout(resolve, 2000));
  630. // Hide loading spinner and navigate
  631. setLoading(false);
  632. router.push('(auth)/(tabs)/(charging)/chargingPage');
  633. }
  634. }
  635. ]);
  636. // Start the navigation attempt loop
  637. // startNavigationAttempts();
  638. } else if (response.status === 400) {
  639. console.log('400 error in paymentSummaryPageComponent');
  640. Alert.alert('餘額不足', '您的餘額不足,請充值後再試。');
  641. } else {
  642. console.log('Failed to start charging:', response);
  643. Alert.alert('掃描失敗 請稍後再試。', response);
  644. }
  645. } catch (error) {
  646. console.log('Failed to start chasssssssrging:', error);
  647. }
  648. };
  649. // const startNavigationAttempts = () => {
  650. // let attempts = 0;
  651. // const maxAttempts = 10; // Try for about 2.5 minutes (10 * 15 seconds)
  652. // const attemptNavigation = async () => {
  653. // try {
  654. // const chargingStarted = await AsyncStorage.getItem('chargingStarted');
  655. // if (chargingStarted === 'true') {
  656. // // Wait for 2 seconds before navigating
  657. // await new Promise((resolve) => setTimeout(resolve, 2000));
  658. // await AsyncStorage.removeItem('chargingStarted');
  659. // router.push('(auth)/(tabs)/(charging)/chargingPage');
  660. // // If navigation is successful, clear the flag
  661. // } else {
  662. // throw new Error('Navigation not ready');
  663. // }
  664. // } catch (error) {
  665. // attempts++;
  666. // if (attempts < maxAttempts) {
  667. // // If navigation fails, try again after 15 seconds
  668. // setTimeout(attemptNavigation, 15000);
  669. // } else {
  670. // // If all attempts fail, show an alert to the user
  671. // Alert.alert('導航失敗', '無法自動跳轉到充電頁面。請手動導航到充電頁面。', [
  672. // { text: 'OK', onPress: () => {} }
  673. // ]);
  674. // }
  675. // }
  676. // };
  677. // // Start the first attempt after 15 seconds
  678. // setTimeout(attemptNavigation, 15000);
  679. // };
  680. // return (
  681. // <View style={styles.container} ref={viewRef}>
  682. // {loading ? (
  683. // <View className="flex-1 items-center justify-center">
  684. // <ActivityIndicator />
  685. // </View>
  686. // ) : (
  687. // <CameraView
  688. // style={styles.camera}
  689. // facing="back"
  690. // barcodeScannerSettings={{
  691. // barcodeTypes: ['qr']
  692. // }}
  693. // onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
  694. // responsiveOrientationWhenOrientationLocked={true}
  695. // >
  696. // <View style={styles.overlay}>
  697. // <View style={styles.topOverlay}>
  698. // {/* <ChooseCar
  699. // carData={carData}
  700. // loading={loading}
  701. // selectedCar={selectedCar}
  702. // setSelectedCar={setSelectedCar}
  703. // /> */}
  704. // <Pressable
  705. // // style={styles.closeButton}
  706. // className="absolute top-20 left-10 z-10 "
  707. // onPress={() => {
  708. // if (router.canGoBack()) {
  709. // router.back();
  710. // } else {
  711. // router.push('/mainPage');
  712. // }
  713. // }}
  714. // >
  715. // <CrossLogoWhiteSvg />
  716. // </Pressable>
  717. // </View>
  718. // <View style={styles.centerRow}>
  719. // <View style={styles.leftOverlay}></View>
  720. // <View style={styles.transparentArea}></View>
  721. // <View style={styles.rightOverlay} />
  722. // </View>
  723. // <View className="items-center justify-between" style={styles.bottomOverlay}>
  724. // <View>
  725. // <Text className="text-white text-lg font-bold mt-2 text-center">
  726. // 請掃瞄充電座上的二維碼
  727. // </Text>
  728. // </View>
  729. // <View className="flex-row space-x-2 items-center ">
  730. // <QuestionSvg />
  731. // <Pressable onPress={() => router.push('assistancePage')}>
  732. // <Text className="text-white text-base">需要協助?</Text>
  733. // </Pressable>
  734. // </View>
  735. // <View />
  736. // </View>
  737. // </View>
  738. // </CameraView>
  739. // )}
  740. // <Modal isVisible={isModalVisible} backdropOpacity={0.5} animationIn="fadeIn" animationOut="fadeOut">
  741. // <View style={styles.modalContent} className="flex flex-col">
  742. // <Text className="text-xl font-bold mt-2 text-center">請選擇充電時間</Text>
  743. // <Text className="text-base m-2 mb-4 text-center">按鈕呈紅色代表該時段已被他人預約</Text>
  744. // <View className="flex flex-row flex-wrap ">
  745. // {Object.entries(availableSlots).map(([duration, available]) => (
  746. // <NormalButton
  747. // key={duration}
  748. // title={
  749. // duration === 'full' ? (
  750. // <Text className={selectedDuration === duration ? 'text-white' : ''}>
  751. // 充滿停機
  752. // </Text>
  753. // ) : (
  754. // <Text
  755. // className={selectedDuration === duration ? 'text-white' : ''}
  756. // >{`${planMap[duration].kWh} 度電 - ${planMap[duration].displayDuration} 分鐘`}</Text>
  757. // )
  758. // }
  759. // onPress={() => handleDurationSelect(duration)}
  760. // extendedStyle={[
  761. // styles.durationButton,
  762. // {
  763. // backgroundColor: available
  764. // ? selectedDuration === duration
  765. // ? '#02677d'
  766. // : 'white'
  767. // : 'red',
  768. // borderColor: available ? 'black' : 'red',
  769. // borderWidth: 1
  770. // }
  771. // ]}
  772. // disabled={!available}
  773. // />
  774. // ))}
  775. // </View>
  776. // {selectedDuration && (
  777. // <NormalButton
  778. // title={
  779. // isConfirmLoading ? (
  780. // <ActivityIndicator color="white" />
  781. // ) : (
  782. // <Text className="text-white">確認</Text>
  783. // )
  784. // }
  785. // onPress={handleConfirm}
  786. // extendedStyle={styles.confirmButton}
  787. // />
  788. // )}
  789. // <NormalButton
  790. // title={<Text className="">取消</Text>}
  791. // onPress={handleCancel}
  792. // extendedStyle={styles.cancelButton}
  793. // />
  794. // </View>
  795. // </Modal>
  796. // </View>
  797. // );
  798. return (
  799. <View style={styles.container} ref={viewRef}>
  800. {!permission ? (
  801. <View />
  802. ) : !permission.granted ? (
  803. <View className="flex-1 justify-center items-center">
  804. <Text style={{ textAlign: 'center' }}>
  805. 我們需要相機權限來掃描機器上的二維碼,以便識別並啟動充電機器。我們不會儲存或共享任何掃描到的資訊。
  806. 請前往設定開啟相機權限
  807. </Text>
  808. </View>
  809. ) : loading ? (
  810. <View className="flex-1 items-center justify-center">
  811. <ActivityIndicator />
  812. </View>
  813. ) : (
  814. <CameraView
  815. style={styles.camera}
  816. facing="back"
  817. barcodeScannerSettings={{
  818. barcodeTypes: ['qr']
  819. }}
  820. onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
  821. responsiveOrientationWhenOrientationLocked={true}
  822. >
  823. <View style={styles.overlay}>
  824. <View style={styles.topOverlay}>
  825. <Pressable
  826. className="absolute top-20 left-10 z-10 "
  827. onPress={() => {
  828. if (router.canGoBack()) {
  829. router.back();
  830. } else {
  831. router.push('/mainPage');
  832. }
  833. }}
  834. >
  835. <CrossLogoWhiteSvg />
  836. </Pressable>
  837. </View>
  838. <View style={styles.centerRow}>
  839. <View style={styles.leftOverlay}></View>
  840. <View style={styles.transparentArea}></View>
  841. <View style={styles.rightOverlay} />
  842. </View>
  843. <View className="items-center justify-between" style={styles.bottomOverlay}>
  844. <View>
  845. <Text className="text-white text-lg font-bold mt-2 text-center">
  846. 請掃瞄充電座上的二維碼
  847. </Text>
  848. </View>
  849. <View className="flex-row space-x-2 items-center ">
  850. <QuestionSvg />
  851. <Pressable onPress={() => router.push('assistancePage')}>
  852. <Text className="text-white text-base">需要協助?</Text>
  853. </Pressable>
  854. </View>
  855. <View />
  856. </View>
  857. </View>
  858. </CameraView>
  859. )}
  860. <Modal isVisible={isModalVisible} backdropOpacity={0.5} animationIn="fadeIn" animationOut="fadeOut">
  861. <View style={styles.modalContent} className="flex flex-col">
  862. <Text className="text-xl font-bold mt-2 text-center">請選擇充電時間</Text>
  863. <Text className="text-base m-2 mb-4 text-center">按鈕呈紅色代表該時段已被他人預約</Text>
  864. <View className="flex flex-row flex-wrap ">
  865. {Object.entries(availableSlots).map(([duration, available]) => (
  866. <NormalButton
  867. key={duration}
  868. title={
  869. duration === 'full' ? (
  870. <Text className={selectedDuration === duration ? 'text-white' : ''}>
  871. 充滿停機
  872. </Text>
  873. ) : (
  874. <Text
  875. className={selectedDuration === duration ? 'text-white' : ''}
  876. >{`${planMap[duration].kWh} 度電 - ${planMap[duration].displayDuration} 分鐘`}</Text>
  877. )
  878. }
  879. onPress={() => handleDurationSelect(duration)}
  880. extendedStyle={[
  881. styles.durationButton,
  882. {
  883. backgroundColor: available
  884. ? selectedDuration === duration
  885. ? '#02677d'
  886. : 'white'
  887. : 'red',
  888. borderColor: available ? 'black' : 'red',
  889. borderWidth: 1
  890. }
  891. ]}
  892. disabled={!available}
  893. />
  894. ))}
  895. </View>
  896. {selectedDuration && (
  897. <NormalButton
  898. title={
  899. isConfirmLoading ? (
  900. <ActivityIndicator color="white" />
  901. ) : (
  902. <Text className="text-white">確認</Text>
  903. )
  904. }
  905. onPress={handleConfirm}
  906. extendedStyle={styles.confirmButton}
  907. />
  908. )}
  909. <NormalButton
  910. title={<Text className="">取消</Text>}
  911. onPress={handleCancel}
  912. extendedStyle={styles.cancelButton}
  913. />
  914. </View>
  915. </Modal>
  916. </View>
  917. );
  918. };
  919. const styles = StyleSheet.create({
  920. container: {
  921. flex: 1
  922. },
  923. camera: {
  924. flex: 1
  925. },
  926. overlay: {
  927. flex: 1
  928. },
  929. topOverlay: {
  930. flex: 35,
  931. alignItems: 'center',
  932. backgroundColor: 'rgba(0,0,0,0.5)'
  933. },
  934. centerRow: {
  935. flex: 30,
  936. flexDirection: 'row'
  937. },
  938. leftOverlay: {
  939. flex: 20,
  940. backgroundColor: 'rgba(0,0,0,0.5)'
  941. },
  942. transparentArea: {
  943. flex: 60,
  944. aspectRatio: 1,
  945. position: 'relative'
  946. },
  947. rightOverlay: {
  948. flex: 20,
  949. backgroundColor: 'rgba(0,0,0,0.5)'
  950. },
  951. bottomOverlay: {
  952. flex: 35,
  953. backgroundColor: 'rgba(0,0,0,0.5)'
  954. },
  955. closeButton: {
  956. position: 'absolute',
  957. top: 40,
  958. left: 20,
  959. zIndex: 1
  960. },
  961. modalContent: {
  962. backgroundColor: 'white',
  963. padding: 22,
  964. alignItems: 'center',
  965. borderRadius: 4,
  966. borderColor: 'rgba(0, 0, 0, 0.1)'
  967. },
  968. durationButton: { margin: 5 },
  969. confirmButton: {
  970. marginTop: 20,
  971. width: '100%'
  972. },
  973. cancelButton: {
  974. marginTop: 20,
  975. width: '100%',
  976. backgroundColor: 'white',
  977. borderColor: 'black',
  978. borderWidth: 1,
  979. color: 'black'
  980. }
  981. });
  982. export default ScanQrPage;