scanQrPage.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  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. Platform,
  16. } from 'react-native';
  17. import sha256 from 'crypto-js/sha256';
  18. import ChooseCarForChargingRow from '../../../../component/global/chooseCarForChargingRow';
  19. import { CrossLogoWhiteSvg, QuestionSvg } from '../../../../component/global/SVG';
  20. import { router, useFocusEffect } from 'expo-router';
  21. import { chargeStationService } from '../../../../service/chargeStationService';
  22. import { authenticationService } from '../../../../service/authService';
  23. import { walletService } from '../../../../service/walletService';
  24. import useUserInfoStore from '../../../../providers/userinfo_store';
  25. import Modal from 'react-native-modal';
  26. import NormalButton from '../../../../component/global/normal_button';
  27. import { ceil } from 'lodash';
  28. import AsyncStorage from '@react-native-async-storage/async-storage';
  29. import { useChargingStore } from '../../../../providers/scan_qr_payload_store';
  30. const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
  31. //reminder: scan qr code page, ic call should be false
  32. const ScanQrPage = () => {
  33. const { userID, currentPrice, setCurrentPrice } = useUserInfoStore();
  34. const [currentPriceFetchedWhenScanQr, setCurrentPriceFetchedWhenScanQr] = useState(0);
  35. const { scanned_qr_code, setScannedQrCode, stationID, setStationId } = useChargingStore();
  36. const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
  37. const [permission, requestPermission] = useCameraPermissions();
  38. const [scanned, setScanned] = useState(false);
  39. const viewRef = useRef(null);
  40. const [scannedResult, setScannedResult] = useState('');
  41. const [selectedCar, setSelectedCar] = useState('');
  42. const now = new Date();
  43. const [loading, setLoading] = useState(true);
  44. const [loading2, setLoading2] = useState(false);
  45. const [loading3, setLoading3] = useState(false);
  46. const [carData, setCarData] = useState([]);
  47. const [isModalVisible, setModalVisible] = useState(false);
  48. const [isConfirmLoading, setIsConfirmLoading] = useState(false);
  49. const [availableSlots, setAvailableSlots] = useState({
  50. // 3: false,
  51. 25: false,
  52. 30: false,
  53. 40: false,
  54. 45: false,
  55. full: false
  56. });
  57. const [selectedDuration, setSelectedDuration] = useState(null);
  58. const appState = useRef(AppState.currentState);
  59. const [paymentStatus, setPaymentStatus] = useState(null);
  60. const [isExpectingPayment, setIsExpectingPayment] = useState(false);
  61. const paymentInitiatedTime = useRef(null);
  62. const PAYMENT_CHECK_TIMEOUT = 5 * 60 * 1000; // 5 minutes in milliseconds
  63. const [outTradeNo, setOutTradeNo] = useState('');
  64. const [totalFee, setTotalFee] = useState(0);
  65. const [walletBalance, setWalletBalance] = useState(0);
  66. // Effect for requesting camera permissions
  67. useEffect(() => {
  68. (async () => {
  69. const { status } = await requestPermission();
  70. if (status !== 'granted') {
  71. alert(
  72. '我們需要相機權限來掃描機器上的二維碼,以便識別並啟動充電機器。我們不會儲存或共享任何掃描到的資訊。 請前往設定開啟相機權限'
  73. );
  74. }
  75. })();
  76. }, []);
  77. useFocusEffect(
  78. useCallback(() => {
  79. // When screen comes into focus, enable scanning
  80. setScanned(false);
  81. return () => {
  82. // When screen loses focus, disable scanning
  83. setScanned(true);
  84. };
  85. }, [])
  86. );
  87. useEffect(() => {
  88. const fetchDefaultCar = async () => {
  89. try {
  90. const response = await chargeStationService.getUserDefaultCars();
  91. if (response) {
  92. // console.log('default car', response.data.id);
  93. setSelectedCar(response.data.id);
  94. }
  95. } catch (error) {
  96. } finally {
  97. setLoading(false);
  98. }
  99. };
  100. fetchDefaultCar();
  101. }, []);
  102. useEffect(() => {
  103. const getWalletBalance = async () => {
  104. try {
  105. const response = await walletService.getWalletBalance();
  106. if (response) {
  107. // console.log('walletBalance setting up', response);
  108. setWalletBalance(response);
  109. }
  110. } catch (error) {
  111. console.log(error);
  112. }
  113. };
  114. getWalletBalance();
  115. }, []);
  116. // Function to handle barcode scanning
  117. const handleBarCodeScanned = async ({ bounds, data, type }: { bounds?: any; data: any; type: any }) => {
  118. if (
  119. !bounds ||
  120. typeof bounds.origin?.x !== 'number' ||
  121. typeof bounds.origin?.y !== 'number' ||
  122. typeof bounds.size?.width !== 'number' ||
  123. typeof bounds.size?.height !== 'number'
  124. ) {
  125. setScanned(true);
  126. setScannedQrCode(data);
  127. Vibration.vibrate(100);
  128. //after scanning, immediately fetch the correct station id and push to optionPage
  129. try {
  130. const stationId = await chargeStationService.noImagefetchChargeStationIdByScannedConnectorId(data);
  131. if (!stationId) {
  132. Alert.alert('錯誤', '無法找到充電站,請稍後再嘗試');
  133. setTimeout(() => {
  134. setScanned(false);
  135. }, 2000);
  136. return;
  137. }
  138. setStationId(stationId);
  139. router.push('/optionPage');
  140. } catch (error) {
  141. console.error('Error fetching station ID:', error);
  142. Alert.alert('錯誤', '無法找到充電站,請稍後再試');
  143. setTimeout(() => {
  144. setScanned(false);
  145. }, 2000);
  146. return;
  147. }
  148. return;
  149. }
  150. // -----------------------------------------------------------------------------------------------------
  151. const { origin, size } = bounds;
  152. // Calculate the size of the square transparent area
  153. const transparentAreaSize = Math.min(screenWidth * 0.6, screenHeight * 0.3);
  154. const transparentAreaX = (screenWidth - transparentAreaSize) / 2;
  155. const transparentAreaY = (screenHeight - transparentAreaSize) / 2;
  156. // Check if the barcode is within the transparent area
  157. // 在iOS上检查二维码是否在扫描框内,在安卓上跳过位置检查
  158. const isIOS = Platform.OS === 'ios';
  159. const isWithinScanArea = isIOS ? (
  160. origin.x >= transparentAreaX &&
  161. origin.y >= transparentAreaY &&
  162. origin.x + size.width <= transparentAreaX + transparentAreaSize &&
  163. origin.y + size.height <= transparentAreaY + transparentAreaSize
  164. ) : true;
  165. if (isWithinScanArea) {
  166. setScanned(true);
  167. setScannedQrCode(data);
  168. Vibration.vibrate(100);
  169. //after scanning, immediately fetch the correct station id and push to optionPage
  170. try {
  171. const stationId = await chargeStationService.noImagefetchChargeStationIdByScannedConnectorId(data);
  172. if (!stationId) {
  173. Alert.alert('錯誤', '無法找到充電站,請稍後再嘗試');
  174. setTimeout(() => {
  175. setScanned(false);
  176. }, 2000);
  177. return;
  178. }
  179. setStationId(stationId);
  180. router.push('/optionPage');
  181. } catch (error) {
  182. console.error('Error fetching station ID:', error);
  183. Alert.alert('錯誤', '無法找到充電站,請稍後再試');
  184. setTimeout(() => {
  185. setScanned(false);
  186. }, 2000);
  187. return;
  188. }
  189. return;
  190. }
  191. };
  192. const handleDurationSelect = (duration) => {
  193. setSelectedDuration(duration);
  194. // console.log(duration);
  195. };
  196. const handleCancel = () => {
  197. setSelectedDuration(null);
  198. setModalVisible(false);
  199. if (router.canGoBack()) {
  200. router.back();
  201. } else {
  202. router.push('/mainPage');
  203. }
  204. };
  205. const handleConfirm = () => {
  206. if (selectedDuration !== null) {
  207. const now = new Date();
  208. let endTime;
  209. let fee;
  210. let totalPower;
  211. //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
  212. const planMap2 = {
  213. // 3: { duration: 10, kWh: 3, displayDuration: 5, fee: 3 * currentPriceFetchedWhenScanQr },
  214. 25: { duration: 40, kWh: 20, displayDuration: 25, fee: 20 * currentPriceFetchedWhenScanQr },
  215. 30: { duration: 45, kWh: 25, displayDuration: 30, fee: 25 * currentPriceFetchedWhenScanQr },
  216. 40: { duration: 55, kWh: 30, displayDuration: 40, fee: 30 * currentPriceFetchedWhenScanQr },
  217. 45: { duration: 60, kWh: 40, displayDuration: 45, fee: 40 * currentPriceFetchedWhenScanQr },
  218. full: { duration: 120, displayDuration: '充滿停機', fee: 80 * currentPriceFetchedWhenScanQr }
  219. };
  220. if (selectedDuration === 'full') {
  221. endTime = new Date(now.getTime() + 2 * 60 * 60 * 1000); // 2 hours for "充滿停機"
  222. fee = planMap2.full.fee;
  223. totalPower = 80; // Set to 130 for "充滿停機"
  224. } else {
  225. const durationInMinutes = parseInt(selectedDuration);
  226. endTime = new Date(now.getTime() + durationInMinutes * 60 * 1000);
  227. // console.log('endTime', endTime);
  228. fee = planMap2[selectedDuration].fee;
  229. totalPower = planMap2[selectedDuration].kWh;
  230. }
  231. setTotalFee(fee);
  232. console.log('fee in scanQrPage-- this is the total_fee i send to backend', fee);
  233. const dataForSubmission = {
  234. stationID: '2405311022116801000',
  235. connector: scannedResult,
  236. user: userID,
  237. book_time: now,
  238. end_time: endTime,
  239. total_power: totalPower,
  240. total_fee: fee,
  241. // total_fee: 1,
  242. promotion_code: '',
  243. car: selectedCar,
  244. type: 'walking',
  245. is_ic_call: false
  246. };
  247. startCharging(dataForSubmission);
  248. setIsConfirmLoading(true);
  249. }
  250. };
  251. useEffect(() => {
  252. const subscription = AppState.addEventListener('change', (nextAppState) => {
  253. if (
  254. appState.current.match(/inactive|background/) &&
  255. nextAppState === 'active' &&
  256. isExpectingPayment &&
  257. // outTradeNo &&
  258. paymentInitiatedTime.current
  259. ) {
  260. const currentTime = new Date().getTime();
  261. if (currentTime - paymentInitiatedTime.current < PAYMENT_CHECK_TIMEOUT) {
  262. checkPaymentStatus();
  263. } else {
  264. // Payment check timeout reached
  265. setIsExpectingPayment(false);
  266. setOutTradeNo('');
  267. paymentInitiatedTime.current = null;
  268. Alert.alert(
  269. 'Payment Timeout',
  270. 'The payment status check has timed out. Please check your payment history.'
  271. );
  272. }
  273. }
  274. appState.current = nextAppState;
  275. });
  276. return () => {
  277. subscription.remove();
  278. };
  279. }, [outTradeNo, isExpectingPayment]);
  280. const checkPaymentStatus = async () => {
  281. try {
  282. // console.log('outTradeNo in scanQR Page checkpaymentstatus ', outTradeNo);
  283. const result = await walletService.checkPaymentStatus(outTradeNo);
  284. setPaymentStatus(result);
  285. // console.log('checkPaymentStatus from scan QR checkpaymentStatus', result);
  286. if (result && !result.some((item) => item.errmsg?.includes('處理中'))) {
  287. // Payment successful
  288. // console.log('totalFee', totalFee);
  289. Alert.alert(
  290. '付款已成功',
  291. `你已成功增值HKD $${
  292. Number.isInteger(totalFee) ? totalFee : totalFee.toFixed(1)
  293. }。請重新掃描去啟動充電槍。`,
  294. [
  295. {
  296. text: '確認',
  297. onPress: async () => {
  298. setModalVisible(false);
  299. router.dismiss();
  300. }
  301. }
  302. ]
  303. );
  304. } else {
  305. Alert.alert('付款失敗', '請再試一次。', [
  306. {
  307. text: '確定',
  308. onPress: () => {
  309. setModalVisible(false);
  310. router.dismiss();
  311. }
  312. }
  313. ]);
  314. }
  315. setIsExpectingPayment(false);
  316. setOutTradeNo('');
  317. paymentInitiatedTime.current = null;
  318. } catch (error) {
  319. console.error('Failed to check payment status:', error);
  320. Alert.alert('Error', 'Failed to check payment status. Please check your payment history.');
  321. }
  322. };
  323. function formatTime(utcTimeString) {
  324. // Parse the UTC time string
  325. const date = new Date(utcTimeString);
  326. // Add 8 hours
  327. date.setHours(date.getHours());
  328. // Format the date
  329. const year = date.getFullYear();
  330. const month = String(date.getMonth() + 1).padStart(2, '0');
  331. const day = String(date.getDate()).padStart(2, '0');
  332. const hours = String(date.getHours()).padStart(2, '0');
  333. const minutes = String(date.getMinutes()).padStart(2, '0');
  334. const seconds = String(date.getSeconds()).padStart(2, '0');
  335. // Return the formatted string
  336. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  337. }
  338. const oneTimeCharging = async (inputAmount) => {
  339. try {
  340. const response = await walletService.getOutTradeNo();
  341. // console.log('outtradeno in oneTimeCharging', response);
  342. if (response) {
  343. setOutTradeNo(response);
  344. setIsExpectingPayment(true);
  345. paymentInitiatedTime.current = new Date().getTime();
  346. const now = new Date();
  347. const formattedTime = formatTime(now);
  348. let amount = inputAmount * 100;
  349. const origin = 'https://openapi-hk.qfapi.com/checkstand/#/?';
  350. const obj = {
  351. // appcode: '6937EF25DF6D4FA78BB2285441BC05E9',
  352. appcode: '636E234FB30D43598FC8F0140A1A7282',
  353. goods_name: 'Crazy Charge 錢包增值',
  354. out_trade_no: response,
  355. paysource: 'crazycharge_checkout',
  356. return_url: 'https://crazycharge.com.hk/completed',
  357. failed_url: 'https://crazycharge.com.hk/failed',
  358. notify_url: 'https://api.crazycharge.com.hk/api/v1/clients/qfpay/webhook',
  359. sign_type: 'sha256',
  360. txamt: amount,
  361. txcurrcd: 'HKD',
  362. txdtm: formattedTime
  363. };
  364. const paramStringify = (json, flag?) => {
  365. let str = '';
  366. let keysArr = Object.keys(json);
  367. keysArr.sort().forEach((val) => {
  368. if (!json[val]) return;
  369. str += `${val}=${flag ? encodeURIComponent(json[val]) : json[val]}&`;
  370. });
  371. return str.slice(0, -1);
  372. };
  373. // const api_key = '8F59E31F6ADF4D2894365F2BB6D2FF2C';
  374. const api_key = '3E2727FBA2DA403EA325E73F36B07824';
  375. const params = paramStringify(obj);
  376. const sign = sha256(`${params}${api_key}`).toString();
  377. const url = `${origin}${paramStringify(obj, true)}&sign=${sign}`;
  378. try {
  379. // console.log(url);
  380. const supported = await Linking.canOpenURL(url);
  381. if (supported) {
  382. await Linking.openURL(url);
  383. } else {
  384. Alert.alert('錯誤', '請稍後再試');
  385. }
  386. } catch (error) {
  387. console.error('Top-up failed:', error);
  388. Alert.alert('Error', '一次性付款失敗,請稍後再試');
  389. }
  390. } else {
  391. }
  392. } catch (error) {}
  393. };
  394. const startCharging = async (dataForSubmission) => {
  395. try {
  396. //before i start below logic, i need to check if the user has penalty unpaid.
  397. //i will call fetchReservationHistories. and the api will return an array of object, within the object there is a field called "penalty_fee".
  398. //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.
  399. const reservationHistories = await chargeStationService.fetchReservationHistories();
  400. const unpaidPenalties = reservationHistories.filter(
  401. (reservation) => reservation.penalty_fee > 0 && reservation.penalty_paid_status === false
  402. );
  403. const mostRecentUnpaidReservation = unpaidPenalties.reduce((mostRecent, current) => {
  404. return new Date(mostRecent.created_at) > new Date(current.created_at) ? mostRecent : current;
  405. }, unpaidPenalties[0]);
  406. if (unpaidPenalties.length > 0) {
  407. Alert.alert(
  408. '未付罰款',
  409. '您有未支付的罰款。請先支付罰款後再開始充電。',
  410. [
  411. {
  412. text: '查看詳情',
  413. onPress: () => {
  414. // Navigate to a page showing penalty details
  415. setModalVisible(false);
  416. setLoading(false);
  417. router.push({
  418. pathname: '(auth)/(tabs)/(home)/penaltyPaymentPage',
  419. params: {
  420. book_time: mostRecentUnpaidReservation.book_time,
  421. end_time: mostRecentUnpaidReservation.end_time,
  422. actual_end_time: mostRecentUnpaidReservation.actual_end_time,
  423. penalty_fee: mostRecentUnpaidReservation.penalty_fee,
  424. format_order_id: mostRecentUnpaidReservation.format_order_id,
  425. id: mostRecentUnpaidReservation.id,
  426. stationName:
  427. mostRecentUnpaidReservation.connector.EquipmentID.StationID.snapshot
  428. .StationName,
  429. address:
  430. mostRecentUnpaidReservation.connector.EquipmentID.StationID.snapshot.Address
  431. }
  432. });
  433. }
  434. },
  435. {
  436. text: '返回',
  437. onPress: () => {
  438. setModalVisible(false);
  439. if (router.canGoBack()) {
  440. router.back();
  441. } else {
  442. router.push('/mainPage');
  443. }
  444. }
  445. }
  446. ],
  447. { cancelable: false }
  448. );
  449. return;
  450. }
  451. const wallet = await walletService.getWalletBalance();
  452. if (wallet < dataForSubmission.total_fee) {
  453. oneTimeCharging(dataForSubmission.total_fee);
  454. // const remainingAmount = dataForSubmission.total_fee - wallet;
  455. // oneTimeCharging(remainingAmount);
  456. return;
  457. }
  458. const response = await walletService.submitPayment(
  459. dataForSubmission.stationID,
  460. dataForSubmission.connector,
  461. dataForSubmission.user,
  462. dataForSubmission.book_time,
  463. dataForSubmission.end_time,
  464. dataForSubmission.total_power,
  465. dataForSubmission.total_fee,
  466. dataForSubmission.promotion_code,
  467. dataForSubmission.car,
  468. dataForSubmission.type,
  469. dataForSubmission.is_ic_call
  470. );
  471. if (response.status === 200 || response.status === 201) {
  472. setSelectedDuration(null);
  473. setIsConfirmLoading(false);
  474. await AsyncStorage.setItem('chargingStarted', 'true');
  475. Alert.alert('啟動成功', '請按下確認並等待頁面稍後自動跳轉至充電介面', [
  476. {
  477. text: 'OK',
  478. onPress: async () => {
  479. setModalVisible(false);
  480. setLoading(true);
  481. // Wait for 2 seconds
  482. await new Promise((resolve) => setTimeout(resolve, 2000));
  483. // Hide loading spinner and navigate
  484. setLoading(false);
  485. router.navigate('(auth)/(tabs)/(home)/mainPage');
  486. router.push('(auth)/(tabs)/(charging)/chargingPage');
  487. }
  488. }
  489. ]);
  490. } else if (response.status === 400) {
  491. Alert.alert('餘額不足', '掃描失敗 請稍後再試。');
  492. } else {
  493. Alert.alert('掃描失敗 請稍後再試。', response);
  494. }
  495. } catch (error) {}
  496. };
  497. return (
  498. <View style={styles.container} ref={viewRef}>
  499. {!permission ? (
  500. <View />
  501. ) : !permission.granted ? (
  502. <View className="flex-1 justify-center items-center">
  503. <Text style={{ textAlign: 'center' }}>
  504. 我們需要相機權限來掃描機器上的二維碼,以便識別並啟動充電機器。我們不會儲存或共享任何掃描到的資訊。
  505. 請前往設定開啟相機權限
  506. </Text>
  507. </View>
  508. ) : loading ? (
  509. <View className="flex-1 items-center justify-center">
  510. <ActivityIndicator />
  511. </View>
  512. ) : (
  513. <View className="flex-1 position-relative">
  514. <CameraView
  515. style={styles.camera}
  516. facing="back"
  517. barcodeScannerSettings={{
  518. barcodeTypes: ['qr']
  519. }}
  520. onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
  521. responsiveOrientationWhenOrientationLocked={true}
  522. >
  523. </CameraView>
  524. <View style={styles.overlay}>
  525. <View style={styles.topOverlay}>
  526. <Pressable
  527. className="absolute top-20 left-10 z-10 p-4"
  528. hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }} // Added hitSlop
  529. onPress={() => {
  530. if (router.canGoBack()) {
  531. router.back();
  532. } else {
  533. router.push('/mainPage');
  534. }
  535. }}
  536. >
  537. <View style={{ transform: [{ scale: 1.5 }] }}>
  538. <CrossLogoWhiteSvg />
  539. </View>
  540. </Pressable>
  541. </View>
  542. <View style={styles.centerRow}>
  543. <View style={styles.leftOverlay}></View>
  544. <View style={styles.transparentArea}></View>
  545. <View style={styles.rightOverlay} />
  546. </View>
  547. <View className="items-center justify-between" style={styles.bottomOverlay}>
  548. <View>
  549. <Text className="text-white text-lg font-bold mt-2 text-center">
  550. 請掃瞄充電座上的二維碼
  551. </Text>
  552. </View>
  553. <View className="flex-row space-x-2 items-center ">
  554. <QuestionSvg />
  555. <Pressable onPress={() => router.push('assistancePage')}>
  556. <Text className="text-white text-base">需要協助?</Text>
  557. </Pressable>
  558. </View>
  559. <View />
  560. </View>
  561. </View>
  562. </View>
  563. )}
  564. </View>
  565. );
  566. };
  567. const styles = StyleSheet.create({
  568. container: {
  569. flex: 1
  570. },
  571. camera: {
  572. flex: 1
  573. },
  574. overlay: {
  575. flex: 1,
  576. width: '100%',
  577. height: '100%',
  578. position: 'absolute',
  579. },
  580. topOverlay: {
  581. flex: 35,
  582. alignItems: 'center',
  583. backgroundColor: 'rgba(0,0,0,0.5)'
  584. },
  585. centerRow: {
  586. flex: 30,
  587. flexDirection: 'row'
  588. },
  589. leftOverlay: {
  590. flex: 20,
  591. backgroundColor: 'rgba(0,0,0,0.5)'
  592. },
  593. transparentArea: {
  594. flex: 60,
  595. aspectRatio: 1,
  596. position: 'relative'
  597. },
  598. rightOverlay: {
  599. flex: 20,
  600. backgroundColor: 'rgba(0,0,0,0.5)'
  601. },
  602. bottomOverlay: {
  603. flex: 35,
  604. backgroundColor: 'rgba(0,0,0,0.5)'
  605. },
  606. closeButton: {
  607. position: 'absolute',
  608. top: 40,
  609. left: 20,
  610. zIndex: 1
  611. },
  612. modalContent: {
  613. backgroundColor: 'white',
  614. padding: 22,
  615. alignItems: 'center',
  616. borderRadius: 4,
  617. borderColor: 'rgba(0, 0, 0, 0.1)'
  618. },
  619. durationButton: { margin: 5 },
  620. confirmButton: {
  621. marginTop: 20,
  622. width: '100%'
  623. },
  624. cancelButton: {
  625. marginTop: 20,
  626. width: '100%',
  627. backgroundColor: 'white',
  628. borderColor: 'black',
  629. borderWidth: 1,
  630. color: 'black'
  631. }
  632. });
  633. export default ScanQrPage;