scanQrPage.tsx 56 KB

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