selectCoupon.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. // app/(auth)/(tabs)/(home)/selectCoupon.tsx
  2. import {
  3. Image,
  4. View,
  5. Text,
  6. Pressable,
  7. Dimensions,
  8. StyleSheet,
  9. Modal,
  10. Animated,
  11. ScrollView,
  12. BackHandler
  13. } from 'react-native';
  14. import { SafeAreaView } from 'react-native-safe-area-context';
  15. import { router, useFocusEffect } from 'expo-router';
  16. import { CrossLogoSvg } from '../../../../component/global/SVG';
  17. import CouponTabViewComponent from '../../../../component/global/couponTabView';
  18. import { useCallback, useEffect, useState } from 'react';
  19. import { useChargingStore } from '../../../../providers/scan_qr_payload_store';
  20. import { ArrowRightSvg } from '../../../../component/global/SVG';
  21. import { useRef } from 'react';
  22. import NormalButton from '../../../../component/global/normal_button';
  23. import { useTranslation } from '../../../../util/hooks/useTranslation';
  24. //this is from optionPage => 優惠券
  25. const SelectCouponComponent = () => {
  26. const { t } = useTranslation();
  27. const screenHeight = Dimensions.get('window').height;
  28. const {
  29. promotion_code,
  30. coupon_detail,
  31. stationID,
  32. setSumOfCoupon,
  33. setCurrentPriceStore,
  34. current_price_store,
  35. setProcessedCouponStore,
  36. setPromotionCode,
  37. setCouponDetail,
  38. setTotalPower
  39. } = useChargingStore();
  40. const [scaleValue, setScaleValue] = useState(1);
  41. const [isBottomSheetVisible, setIsBottomSheetVisible] = useState(false);
  42. const [processedCoupons, setProcessedCoupons] = useState([]);
  43. const translateY = useRef(new Animated.Value(300)).current;
  44. const showBottomSheet = () => {
  45. setIsBottomSheetVisible(true);
  46. Animated.timing(translateY, {
  47. toValue: 0,
  48. duration: 300,
  49. useNativeDriver: true
  50. }).start();
  51. };
  52. const hideBottomSheet = () => {
  53. Animated.timing(translateY, {
  54. toValue: 0,
  55. duration: 0,
  56. useNativeDriver: true
  57. }).start(() => setIsBottomSheetVisible(false));
  58. };
  59. //process coupon so that coupons with same expire date and amount are grouped together to show abcoupon x 2
  60. useEffect(() => {
  61. if (Array.isArray(coupon_detail) && coupon_detail.length > 0) {
  62. const processed = processCoupons(coupon_detail);
  63. setProcessedCoupons(processed);
  64. setProcessedCouponStore(processed);
  65. }
  66. }, [coupon_detail]);
  67. //fetch original price for coupon valid calculation
  68. const processCoupons = (coupon_details_array: any) => {
  69. //coupon_details_array contains all information. i skim it down here.
  70. if (!coupon_details_array || coupon_details_array.length === 0) {
  71. return [];
  72. }
  73. const skimmedDownArray = coupon_details_array?.map((couponDetailObj: any) => ({
  74. amount: couponDetailObj.coupon.amount,
  75. id: couponDetailObj.id,
  76. expire_date: couponDetailObj.expire_date || t('common.permanent')
  77. }));
  78. const totalCouponAmount = skimmedDownArray.reduce((acc: number, coupon: any) => acc + coupon.amount, 0);
  79. setSumOfCoupon(totalCouponAmount);
  80. //process the skimmed array by combining coupons with same expire_date and amount
  81. const processedArray = (skimmedDownArray: { amount: number; id: string; expire_date: string }[]) => {
  82. const groupedCoupons: { [key: string]: any } = {};
  83. for (const coupon of skimmedDownArray) {
  84. const key = `${coupon.amount}-${coupon.expire_date}`;
  85. if (!groupedCoupons[key]) {
  86. groupedCoupons[key] = {
  87. coupon_detail: {
  88. amount: coupon.amount,
  89. expire_date: coupon.expire_date
  90. },
  91. frequency: 1
  92. };
  93. } else {
  94. groupedCoupons[key].frequency++;
  95. }
  96. }
  97. return Object.values(groupedCoupons);
  98. };
  99. return processedArray(skimmedDownArray);
  100. };
  101. const cleanupData = () => {
  102. setPromotionCode([]);
  103. setCouponDetail([]);
  104. setProcessedCouponStore([]);
  105. setSumOfCoupon(0);
  106. setTotalPower(null);
  107. };
  108. // Add this effect to handle Android back button
  109. useFocusEffect(
  110. useCallback(() => {
  111. const onBackPress = () => {
  112. cleanupData();
  113. if (router.canGoBack()) {
  114. router.back();
  115. } else {
  116. router.replace('/scanQrPage');
  117. }
  118. return true;
  119. };
  120. const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress);
  121. return () => subscription.remove()
  122. }, [])
  123. );
  124. return (
  125. <SafeAreaView className="flex-1 bg-white" edges={['top', 'right', 'left']}>
  126. <View style={{ minHeight: screenHeight, flex: 1 }}>
  127. <View className="mx-[5%]" style={{ marginTop: 25 }}>
  128. <Pressable
  129. onPress={() => {
  130. cleanupData();
  131. if (router.canGoBack()) {
  132. router.back();
  133. } else {
  134. router.replace('/optionPage');
  135. }
  136. }}
  137. >
  138. <CrossLogoSvg />
  139. </Pressable>
  140. <Text style={{ fontSize: 45, marginVertical: 25 }}>{t('selectCoupons.coupons.title')}</Text>
  141. </View>
  142. <View className="flex-1">
  143. <CouponTabViewComponent titles={[t('selectCoupons.coupons.available'), t('selectCoupons.coupons.used_expired')]} />
  144. {promotion_code.length > 0 && (
  145. <View
  146. style={{
  147. position: 'absolute',
  148. alignItems: 'center',
  149. left: 0,
  150. right: 0,
  151. padding: 20,
  152. height: '100%',
  153. top: '65%'
  154. }}
  155. >
  156. <Pressable
  157. className="bg-white rounded-full items-center justify-center w-full flex flex-row"
  158. style={[styles.floatingButton, { transform: [{ scale: scaleValue }] }]}
  159. onPressIn={() => setScaleValue(0.96)}
  160. onPressOut={() => setScaleValue(1)}
  161. onPress={showBottomSheet}
  162. >
  163. <Text className="text-[#02677D] text-2xl lg:text-4xl text-center py-1 pr-4 lg:pr-6 font-[600] lg:py-4">
  164. {t('common.use_now')}
  165. </Text>
  166. <View className="flex mb-2 bg-[#02677D] rounded-full items-center justify-center w-10 h-10 relative">
  167. <ArrowRightSvg />
  168. <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">
  169. <Text className="text-white text-xs text-center">{promotion_code.length}</Text>
  170. </View>
  171. </View>
  172. </Pressable>
  173. </View>
  174. )}
  175. </View>
  176. </View>
  177. <Modal transparent={true} visible={isBottomSheetVisible} animationType="none">
  178. <View style={{ flex: 1 }}>
  179. <Pressable style={{ flex: 1 }} onPress={hideBottomSheet}>
  180. <View className="flex-1 bg-black/50" />
  181. </Pressable>
  182. <Animated.View
  183. style={{
  184. position: 'absolute',
  185. bottom: 0,
  186. left: 0,
  187. right: 0,
  188. height: '80%',
  189. backgroundColor: 'white',
  190. transform: [{ translateY }],
  191. borderTopLeftRadius: 30,
  192. borderTopRightRadius: 30,
  193. overflow: 'hidden'
  194. }}
  195. >
  196. <ScrollView
  197. className="flex-1 flex-col bg-white p-8 "
  198. contentContainerStyle={{ paddingBottom: 100 }}
  199. >
  200. <Text className="text-lg md:text-xl lg:text-2xl">{t('selectCoupons.coupon_details')}</Text>
  201. <View style={{ height: 1, backgroundColor: '#ccc', marginVertical: 24 }} />
  202. {/* coupon row */}
  203. {processedCoupons &&
  204. processedCoupons?.map((couponObj: any) => (
  205. <View
  206. key={`${couponObj.coupon_detail.amount}-${couponObj.coupon_detail.expire_date}`}
  207. className="flex flex-row items-center justify-between"
  208. >
  209. <View className="flex flex-row items-start ">
  210. <Image
  211. className="w-6 lg:w-8 xl:w-10 h-6 lg:h-8 xl:h-10"
  212. source={require('../../../../assets/couponlogo.png')}
  213. />
  214. <View
  215. key={couponObj.coupon_detail.id}
  216. className="flex flex-col ml-2 lg:ml-4 "
  217. >
  218. <Text className="text-base lg:text-xl ">
  219. ${couponObj.coupon_detail.amount} {t('selectCoupons.coupons.cash_voucher')}
  220. </Text>
  221. <Text className=" text-sm lg:text-base my-1 lg:mt-2 lg:mb-4 ">
  222. {t('selectCoupons.coupons.valid_until')}{' '}
  223. <Text className="font-[500] text-[#02677D]">
  224. {couponObj.coupon_detail.expire_date === t('common.permanent')
  225. ? t('common.permanent')
  226. : t('common.to_date', { date: couponObj.coupon_detail.expire_date.slice(0, 10) })}
  227. </Text>
  228. </Text>
  229. </View>
  230. </View>
  231. {/* x 1 */}
  232. <View className="flex flex-row items-center ">
  233. <Text>X </Text>
  234. <View className="w-8 h-8 rounded-full bg-[#02677D] flex items-center justify-center">
  235. <Text className="text-white text-center text-lg">
  236. {couponObj.frequency}
  237. </Text>
  238. </View>
  239. </View>
  240. </View>
  241. ))}
  242. <View style={{ height: 1, backgroundColor: '#ccc', marginVertical: 12 }} />
  243. {/* 服務條款 */}
  244. <NormalButton
  245. title={<Text className="text-white text-sm lg:text-lg">{t('common.use_now')}</Text>}
  246. onPress={() => {
  247. setIsBottomSheetVisible(false);
  248. router.push('/totalPayment');
  249. }}
  250. extendedStyle={{ marginTop: 12, marginBottom: 24 }}
  251. />
  252. <View>
  253. <View className="">
  254. <Text className="text-base md:text-lg lg:text-xl pb-2 lg:pb-3 xl:pb-4">
  255. {t('selectCoupons.coupon.terms_and_conditions')}
  256. </Text>
  257. <View className="flex flex-col items-center space-y-2">
  258. <Text className="text-xs md:text-sm font-[300]">
  259. {t('selectCoupons.coupon.term1')}
  260. </Text>
  261. <Text className="text-xs lg:text-sm font-[300]">
  262. {t('selectCoupons.coupon.term2')}
  263. </Text>
  264. <Text className="text-xs lg:text-sm font-[300]">
  265. {t('selectCoupons.coupon.term3')}
  266. </Text>
  267. <Text className="text-xs lg:text-sm font-[300]">
  268. {t('selectCoupons.coupon.term4')}
  269. </Text>
  270. <Text className="text-xs lg:text-sm font-[300]">
  271. {t('selectCoupons.coupon.term5')}
  272. </Text>
  273. <Text className="text-xs lg:text-sm font-[300]">
  274. {t('selectCoupons.coupon.term6')}
  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;