walletPageComponent.tsx 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. // import {
  2. // View,
  3. // Image,
  4. // Text,
  5. // ScrollView,
  6. // AppState,
  7. // Pressable,
  8. // ImageBackground,
  9. // ActivityIndicator,
  10. // Modal,
  11. // Alert,
  12. // TextInput,
  13. // Linking
  14. // } from 'react-native';
  15. // import { SafeAreaView } from 'react-native-safe-area-context';
  16. // import { router } from 'expo-router';
  17. // import { CrossLogoSvg } from '../global/SVG';
  18. // import { useEffect, useRef, useState } from 'react';
  19. // import { walletService } from '../../service/walletService';
  20. // import UnionPayImage from '../../assets/unionpay.png';
  21. // import PayMeImage from '../../assets/payme.png';
  22. // import { formatCouponDate, formatDate } from '../../util/lib';
  23. // import { set } from 'date-fns';
  24. // import { reloadAppAsync } from 'expo';
  25. // const TopUpModal = ({ visible, onClose, onSelect, paymentOptions }) => {
  26. // const getPaymentImage = (key) => {
  27. // switch (key) {
  28. // case 'union_pay_wap_payment':
  29. // return UnionPayImage;
  30. // case 'payme_wap_payment':
  31. // return PayMeImage;
  32. // default:
  33. // return null;
  34. // }
  35. // };
  36. // return (
  37. // <Modal animationType="fade" transparent={true} visible={visible} onRequestClose={onClose}>
  38. // <View
  39. // style={{
  40. // flex: 1,
  41. // justifyContent: 'center',
  42. // alignItems: 'center',
  43. // backgroundColor: 'rgba(0,0,0,0.5)'
  44. // }}
  45. // >
  46. // <View
  47. // style={{
  48. // backgroundColor: 'white',
  49. // padding: 20,
  50. // borderRadius: 10,
  51. // width: '80%',
  52. // maxHeight: '80%'
  53. // }}
  54. // >
  55. // <Text style={{ fontSize: 20, marginBottom: 20 }}>選擇支付方式</Text>
  56. // <ScrollView>
  57. // {Object.entries(paymentOptions).map(([key, value]) => (
  58. // <Pressable
  59. // key={key}
  60. // onPress={() => onSelect(value)}
  61. // style={{
  62. // padding: 10,
  63. // marginBottom: 10,
  64. // borderBottomWidth: 1,
  65. // borderBottomColor: '#eee'
  66. // }}
  67. // >
  68. // <View className="flex flex-row items-center space-x-2">
  69. // <Image
  70. // source={getPaymentImage(key)}
  71. // style={{ width: 40, height: 40, marginRight: 10 }}
  72. // resizeMode="contain"
  73. // />
  74. // <Text className="tracking-wider">
  75. // {key === 'union_pay_wap_payment' ? 'UnionPay' : 'PayMe'}
  76. // </Text>
  77. // </View>
  78. // </Pressable>
  79. // ))}
  80. // </ScrollView>
  81. // <Pressable onPress={onClose} style={{ padding: 10, alignItems: 'center', marginTop: 10 }}>
  82. // <Text style={{ color: 'red' }}>關閉</Text>
  83. // </Pressable>
  84. // </View>
  85. // </View>
  86. // </Modal>
  87. // );
  88. // };
  89. // const AmountInputModal = ({ visible, onClose, onConfirm }) => {
  90. // const [inputAmount, setInputAmount] = useState('');
  91. // return (
  92. // <Modal animationType="fade" transparent={true} visible={visible} onRequestClose={onClose}>
  93. // <View
  94. // style={{
  95. // flex: 1,
  96. // justifyContent: 'center',
  97. // alignItems: 'center',
  98. // backgroundColor: 'rgba(0,0,0,0.5)'
  99. // }}
  100. // >
  101. // <View
  102. // style={{
  103. // backgroundColor: 'white',
  104. // padding: 20,
  105. // borderRadius: 10,
  106. // width: '80%'
  107. // }}
  108. // >
  109. // <Text style={{ fontSize: 20, marginBottom: 20 }}>輸入增值金額</Text>
  110. // <TextInput
  111. // style={{
  112. // borderWidth: 1,
  113. // borderColor: '#ccc',
  114. // borderRadius: 5,
  115. // padding: 10,
  116. // marginBottom: 20,
  117. // fontSize: 18
  118. // }}
  119. // keyboardType="numeric"
  120. // placeholder="輸入金額"
  121. // value={inputAmount}
  122. // onChangeText={setInputAmount}
  123. // />
  124. // <Pressable
  125. // onPress={() => onConfirm(inputAmount)}
  126. // style={{
  127. // backgroundColor: '#02677D',
  128. // padding: 10,
  129. // borderRadius: 5,
  130. // alignItems: 'center'
  131. // }}
  132. // >
  133. // <Text style={{ color: 'white', fontSize: 18 }}>確認</Text>
  134. // </Pressable>
  135. // <Pressable onPress={onClose} style={{ padding: 10, alignItems: 'center', marginTop: 10 }}>
  136. // <Text style={{ color: 'red' }}>取消</Text>
  137. // </Pressable>
  138. // </View>
  139. // </View>
  140. // </Modal>
  141. // );
  142. // };
  143. // export const CouponComponent = ({
  144. // title,
  145. // price,
  146. // detail,
  147. // date
  148. // }: {
  149. // title: string;
  150. // price: string;
  151. // detail: string;
  152. // date: string;
  153. // }) => {
  154. // return (
  155. // <Pressable onPress={() => console.log('abc')}>
  156. // <View className="bg-[#e7f2f8] h-[124px] rounded-xl flex-row mb-3">
  157. // <View className="bg-white mx-3 my-3 w-[28%] rounded-xl">
  158. // <View className="flex-row justify-center items-center pr-4 pt-4 ">
  159. // <Text className="color-[#02677d] text-2xl pl-2 pr-1">$</Text>
  160. // <Text className="color-[#02677d] text-3xl font-bold" adjustsFontSizeToFit={true}>
  161. // {price}
  162. // </Text>
  163. // </View>
  164. // <View className="items-center justify-center">
  165. // <Text className="text-base mt-1">{title}</Text>
  166. // </View>
  167. // </View>
  168. // {/* //dash line */}
  169. // <View style={{ overflow: 'hidden' }}>
  170. // <View
  171. // style={{
  172. // borderStyle: 'dashed',
  173. // borderWidth: 1,
  174. // borderColor: '#CCCCCC',
  175. // margin: -1,
  176. // width: 0,
  177. // marginRight: 0,
  178. // height: '100%'
  179. // }}
  180. // >
  181. // <View style={{ height: 60 }}></View>
  182. // </View>
  183. // </View>
  184. // <View className="flex-col flex-1 m-[5%] justify-center ">
  185. // <Text className="text-lg">{title}</Text>
  186. // <Text className="color-[#888888] text-sm">{detail}</Text>
  187. // <View className="flex-row items-center ">
  188. // <Text className="text-base">有效期 </Text>
  189. // <Text className="text-base font-bold text-[#02677d]">{date}</Text>
  190. // </View>
  191. // </View>
  192. // </View>
  193. // </Pressable>
  194. // );
  195. // };
  196. // const WalletPageComponent = () => {
  197. // const [walletBalance, setWalletBalance] = useState<string | null>(null);
  198. // const [loading, setLoading] = useState<boolean>(false);
  199. // const [modalVisible, setModalVisible] = useState(false);
  200. // const [coupons, setCoupons] = useState([]);
  201. // const [paymentType, setPaymentType] = useState({});
  202. // const [userID, setUserID] = useState('');
  203. // const [selectedPaymentType, setSelectedPaymentType] = useState<string | null>(null);
  204. // const [amount, setAmount] = useState<number>(0);
  205. // const [amountModalVisible, setAmountModalVisible] = useState(false);
  206. // const [outTradeNo, setOutTradeNo] = useState('');
  207. // const PAYMENT_CHECK_TIMEOUT = 5 * 60 * 1000; // 5 minutes in milliseconds
  208. // const [paymentStatus, setPaymentStatus] = useState(null);
  209. // const [isExpectingPayment, setIsExpectingPayment] = useState(false);
  210. // const appState = useRef(AppState.currentState);
  211. // const paymentInitiatedTime = useRef(null);
  212. // useEffect(() => {
  213. // const subscription = AppState.addEventListener('change', (nextAppState) => {
  214. // if (
  215. // appState.current.match(/inactive|background/) &&
  216. // nextAppState === 'active' &&
  217. // isExpectingPayment &&
  218. // outTradeNo &&
  219. // paymentInitiatedTime.current
  220. // ) {
  221. // const currentTime = new Date().getTime();
  222. // if (currentTime - paymentInitiatedTime.current < PAYMENT_CHECK_TIMEOUT) {
  223. // checkPaymentStatus();
  224. // } else {
  225. // // Payment check timeout reached
  226. // setIsExpectingPayment(false);
  227. // setOutTradeNo('');
  228. // paymentInitiatedTime.current = null;
  229. // Alert.alert(
  230. // 'Payment Timeout',
  231. // 'The payment status check has timed out. Please check your payment history.'
  232. // );
  233. // }
  234. // }
  235. // appState.current = nextAppState;
  236. // });
  237. // return () => {
  238. // subscription.remove();
  239. // };
  240. // }, [outTradeNo, isExpectingPayment]);
  241. // const checkPaymentStatus = async () => {
  242. // try {
  243. // const result = await walletService.checkPaymentStatus(outTradeNo);
  244. // setPaymentStatus(result);
  245. // console.log('checkPaymentStatus from walletPageComponent', result);
  246. // if (result[0].respcd === '0000') {
  247. // console.log(result);
  248. // // Payment successful
  249. // Alert.alert('Success', 'Payment was successful!', [
  250. // {
  251. // text: 'OK',
  252. // onPress: async () => {
  253. // const wallet = await walletService.getWalletBalance();
  254. // setWalletBalance(wallet);
  255. // console.log('new wallet:', wallet);
  256. // }
  257. // }
  258. // ]);
  259. // } else {
  260. // Alert.alert('Payment Failed', 'Payment was not successful. Please try again.');
  261. // }
  262. // setIsExpectingPayment(false);
  263. // setOutTradeNo('');
  264. // paymentInitiatedTime.current = null;
  265. // } catch (error) {
  266. // console.error('Failed to check payment status:', error);
  267. // Alert.alert('Error', 'Failed to check payment status. Please check your payment history.');
  268. // }
  269. // };
  270. // // useEffect(() => {
  271. // // const handleAppStateChange = (nextAppState) => {
  272. // // if (appState.match(/inactive|background/) && nextAppState === 'active') {
  273. // // console.log('App has come to the foreground!');
  274. // // // Check payment status or update UI here
  275. // // console.log('outTradeNo', outTradeNo);
  276. // // }
  277. // // setAppState(nextAppState);
  278. // // };
  279. // // AppState.addEventListener('change', handleAppStateChange);
  280. // // }, [appState]);
  281. // useEffect(() => {
  282. // const fetchData = async () => {
  283. // try {
  284. // setLoading(true);
  285. // const info = await walletService.getCustomerInfo();
  286. // // const coupon = await walletService.getCouponForSpecificUser(info.id);
  287. // const wallet = await walletService.getWalletBalance();
  288. // console.log(wallet);
  289. // setUserID(info.id);
  290. // setWalletBalance(wallet);
  291. // setCoupons(coupon);
  292. // } catch (error) {
  293. // console.log(error);
  294. // } finally {
  295. // setLoading(false);
  296. // }
  297. // };
  298. // fetchData();
  299. // }, []);
  300. // const formatMoney = (amount: any) => {
  301. // if (typeof amount !== 'number') {
  302. // amount = Number(amount);
  303. // }
  304. // return amount.toLocaleString('en-US');
  305. // };
  306. // const filterPaymentOptions = (options, allowedKeys) => {
  307. // return Object.fromEntries(Object.entries(options).filter(([key]) => allowedKeys.includes(key)));
  308. // };
  309. // useEffect(() => {
  310. // const fetchPaymentType = async () => {
  311. // const response = await walletService.selectPaymentType();
  312. // console.log('response', response);
  313. // const filteredPaymentTypes = filterPaymentOptions(response, ['union_pay_wap_payment', 'payme_wap_payment']);
  314. // setPaymentType(filteredPaymentTypes);
  315. // };
  316. // fetchPaymentType();
  317. // }, []);
  318. // const handleTopUp = (selectedValue) => {
  319. // setSelectedPaymentType(selectedValue);
  320. // setModalVisible(false);
  321. // setAmountModalVisible(true);
  322. // };
  323. // const handleAmountConfirm = async (inputAmount) => {
  324. // setAmountModalVisible(false);
  325. // try {
  326. // const numericAmount = parseFloat(inputAmount);
  327. // if (isNaN(numericAmount) || numericAmount <= 0) {
  328. // throw new Error('Invalid amount');
  329. // }
  330. // const response = await walletService.submitPaymentAfterSelectingType(
  331. // numericAmount,
  332. // selectedPaymentType,
  333. // 'test'
  334. // );
  335. // setOutTradeNo(response.out_trade_no);
  336. // console.log('handleAmountConfirm outtradeno here,', response.out_trade_no);
  337. // setIsExpectingPayment(true);
  338. // paymentInitiatedTime.current = new Date().getTime();
  339. // const payUrl = response.pay_url;
  340. // const supported = await Linking.canOpenURL(payUrl);
  341. // if (supported) {
  342. // await Linking.openURL(payUrl);
  343. // } else {
  344. // throw new Error("Can't open payment URL");
  345. // }
  346. // } catch (error) {
  347. // console.error('Top-up failed:', error);
  348. // Alert.alert('Error', 'Failed to process top-up. Please try again.');
  349. // }
  350. // };
  351. // return (
  352. // <SafeAreaView className="flex-1 bg-white" edges={['top', 'right', 'left']}>
  353. // <ScrollView className="flex-1 ">
  354. // <View className="flex-1 mx-[5%]">
  355. // <View style={{ marginTop: 25 }}>
  356. // <Pressable
  357. // onPress={() => {
  358. // if (router.canGoBack()) {
  359. // router.back();
  360. // } else {
  361. // router.replace('/accountMainPage');
  362. // }
  363. // }}
  364. // >
  365. // <CrossLogoSvg />
  366. // </Pressable>
  367. // <Text style={{ fontSize: 45, marginVertical: 25 }}>錢包</Text>
  368. // </View>
  369. // <View>
  370. // <ImageBackground
  371. // className="flex-col-reverse shadow-lg"
  372. // style={{ height: 200 }}
  373. // source={require('../../assets/walletCard1.png')}
  374. // resizeMode="contain"
  375. // >
  376. // <View className="mx-[5%] pb-6">
  377. // <Text className="text-white text-xl">餘額 (HKD)</Text>
  378. // <View className="flex-row items-center justify-between ">
  379. // <Text style={{ fontSize: 52 }} className=" text-white font-bold">
  380. // {loading ? (
  381. // <View className="items-center justify-center">
  382. // <ActivityIndicator />
  383. // </View>
  384. // ) : (
  385. // <>
  386. // <Text>$</Text>
  387. // {formatMoney(walletBalance)}
  388. // </>
  389. // )}
  390. // </Text>
  391. // <Pressable
  392. // className="rounded-2xl items-center justify-center p-3 px-5 pr-6 "
  393. // style={{
  394. // backgroundColor: 'rgba(231, 242, 248, 0.2)'
  395. // }}
  396. // onPress={() => {
  397. // console.log('增值');
  398. // setModalVisible(true);
  399. // }}
  400. // >
  401. // <Text className="text-white font-bold">+ 增值</Text>
  402. // </Pressable>
  403. // </View>
  404. // </View>
  405. // </ImageBackground>
  406. // </View>
  407. // <View className="flex-row-reverse mt-2 mb-6">
  408. // <Pressable
  409. // onPress={() => {
  410. // router.push({
  411. // pathname: '/paymentRecord',
  412. // params: { walletBalance: formatMoney(walletBalance) }
  413. // });
  414. // }}
  415. // >
  416. // <Text className="text-[#02677D] text-lg underline">付款記錄</Text>
  417. // </Pressable>
  418. // </View>
  419. // </View>
  420. // <View className="w-full h-1 bg-[#DBE4E8]" />
  421. // <View className="flex-row justify-between mx-[5%] pt-6 pb-3">
  422. // <Text className="text-xl">優惠券</Text>
  423. // <Pressable onPress={() => router.push('couponPage')}>
  424. // <Text className="text-xl text-[#888888]">顯示所有</Text>
  425. // </Pressable>
  426. // </View>
  427. // <View className="flex-1 flex-col mx-[5%]">
  428. // {loading ? (
  429. // <View className="items-center justify-center">
  430. // <ActivityIndicator />
  431. // </View>
  432. // ) : (
  433. // <View>
  434. // {coupons
  435. // .filter(
  436. // (coupon) =>
  437. // coupon.is_consumed === false && new Date(coupon.expire_date) > new Date()
  438. // )
  439. // .slice(0, 2)
  440. // .map((coupon, index) => (
  441. // <CouponComponent
  442. // key={index}
  443. // title={coupon.name}
  444. // price={coupon.amount}
  445. // detail={coupon.description}
  446. // date={formatCouponDate(coupon.expire_date)}
  447. // />
  448. // ))}
  449. // </View>
  450. // )}
  451. // </View>
  452. // </ScrollView>
  453. // <TopUpModal
  454. // visible={modalVisible}
  455. // onClose={() => setModalVisible(false)}
  456. // onSelect={handleTopUp}
  457. // paymentOptions={paymentType}
  458. // />
  459. // <AmountInputModal
  460. // visible={amountModalVisible}
  461. // onClose={() => setAmountModalVisible(false)}
  462. // onConfirm={handleAmountConfirm}
  463. // />
  464. // </SafeAreaView>
  465. // );
  466. // };
  467. // export default WalletPageComponent;
  468. //////BELOW uses QFPay 的托管收银台页面 to 增值
  469. //////BELOW uses QFPay 的托管收银台页面 to 增值
  470. //////BELOW uses QFPay 的托管收银台页面 to 增值
  471. //////BELOW uses QFPay 的托管收银台页面 to 增值
  472. //////BELOW uses QFPay 的托管收银台页面 to 增值
  473. import {
  474. View,
  475. Image,
  476. Text,
  477. ScrollView,
  478. AppState,
  479. Pressable,
  480. ImageBackground,
  481. ActivityIndicator,
  482. Modal,
  483. Alert,
  484. TextInput,
  485. Linking,
  486. Dimensions
  487. } from 'react-native';
  488. import { SafeAreaView } from 'react-native-safe-area-context';
  489. import { router } from 'expo-router';
  490. import { CrossLogoSvg } from '../global/SVG';
  491. import { useEffect, useRef, useState } from 'react';
  492. import { walletService } from '../../service/walletService';
  493. // import UnionPayImage from '../../assets/unionpay.png';
  494. // import PayMeImage from '../../assets/payme.png';
  495. import { formatCouponDate, formatDate } from '../../util/lib';
  496. import { set } from 'date-fns';
  497. import { reloadAppAsync } from 'expo';
  498. import sha256 from 'crypto-js/sha256';
  499. import { useCallback } from 'react';
  500. import { useChargingStore } from '../../providers/scan_qr_payload_store';
  501. const AmountInputModal = ({ visible, onClose, onConfirm }) => {
  502. const amounts = [
  503. // { amount: 1, percentage: 0 },
  504. { amount: 200, percentage: 0 },
  505. { amount: 500, percentage: 5 },
  506. { amount: 1000, percentage: 10 },
  507. { amount: 2000, percentage: 15 }
  508. ];
  509. const getFontSize = () => {
  510. const { width } = Dimensions.get('window');
  511. if (width < 320) return 8;
  512. if (width < 350) return 10; //super small phones
  513. if (width < 375) return 12; // Smaller phones
  514. if (width < 414) return 14; // Average phones
  515. return 16; // Larger phones
  516. };
  517. return (
  518. <Modal animationType="fade" transparent={true} visible={visible} onRequestClose={onClose}>
  519. <View
  520. style={{
  521. flex: 1,
  522. justifyContent: 'center',
  523. alignItems: 'center',
  524. backgroundColor: 'rgba(0,0,0,0.5)'
  525. }}
  526. >
  527. <View
  528. style={{
  529. backgroundColor: 'white',
  530. padding: 20,
  531. borderRadius: 10,
  532. width: '80%'
  533. }}
  534. >
  535. <Text style={{ fontSize: 20, marginBottom: 20 }}>選擇增值金額</Text>
  536. <View
  537. style={{
  538. flexDirection: 'row',
  539. flexWrap: 'wrap',
  540. justifyContent: 'space-between',
  541. marginBottom: 20
  542. }}
  543. >
  544. {amounts.map((amount) => (
  545. <Pressable
  546. key={amount.amount}
  547. onPress={() => onConfirm(amount.amount)}
  548. style={{
  549. backgroundColor: '#02677D',
  550. padding: 10,
  551. borderRadius: 5,
  552. width: '48%',
  553. alignItems: 'center',
  554. marginBottom: 10
  555. }}
  556. >
  557. <Text style={{ color: 'white', fontSize: getFontSize() }}>
  558. ${amount.amount}
  559. {amount.percentage > 0 ? ` (+${amount.percentage}%) ` : ''}
  560. </Text>
  561. </Pressable>
  562. ))}
  563. </View>
  564. <Text>*括號為回贈比例</Text>
  565. <Pressable onPress={onClose} style={{ padding: 10, alignItems: 'center', marginTop: 10 }}>
  566. <Text style={{ color: 'red' }}>取消</Text>
  567. </Pressable>
  568. </View>
  569. </View>
  570. </Modal>
  571. );
  572. };
  573. export const IndividualCouponComponent = ({
  574. title,
  575. price,
  576. detail,
  577. date,
  578. setOpacity,
  579. redeem_code,
  580. onCouponClick,
  581. noCircle
  582. }: {
  583. title: string;
  584. price: string;
  585. detail: string;
  586. onCouponClick?: (clickedCoupon: string, clickedCouponDescription: string) => void;
  587. date: string;
  588. setOpacity?: boolean;
  589. redeem_code?: string;
  590. noCircle?: boolean;
  591. }) => {
  592. const { promotion_code } = useChargingStore();
  593. return (
  594. <ImageBackground
  595. source={require('../../assets/empty_coupon.png')}
  596. resizeMode="contain"
  597. style={{ width: '100%', aspectRatio: 16 / 5, justifyContent: 'center' }}
  598. className={`mb-3 lg:mb-4
  599. ${setOpacity ? 'opacity-50' : ''}`}
  600. >
  601. {/* largest container */}
  602. <Pressable
  603. className="flex-row w-full h-full "
  604. onPress={setOpacity ? () => {} : () => onCouponClick(redeem_code)}
  605. >
  606. {/* price column on the left */}
  607. <View className="flex-row items-center w-[31%] items-center justify-center">
  608. <Text className="pl-1 lg:pl-2 text-[#02677D] text-base md:text-lg lg:text-xl">$</Text>
  609. <Text className="text-3xl lg:text-4xl text-[#02677D] font-[600]">{price}</Text>
  610. </View>
  611. {/* this is a hack for good coupon display */}
  612. <View className="w-[7%] " />
  613. {/* detail column on the right */}
  614. <View className=" w-[62%] flex flex-col justify-evenly">
  615. <View className="flex flex-row justify-between items-center w-[90%]">
  616. <Text className="text-base lg:text-lg xl:text-xl">{title}</Text>
  617. {/* if opacity is true=used coupon= no circle */}
  618. {noCircle ? (
  619. <></>
  620. ) : (
  621. <View
  622. style={{
  623. width: 24,
  624. height: 24,
  625. borderRadius: 12,
  626. borderWidth: 2,
  627. borderColor: '#02677D',
  628. justifyContent: 'center',
  629. alignItems: 'center'
  630. }}
  631. className={`${promotion_code?.includes(redeem_code) ? 'bg-[#02677D]' : 'bg-white'}`}
  632. >
  633. <Text className="text-white">{promotion_code?.indexOf(redeem_code) + 1}</Text>
  634. </View>
  635. )}
  636. </View>
  637. <Text numberOfLines={2} ellipsizeMode="tail" className="text-xs w-[90%]">
  638. {detail}
  639. </Text>
  640. <View className="flex flex-row">
  641. <Text className="text-sm lg:text-base xl:text-lg">有效期至 {' '}</Text>
  642. <Text className="text-sm lg:text-base xl:text-lg font-bold text-[#02677D]">{date}</Text>
  643. </View>
  644. </View>
  645. </Pressable>
  646. </ImageBackground>
  647. );
  648. };
  649. const WalletPageComponent = () => {
  650. const [walletBalance, setWalletBalance] = useState<string | null>(null);
  651. const [loading, setLoading] = useState<boolean>(false);
  652. const [modalVisible, setModalVisible] = useState(false);
  653. const [coupons, setCoupons] = useState([]);
  654. const [paymentType, setPaymentType] = useState({});
  655. const [userID, setUserID] = useState('');
  656. const [selectedPaymentType, setSelectedPaymentType] = useState<string | null>(null);
  657. const [amount, setAmount] = useState<number>(0);
  658. const [amountModalVisible, setAmountModalVisible] = useState(false);
  659. const [outTradeNo, setOutTradeNo] = useState('');
  660. const PAYMENT_CHECK_TIMEOUT = 5 * 60 * 1000; // 5 minutes in milliseconds
  661. const [paymentStatus, setPaymentStatus] = useState(null);
  662. const [isExpectingPayment, setIsExpectingPayment] = useState(false);
  663. const appState = useRef(AppState.currentState);
  664. const paymentInitiatedTime = useRef(null);
  665. useEffect(() => {
  666. const fetchData = async () => {
  667. try {
  668. setLoading(true);
  669. const info = await walletService.getCustomerInfo();
  670. const coupon = await walletService.getCouponForSpecificUser(info.id);
  671. const useableConpon = coupon.filter((couponObj: any) => {
  672. const today = new Date();
  673. if (couponObj.expire_date === null) {
  674. return couponObj.is_consumed === false;
  675. }
  676. const expireDate = new Date(couponObj.expire_date);
  677. return expireDate > today && couponObj.is_consumed === false;
  678. });
  679. setCoupons(useableConpon);
  680. } catch (error) {
  681. console.log(error);
  682. } finally {
  683. setLoading(false);
  684. }
  685. };
  686. fetchData();
  687. }, []);
  688. //monitor app state
  689. useEffect(() => {
  690. const subscription = AppState.addEventListener('change', (nextAppState) => {
  691. if (
  692. appState.current.match(/inactive|background/) &&
  693. nextAppState === 'active' &&
  694. isExpectingPayment &&
  695. // outTradeNo &&
  696. paymentInitiatedTime.current
  697. ) {
  698. const currentTime = new Date().getTime();
  699. if (currentTime - paymentInitiatedTime.current < PAYMENT_CHECK_TIMEOUT) {
  700. checkPaymentStatus();
  701. } else {
  702. // Payment check timeout reached
  703. setIsExpectingPayment(false);
  704. setOutTradeNo('');
  705. paymentInitiatedTime.current = null;
  706. Alert.alert(
  707. 'Payment Timeout',
  708. 'The payment status check has timed out. Please check your payment history.'
  709. );
  710. }
  711. }
  712. appState.current = nextAppState;
  713. });
  714. return () => {
  715. subscription.remove();
  716. };
  717. }, [outTradeNo, isExpectingPayment]);
  718. //check payment status
  719. const checkPaymentStatus = async () => {
  720. try {
  721. console.log('what is the outTradeNo?? ', outTradeNo);
  722. const result = await walletService.checkPaymentStatus(outTradeNo);
  723. setPaymentStatus(result);
  724. console.log('checkPaymentStatus from walletPageComponent', result);
  725. if (result && !result.some((item) => item.errmsg?.includes('處理中'))) {
  726. // Payment successful
  727. Alert.alert('Success', 'Payment was successful!', [
  728. {
  729. text: '成功',
  730. onPress: async () => {
  731. const wallet = await walletService.getWalletBalance();
  732. setWalletBalance(wallet);
  733. console.log('new wallet:', wallet);
  734. }
  735. }
  736. ]);
  737. } else {
  738. Alert.alert('Payment Failed', 'Payment was not successful. Please try again.');
  739. }
  740. setIsExpectingPayment(false);
  741. setOutTradeNo('');
  742. paymentInitiatedTime.current = null;
  743. } catch (error) {
  744. console.error('Failed to check payment status:', error);
  745. Alert.alert('Error', 'Failed to check payment status. Please check your payment history.');
  746. }
  747. };
  748. //fetch customer wallet balance
  749. useEffect(() => {
  750. const fetchData = async () => {
  751. try {
  752. setLoading(true);
  753. const info = await walletService.getCustomerInfo();
  754. const wallet = await walletService.getWalletBalance();
  755. console.log('wallet', wallet);
  756. console.log('type of wallet', typeof wallet);
  757. setUserID(info.id);
  758. setWalletBalance(wallet);
  759. setCoupons(coupon);
  760. } catch (error) {
  761. console.log(error);
  762. } finally {
  763. setLoading(false);
  764. }
  765. };
  766. fetchData();
  767. }, []);
  768. const formatMoney = (amount: any) => {
  769. if (amount === null || amount === undefined || isNaN(Number(amount))) {
  770. return 'LOADING';
  771. }
  772. if (typeof amount !== 'number') {
  773. amount = Number(amount);
  774. }
  775. // Check if the number is a whole number
  776. if (Number.isInteger(amount)) {
  777. return amount.toLocaleString('en-US');
  778. }
  779. // For decimal numbers, show one decimal place
  780. return Number(amount)
  781. .toFixed(1)
  782. .replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  783. };
  784. const filterPaymentOptions = (options, allowedKeys) => {
  785. return Object.fromEntries(Object.entries(options).filter(([key]) => allowedKeys.includes(key)));
  786. };
  787. function formatTime(utcTimeString) {
  788. // Parse the UTC time string
  789. const date = new Date(utcTimeString);
  790. // Add 8 hours
  791. date.setHours(date.getHours());
  792. // Format the date
  793. const year = date.getFullYear();
  794. const month = String(date.getMonth() + 1).padStart(2, '0');
  795. const day = String(date.getDate()).padStart(2, '0');
  796. const hours = String(date.getHours()).padStart(2, '0');
  797. const minutes = String(date.getMinutes()).padStart(2, '0');
  798. const seconds = String(date.getSeconds()).padStart(2, '0');
  799. // Return the formatted string
  800. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  801. }
  802. useEffect(() => {
  803. const fetchPaymentType = async () => {
  804. const response = await walletService.selectPaymentType();
  805. // console.log('response', response);
  806. const filteredPaymentTypes = filterPaymentOptions(response, ['union_pay_wap_payment', 'payme_wap_payment']);
  807. setPaymentType(filteredPaymentTypes);
  808. };
  809. fetchPaymentType();
  810. }, []);
  811. const handleAmountConfirm = async (inputAmount) => {
  812. setAmountModalVisible(false);
  813. try {
  814. const response = await walletService.getOutTradeNo();
  815. console.log('do i have outtrade no??', response);
  816. if (response) {
  817. setOutTradeNo(response);
  818. setIsExpectingPayment(true);
  819. paymentInitiatedTime.current = new Date().getTime();
  820. const now = new Date();
  821. const formattedTime = formatTime(now);
  822. console.log('formattedTime in walletPageComponent', formattedTime);
  823. const out_trade_no = response;
  824. console.log('inputAmount in walletPageComponent', inputAmount);
  825. let amount = inputAmount * 100;
  826. console.log('amount in walletPageComponent', amount);
  827. const origin = 'https://openapi-hk.qfapi.com/checkstand/#/?';
  828. const obj = {
  829. // appcode: '6937EF25DF6D4FA78BB2285441BC05E9',
  830. appcode: '636E234FB30D43598FC8F0140A1A7282',
  831. goods_name: 'Crazy Charge 錢包增值',
  832. out_trade_no: response,
  833. paysource: 'crazycharge_checkout',
  834. return_url: 'https://www.google.com',
  835. failed_url: 'https://www.google.com',
  836. notify_url: 'https://api.crazycharge.com.hk/api/v1/clients/qfpay/webhook',
  837. sign_type: 'sha256',
  838. txamt: amount,
  839. txcurrcd: 'HKD',
  840. txdtm: formattedTime
  841. };
  842. const paramStringify = (json, flag?) => {
  843. let str = '';
  844. let keysArr = Object.keys(json);
  845. keysArr.sort().forEach((val) => {
  846. if (!json[val]) return;
  847. str += `${val}=${flag ? encodeURIComponent(json[val]) : json[val]}&`;
  848. });
  849. return str.slice(0, -1);
  850. };
  851. // const api_key = '8F59E31F6ADF4D2894365F2BB6D2FF2C';
  852. const api_key = '3E2727FBA2DA403EA325E73F36B07824';
  853. const params = paramStringify(obj);
  854. const sign = sha256(`${params}${api_key}`).toString();
  855. const url = `${origin}${paramStringify(obj, true)}&sign=${sign}`;
  856. try {
  857. console.log(url);
  858. const supported = await Linking.canOpenURL(url);
  859. if (supported) {
  860. await Linking.openURL(url);
  861. } else {
  862. Alert.alert('錯誤', '請稍後再試');
  863. }
  864. } catch (error) {
  865. console.error('Top-up failed:', error);
  866. Alert.alert('Error', 'Failed to process top-up. Please try again.');
  867. }
  868. } else {
  869. console.log('no');
  870. }
  871. } catch (error) {
  872. console.log(error);
  873. }
  874. };
  875. // const handleCouponClick = async (clickedCoupon: string) => {
  876. // Alert.alert(
  877. // '立即使用優惠券', // Title
  878. // '按確認打開相機,掃描充電站上的二維碼以使用優惠券', // Message
  879. // [
  880. // {
  881. // text: '取消',
  882. // style: 'cancel'
  883. // },
  884. // {
  885. // text: '確認',
  886. // onPress: () => router.push('scanQrPage')
  887. // }
  888. // ]
  889. // );
  890. // };
  891. const handleCouponClick = async (couponName: string, couponDescription: string) => {
  892. router.push({
  893. pathname: '/couponDetailPage',
  894. params: {
  895. couponName: couponName,
  896. couponDescription: couponDescription
  897. }
  898. });
  899. };
  900. const formattedAmount = formatMoney(walletBalance);
  901. return (
  902. <SafeAreaView className="flex-1 bg-white" edges={['top', 'right', 'left']}>
  903. <ScrollView className="flex-1 ">
  904. <View className="flex-1 mx-[5%]">
  905. <View style={{ marginTop: 25 }}>
  906. <Pressable
  907. onPress={() => {
  908. if (router.canGoBack()) {
  909. router.back();
  910. } else {
  911. router.replace('/accountMainPage');
  912. }
  913. }}
  914. >
  915. <CrossLogoSvg />
  916. </Pressable>
  917. <Text style={{ fontSize: 45, marginVertical: 25 }}>錢包</Text>
  918. </View>
  919. <View>
  920. <ImageBackground
  921. className="flex-col-reverse shadow-lg"
  922. style={{ height: 200 }}
  923. source={require('../../assets/walletCard1.png')}
  924. resizeMode="contain"
  925. >
  926. <View className="mx-[5%] pb-6">
  927. <Text className="text-white text-xl">餘額 (HKD)</Text>
  928. <View className="flex-row items-center justify-between ">
  929. <Text style={{ fontSize: 52 }} className=" text-white font-bold">
  930. {loading ? (
  931. <View className="items-center justify-center">
  932. <ActivityIndicator />
  933. </View>
  934. ) : (
  935. <>
  936. <Text>$</Text>
  937. {formattedAmount === 'LOADING' || amount == null ? (
  938. <ActivityIndicator />
  939. ) : (
  940. `${formattedAmount}`
  941. )}
  942. </>
  943. )}
  944. </Text>
  945. <Pressable
  946. className="rounded-2xl items-center justify-center p-3 px-5 pr-6 "
  947. style={{
  948. backgroundColor: 'rgba(231, 242, 248, 0.2)'
  949. }}
  950. onPress={() => {
  951. setAmountModalVisible(true);
  952. }}
  953. >
  954. <Text className="text-white font-bold">+ 增值</Text>
  955. </Pressable>
  956. </View>
  957. </View>
  958. </ImageBackground>
  959. </View>
  960. <View className="flex-row-reverse mt-2 mb-6">
  961. <Pressable
  962. onPress={() => {
  963. router.push({
  964. pathname: '/paymentRecord',
  965. params: { walletBalance: formatMoney(walletBalance) }
  966. });
  967. }}
  968. >
  969. <Text className="text-[#02677D] text-lg underline">訂單紀錄</Text>
  970. </Pressable>
  971. </View>
  972. </View>
  973. <View className="w-full h-1 bg-[#DBE4E8]" />
  974. <View className="flex-row justify-between mx-[5%] pt-6 pb-3">
  975. <Text className="text-xl">優惠券</Text>
  976. <Pressable onPress={() => router.push('couponPage')}>
  977. <Text className="text-xl text-[#888888]">顯示所有</Text>
  978. </Pressable>
  979. </View>
  980. <View className="flex-1 flex-col mx-[5%]">
  981. {loading ? (
  982. <View className="items-center justify-center">
  983. <ActivityIndicator />
  984. </View>
  985. ) : (
  986. <View>
  987. {coupons
  988. .filter(
  989. (coupon) =>
  990. coupon.is_consumed === false && new Date(coupon.expire_date) > new Date()
  991. )
  992. .slice(0, 2)
  993. .map((coupon, index) => (
  994. <IndividualCouponComponent
  995. key={index}
  996. title={coupon.coupon.name}
  997. price={coupon.coupon.amount}
  998. detail={coupon.coupon.description}
  999. date={formatCouponDate(coupon.expire_date)}
  1000. noCircle={true}
  1001. onCouponClick={() => handleCouponClick(coupon.coupon.name, coupon.coupon.description)}
  1002. />
  1003. ))}
  1004. </View>
  1005. )}
  1006. </View>
  1007. </ScrollView>
  1008. {/* <TopUpModal
  1009. visible={modalVisible}
  1010. onClose={() => setModalVisible(false)}
  1011. onSelect={handleTopUp}
  1012. paymentOptions={paymentType}
  1013. /> */}
  1014. <AmountInputModal
  1015. visible={amountModalVisible}
  1016. onClose={() => setAmountModalVisible(false)}
  1017. onConfirm={handleAmountConfirm}
  1018. />
  1019. </SafeAreaView>
  1020. );
  1021. };
  1022. export default WalletPageComponent;