selectCoupon.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import {
  2. Image,
  3. View,
  4. Text,
  5. Pressable,
  6. Dimensions,
  7. StyleSheet,
  8. Modal,
  9. Animated,
  10. ScrollView,
  11. Button,
  12. BackHandler,
  13. Alert
  14. } from 'react-native';
  15. import { SafeAreaView } from 'react-native-safe-area-context';
  16. import { router, useFocusEffect } from 'expo-router';
  17. import { CrossLogoSvg } from '../../../../component/global/SVG';
  18. import CouponTabViewComponent from '../../../../component/global/couponTabView';
  19. import { useCallback, useEffect, useState } from 'react';
  20. import { useChargingStore } from '../../../../providers/scan_qr_payload_store';
  21. import { ArrowRightSvg } from '../../../../component/global/SVG';
  22. import { useRef } from 'react';
  23. import NormalButton from '../../../../component/global/normal_button';
  24. //this is from optionPage => 優惠券
  25. const SelectCouponComponent = () => {
  26. const screenHeight = Dimensions.get('window').height;
  27. const {
  28. promotion_code,
  29. coupon_detail,
  30. stationID,
  31. setSumOfCoupon,
  32. setCurrentPriceStore,
  33. current_price_store,
  34. setProcessedCouponStore,
  35. setPromotionCode,
  36. setCouponDetail,
  37. setTotalPower
  38. } = useChargingStore();
  39. const [scaleValue, setScaleValue] = useState(1);
  40. const [isBottomSheetVisible, setIsBottomSheetVisible] = useState(false);
  41. const [processedCoupons, setProcessedCoupons] = useState([]);
  42. const translateY = useRef(new Animated.Value(300)).current;
  43. const showBottomSheet = () => {
  44. setIsBottomSheetVisible(true);
  45. Animated.timing(translateY, {
  46. toValue: 0,
  47. duration: 300,
  48. useNativeDriver: true
  49. }).start();
  50. };
  51. const hideBottomSheet = () => {
  52. Animated.timing(translateY, {
  53. toValue: 0,
  54. duration: 0,
  55. useNativeDriver: true
  56. }).start(() => setIsBottomSheetVisible(false));
  57. };
  58. //process coupon so that coupons with same expire date and amount are grouped together to show abcoupon x 2
  59. useEffect(() => {
  60. if (Array.isArray(coupon_detail) && coupon_detail.length > 0) {
  61. const processed = processCoupons(coupon_detail);
  62. setProcessedCoupons(processed);
  63. setProcessedCouponStore(processed);
  64. }
  65. }, [coupon_detail]);
  66. //fetch original price for coupon valid calculation
  67. const processCoupons = (coupon_details_array: any) => {
  68. //coupon_details_array contains all information. i skim it down here.
  69. if (!coupon_details_array || coupon_details_array.length === 0) {
  70. return [];
  71. }
  72. const skimmedDownArray = coupon_details_array?.map((couponDetailObj: any) => ({
  73. amount: couponDetailObj.coupon.amount,
  74. id: couponDetailObj.id,
  75. expire_date: couponDetailObj.expire_date || '永久'
  76. }));
  77. const totalCouponAmount = skimmedDownArray.reduce((acc: number, coupon: any) => acc + coupon.amount, 0);
  78. setSumOfCoupon(totalCouponAmount);
  79. //process the skimmed array by combining coupons with same expire_date and amount
  80. const processedArray = (skimmedDownArray: { amount: number; id: string; expire_date: string }[]) => {
  81. const groupedCoupons: { [key: string]: any } = {};
  82. for (const coupon of skimmedDownArray) {
  83. const key = `${coupon.amount}-${coupon.expire_date}`;
  84. if (!groupedCoupons[key]) {
  85. groupedCoupons[key] = {
  86. coupon_detail: {
  87. amount: coupon.amount,
  88. expire_date: coupon.expire_date
  89. },
  90. frequency: 1
  91. };
  92. } else {
  93. groupedCoupons[key].frequency++;
  94. }
  95. }
  96. return Object.values(groupedCoupons);
  97. };
  98. return processedArray(skimmedDownArray);
  99. };
  100. const cleanupData = () => {
  101. setPromotionCode([]);
  102. setCouponDetail([]);
  103. setProcessedCouponStore([]);
  104. setSumOfCoupon(0);
  105. setTotalPower(null);
  106. };
  107. // Add this effect to handle Android back button
  108. useFocusEffect(
  109. useCallback(() => {
  110. const onBackPress = () => {
  111. cleanupData();
  112. if (router.canGoBack()) {
  113. router.back();
  114. } else {
  115. router.replace('/scanQrPage');
  116. }
  117. return true;
  118. };
  119. const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress);
  120. return () => subscription.remove()
  121. }, [])
  122. );
  123. return (
  124. <SafeAreaView className="flex-1 bg-white" edges={['top', 'right', 'left']}>
  125. <View style={{ minHeight: screenHeight, flex: 1 }}>
  126. <View className="mx-[5%]" style={{ marginTop: 25 }}>
  127. <Pressable
  128. onPress={() => {
  129. cleanupData();
  130. if (router.canGoBack()) {
  131. router.back();
  132. } else {
  133. router.replace('/optionPage');
  134. }
  135. }}
  136. >
  137. <CrossLogoSvg />
  138. </Pressable>
  139. <Text style={{ fontSize: 45, marginVertical: 25 }}>優惠券</Text>
  140. </View>
  141. <View className="flex-1">
  142. <CouponTabViewComponent titles={['可用優惠券', '已使用/失效']} />
  143. {promotion_code.length > 0 && (
  144. <View
  145. style={{
  146. position: 'absolute',
  147. alignItems: 'center',
  148. left: 0,
  149. right: 0,
  150. padding: 20,
  151. height: '100%',
  152. top: '65%'
  153. }}
  154. >
  155. <Pressable
  156. className="bg-white rounded-full items-center justify-center w-full flex flex-row"
  157. style={[styles.floatingButton, { transform: [{ scale: scaleValue }] }]}
  158. onPressIn={() => setScaleValue(0.96)}
  159. onPressOut={() => setScaleValue(1)}
  160. onPress={showBottomSheet}
  161. >
  162. <Text className="text-[#02677D] text-2xl lg:text-4xl text-center py-1 pr-4 lg:pr-6 font-[600] lg:py-4">
  163. 馬上使用
  164. </Text>
  165. <View className="flex mb-2 bg-[#02677D] rounded-full items-center justify-center w-10 h-10 relative">
  166. <ArrowRightSvg />
  167. <View className="rounded-full bg-[#02677D] h-5 w-5 absolute bottom-7 left-7 z-10 border border-white flex items-center justify-center">
  168. <Text className="text-white text-xs text-center">{promotion_code.length}</Text>
  169. </View>
  170. </View>
  171. </Pressable>
  172. </View>
  173. )}
  174. </View>
  175. </View>
  176. <Modal transparent={true} visible={isBottomSheetVisible} animationType="none">
  177. <View style={{ flex: 1 }}>
  178. <Pressable style={{ flex: 1 }} onPress={hideBottomSheet}>
  179. <View className="flex-1 bg-black/50" />
  180. </Pressable>
  181. <Animated.View
  182. style={{
  183. position: 'absolute',
  184. bottom: 0,
  185. left: 0,
  186. right: 0,
  187. height: '80%',
  188. backgroundColor: 'white',
  189. transform: [{ translateY }],
  190. borderTopLeftRadius: 30,
  191. borderTopRightRadius: 30,
  192. overflow: 'hidden'
  193. }}
  194. >
  195. <ScrollView
  196. className="flex-1 flex-col bg-white p-8 "
  197. contentContainerStyle={{ paddingBottom: 100 }}
  198. >
  199. <Text className="text-lg md:text-xl lg:text-2xl">優惠券細節</Text>
  200. <View style={{ height: 1, backgroundColor: '#ccc', marginVertical: 24 }} />
  201. {/* coupon row */}
  202. {processedCoupons &&
  203. processedCoupons?.map((couponObj: any) => (
  204. <View
  205. key={`${couponObj.coupon_detail.amount}-${couponObj.coupon_detail.expire_date}`}
  206. className="flex flex-row items-center justify-between"
  207. >
  208. <View className="flex flex-row items-start ">
  209. <Image
  210. className="w-6 lg:w-8 xl:w-10 h-6 lg:h-8 xl:h-10"
  211. source={require('../../../../assets/couponlogo.png')}
  212. />
  213. <View
  214. key={couponObj.coupon_detail.id}
  215. className="flex flex-col ml-2 lg:ml-4 "
  216. >
  217. <Text className="text-base lg:text-xl ">
  218. ${couponObj.coupon_detail.amount} 現金劵
  219. </Text>
  220. <Text className=" text-sm lg:text-base my-1 lg:mt-2 lg:mb-4 ">
  221. 有效期{' '}
  222. <Text className="font-[500] text-[#02677D]">
  223. 至 {couponObj.coupon_detail.expire_date.slice(0, 10)}
  224. </Text>
  225. </Text>
  226. </View>
  227. </View>
  228. {/* x 1 */}
  229. <View className="flex flex-row items-center ">
  230. <Text>X </Text>
  231. <View className="w-8 h-8 rounded-full bg-[#02677D] flex items-center justify-center">
  232. <Text className="text-white text-center text-lg">
  233. {couponObj.frequency}
  234. </Text>
  235. </View>
  236. </View>
  237. </View>
  238. ))}
  239. <View style={{ height: 1, backgroundColor: '#ccc', marginVertical: 12 }} />
  240. {/* 服務條款 */}
  241. <NormalButton
  242. title={<Text className="text-white text-sm lg:text-lg">立即使用</Text>}
  243. onPress={() => {
  244. setIsBottomSheetVisible(false);
  245. router.push('/totalPayment');
  246. }}
  247. extendedStyle={{ marginTop: 12, marginBottom: 24 }}
  248. />
  249. <View>
  250. <View className="">
  251. <Text className="text-base md:text-lg lg:text-xl pb-2 lg:pb-3 xl:pb-4">
  252. 服務條款與細則
  253. </Text>
  254. <View className="flex flex-col items-center space-y-2">
  255. <Text className="text-xs md:text-sm font-[300]">
  256. ・ 此券持有人可在本券有效期內於任何位於Crazy Charge
  257. 之香港分店換取同等價值充電服務,逾期無效。
  258. </Text>
  259. <Text className="text-xs lg:text-sm font-[300]">
  260. 此優惠券使用時,電費將以正常價格$3.5元/每度電計算,不適用於貓頭鷹時段或其他折扣時段的電力價格計算。
  261. </Text>
  262. <Text className="text-xs lg:text-sm font-[300]">
  263. ・ 此券不能用以套换現金或其他面值之現金券,持有人不獲現金或其他形式之找贖。
  264. </Text>
  265. <Text className="text-xs lg:text-sm font-[300]">
  266. ・ 使用者一旦在本 APP
  267. 內確認使用電子優惠券,即視為同意依優惠券規則進行消費抵扣,相關優惠券將立即從帳戶中扣除,且扣除後不得退還。
  268. </Text>
  269. <Text className="text-xs lg:text-sm font-[300]">
  270. 即便實際充電消費金額未達到電子優惠券的面額,亦不會就差額部分進行退款。優惠券的使用旨在為用戶提供充電優惠,而非現金兌換或退款工具。
  271. </Text>
  272. <Text className="text-xs lg:text-sm font-[300]">
  273. ・如有任何爭議,Crazy Charge
  274. 保留更改有關使用此現金券之條款及細則,而毋須另行通知。
  275. </Text>
  276. </View>
  277. </View>
  278. </View>
  279. </ScrollView>
  280. </Animated.View>
  281. </View>
  282. </Modal>
  283. </SafeAreaView>
  284. );
  285. };
  286. const styles = StyleSheet.create({
  287. floatingButton: {
  288. elevation: 5,
  289. shadowColor: '#000',
  290. shadowOffset: { width: 0, height: 2 },
  291. shadowOpacity: 0.25,
  292. shadowRadius: 3.84
  293. }
  294. });
  295. export default SelectCouponComponent;