scanQrPage.tsx 28 KB

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