| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542 |
- import {
- View,
- Image,
- Text,
- ScrollView,
- AppState,
- Pressable,
- ImageBackground,
- ActivityIndicator,
- Modal,
- Alert,
- TextInput,
- Linking,
- StyleSheet,
- TouchableOpacity,
- Dimensions
- } from 'react-native';
- import { SafeAreaView } from 'react-native-safe-area-context';
- import { router } from 'expo-router';
- import { CrossLogoSvg } from '../global/SVG';
- import { use, useEffect, useRef, useState } from 'react';
- import { walletService } from '../../service/walletService';
- import NormalButton from '../global/normal_button';
- import sha256 from 'crypto-js/sha256';
- import { useChargingStore } from '../../providers/scan_qr_payload_store';
- import { PaymentBonusList } from '../../service/type/walletServiceType';
- interface AmountInputModalProps {
- visible: boolean;
- onClose: () => void;
- onConfirm: (amount: number) => void;
- }
- const AmountInputModal = ({ visible, onClose, onConfirm }: AmountInputModalProps) => {
- const [amounts, setAmounts] = useState<Array<{amount: number, percentage: number}>>([]);
- useEffect(() => {
- const fetchData = async () => {
- const res: PaymentBonusList[] = await walletService.getPaymentBonusList()
- setAmounts(res.map(item => ({amount: item.base_amount, percentage: item.gift_amount})))
- }
- if (visible){
- fetchData()
- }
- }, [visible])
- const getFontSize = () => {
- const { width } = Dimensions.get('window');
- if (width < 320) return 8;
- if (width < 350) return 10; //super small phones
- if (width < 375) return 12; // Smaller phones
- if (width < 414) return 14; // Average phones
- return 16; // Larger phones
- };
- return (
- <Modal animationType="fade" transparent={true} visible={visible} onRequestClose={onClose}>
- <View
- style={{
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: 'rgba(0,0,0,0.5)'
- }}
- >
- <View
- style={{
- backgroundColor: 'white',
- padding: 20,
- borderRadius: 10,
- width: '80%'
- }}
- >
- <Text style={{ fontSize: 20, marginBottom: 20 }}>選擇增值金額</Text>
- <View
- style={{
- flexDirection: 'row',
- flexWrap: 'wrap',
- justifyContent: 'space-between',
- marginBottom: 20
- }}
- >
- {amounts.map((amount) => (
- <Pressable
- key={amount.amount}
- onPress={() => onConfirm(amount.amount)}
- style={{
- backgroundColor: '#02677D',
- padding: 10,
- borderRadius: 5,
- width: '48%',
- alignItems: 'center',
- marginBottom: 10
- }}
- >
- <Text style={{ color: 'white', fontSize: getFontSize() }}>
- ${amount.amount}
- {amount.percentage > 0 ? ` (送$${amount.percentage}) ` : ''}
- </Text>
- </Pressable>
- ))}
- </View>
- <Text>*括號為贈款金額</Text>
- <Pressable onPress={onClose} style={{ padding: 10, alignItems: 'center', marginTop: 10 }}>
- <Text style={{ color: 'red' }}>取消</Text>
- </Pressable>
- </View>
- </View>
- </Modal>
- );
- };
- export const IndividualCouponComponent = ({
- title,
- price,
- detail,
- date,
- setOpacity,
- redeem_code,
- onCouponClick = () => {}, // 添加默认空函数防止 undefined 报错
- noCircle
- }: {
- title: string;
- price: string;
- detail: string;
- onCouponClick?: (clickedCoupon: string, clickedCouponDescription: string) => void;
- date: string;
- setOpacity?: boolean;
- redeem_code?: string;
- noCircle?: boolean;
- }) => {
- const { promotion_code } = useChargingStore();
- return (
- <ImageBackground
- source={require('../../assets/empty_coupon.png')}
- resizeMode="contain"
- style={{ width: '100%', aspectRatio: 16 / 5, justifyContent: 'center' }}
- className={`mb-3 lg:mb-4
- ${setOpacity ? 'opacity-50' : ''}`}
- >
- {/* largest container */}
- <Pressable
- className="flex-row w-full h-full "
- onPress={setOpacity ? () => {} : () => onCouponClick(redeem_code as string, title)}
- >
- {/* price column on the left */}
- <View className="flex-row items-center w-[31%] justify-center">
- <Text className="pl-1 lg:pl-2 text-[#02677D] text-base md:text-lg lg:text-xl">$</Text>
- <Text className="text-3xl lg:text-4xl text-[#02677D] font-[600]">{price}</Text>
- </View>
- {/* this is a hack for good coupon display */}
- <View className="w-[7%] " />
- {/* detail column on the right */}
- <View className=" w-[62%] flex flex-col justify-evenly">
- <View className="flex flex-row justify-between items-center w-[90%]">
- <Text className="text-base lg:text-lg xl:text-xl">{title}</Text>
- {/* if opacity is true=used coupon= no circle */}
- {noCircle ? (
- <></>
- ) : (
- <View
- style={{
- width: 24,
- height: 24,
- borderRadius: 12,
- borderWidth: 2,
- borderColor: '#02677D',
- justifyContent: 'center',
- alignItems: 'center'
- }}
- className={`${promotion_code?.includes(redeem_code as string) ? 'bg-[#02677D]' : 'bg-white'}`}
- >
- <Text className="text-white">{promotion_code?.indexOf(redeem_code as string) + 1}</Text>
- </View>
- )}
- </View>
- <Text numberOfLines={2} ellipsizeMode="tail" className="text-xs w-[90%]">
- {detail}
- </Text>
- <View className="flex flex-row">
- <Text className="text-sm lg:text-base xl:text-lg">有效期至 {' '}</Text>
- <Text className="text-sm lg:text-base xl:text-lg font-bold text-[#02677D]">{date}</Text>
- </View>
- </View>
- </Pressable>
- </ImageBackground>
- );
- };
- const WalletPageComponent = () => {
- const [walletBalance, setWalletBalance] = useState<string | null>(null);
- const [loading, setLoading] = useState<boolean>(false);
- const [coupons, setCoupons] = useState([]);
- const [paymentType, setPaymentType] = useState({});
- const [userID, setUserID] = useState('');
- const [amount, setAmount] = useState<number>(0);
- const [amountModalVisible, setAmountModalVisible] = useState(false);
- const [outTradeNo, setOutTradeNo] = useState('');
- const PAYMENT_CHECK_TIMEOUT = 5 * 60 * 1000; // 5 minutes in milliseconds
- const [paymentStatus, setPaymentStatus] = useState(null);
- const [isExpectingPayment, setIsExpectingPayment] = useState(false);
- const appState = useRef(AppState.currentState);
- const paymentInitiatedTime = useRef<number>(null);
- // 优惠券注释
- useEffect(() => {
- const fetchData = async () => {
- try {
- setLoading(true);
- const info = await walletService.getCustomerInfo();
- const coupon = await walletService.getCouponForSpecificUser(info.id);
- const useableConpon = coupon.filter((couponObj: any) => {
- const today = new Date();
- if (couponObj.expire_date === null) {
- return couponObj.is_consumed === false;
- }
- const expireDate = new Date(couponObj.expire_date);
- return expireDate > today && couponObj.is_consumed === false;
- });
- setCoupons(useableConpon);
- } catch (error) {
- } finally {
- setLoading(false);
- }
- };
- fetchData();
- }, []);
- //monitor app state
- useEffect(() => {
- const subscription = AppState.addEventListener('change', (nextAppState) => {
- if (
- appState.current.match(/inactive|background/) &&
- nextAppState === 'active' &&
- isExpectingPayment &&
- // outTradeNo &&
- paymentInitiatedTime.current
- ) {
- const currentTime = new Date().getTime();
- if (currentTime - paymentInitiatedTime.current < PAYMENT_CHECK_TIMEOUT) {
- checkPaymentStatus();
- } else {
- // Payment check timeout reached
- setIsExpectingPayment(false);
- setOutTradeNo('');
- paymentInitiatedTime.current = null;
- Alert.alert(
- 'Payment Timeout',
- 'The payment status check has timed out. Please check your payment history.'
- );
- }
- }
- appState.current = nextAppState;
- });
- return () => {
- subscription.remove();
- };
- }, [outTradeNo, isExpectingPayment]);
- //check payment status
- const checkPaymentStatus = async () => {
- try {
- const result = await walletService.checkPaymentStatus(outTradeNo);
- setPaymentStatus(result);
- if (result && !result.some((item: any) => item.errmsg?.includes('處理中'))) {
- // Payment successful
- Alert.alert('Success', 'Payment was successful!', [
- {
- text: '成功',
- onPress: async () => {
- const wallet = await walletService.getWalletBalance();
- setWalletBalance(wallet);
- }
- }
- ]);
- } else {
- Alert.alert('Payment Failed', 'Payment was not successful. Please try again.');
- }
- setIsExpectingPayment(false);
- setOutTradeNo('');
- paymentInitiatedTime.current = null;
- } catch (error) {
- console.error('Failed to check payment status:', error);
- Alert.alert('Error', 'Failed to check payment status. Please check your payment history.');
- }
- };
- //fetch customer wallet balance
- useEffect(() => {
- const fetchData = async () => {
- try {
- setLoading(true);
- const info = await walletService.getCustomerInfo();
- const wallet = await walletService.getWalletBalance();
- setUserID(info.id);
- setWalletBalance(wallet);
- // setCoupons(coupon);
- } catch (error) {
- } finally {
- setLoading(false);
- }
- };
- fetchData();
- }, []);
- const formatMoney = (amount: any) => {
- if (amount === null || amount === undefined || isNaN(Number(amount))) {
- return 'LOADING';
- }
- if (typeof amount !== 'number') {
- amount = Number(amount);
- }
- // Check if the number is a whole number
- if (Number.isInteger(amount)) {
- return amount.toLocaleString('en-US');
- }
- // For decimal numbers, show one decimal place
- return Number(amount)
- .toFixed(1)
- .replace(/\B(?=(\d{3})+(?!\d))/g, ',');
- };
- const filterPaymentOptions = (options: Record<string, any>, allowedKeys: string[]) => {
- return Object.fromEntries(Object.entries(options).filter(([key]) => allowedKeys.includes(key)));
- };
- function formatTime(utcTimeString: string) {
- // Parse the UTC time string
- const date = new Date(utcTimeString);
- // Add 8 hours
- date.setHours(date.getHours());
- // Format the date
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- const hours = String(date.getHours()).padStart(2, '0');
- const minutes = String(date.getMinutes()).padStart(2, '0');
- const seconds = String(date.getSeconds()).padStart(2, '0');
- // Return the formatted string
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
- }
- useEffect(() => {
- const fetchPaymentType = async () => {
- const response = await walletService.selectPaymentType();
- // console.log('response', response);
- const filteredPaymentTypes = filterPaymentOptions(response, ['union_pay_wap_payment', 'payme_wap_payment']);
- setPaymentType(filteredPaymentTypes);
- };
- fetchPaymentType();
- }, []);
- const handleAmountConfirm = async (inputAmount: number) => {
- setAmountModalVisible(false);
- try {
- const response = await walletService.getOutTradeNo();
- if (response) {
- setOutTradeNo(response);
- setIsExpectingPayment(true);
- paymentInitiatedTime.current = new Date().getTime();
- const now = new Date();
- const formattedTime = formatTime(now.toISOString());
- let amount = inputAmount * 100;
- const origin = 'https://openapi-hk.qfapi.com/checkstand/#/?';
- const obj = {
- // appcode: '6937EF25DF6D4FA78BB2285441BC05E9',
- appcode: '636E234FB30D43598FC8F0140A1A7282',
- goods_name: 'Crazy Charge 錢包增值',
- out_trade_no: response,
- paysource: 'crazycharge_checkout',
- return_url: 'https://www.google.com',
- failed_url: 'https://www.google.com',
- notify_url: 'https://api.crazycharge.com.hk/api/v1/clients/qfpay/webhook',
- sign_type: 'sha256',
- txamt: amount,
- txcurrcd: 'HKD',
- txdtm: formattedTime
- };
- const paramStringify = (json: Record<string, any>, flag?: boolean) => {
- let str = '';
- let keysArr = Object.keys(json);
- keysArr.sort().forEach((val) => {
- if (!json[val]) return;
- str += `${val}=${flag ? encodeURIComponent(json[val]) : json[val]}&`;
- });
- return str.slice(0, -1);
- };
- // const api_key = '8F59E31F6ADF4D2894365F2BB6D2FF2C';
- const api_key = '3E2727FBA2DA403EA325E73F36B07824';
- const params = paramStringify(obj);
- const sign = sha256(`${params}${api_key}`).toString();
- const url = `${origin}${paramStringify(obj, true)}&sign=${sign}`;
- try {
- const supported = await Linking.canOpenURL(url);
- if (supported) {
- await Linking.openURL(url);
- } else {
- Alert.alert('錯誤', '請稍後再試');
- }
- } catch (error) {
- console.error('Top-up failed:', error);
- Alert.alert('Error', 'Failed to process top-up. Please try again.');
- }
- } else {
- }
- } catch (error) {}
- };
- const handleCouponClick = async (couponName: string, couponDescription: string) => {
- router.push({
- pathname: '/couponDetailPage',
- params: {
- couponName: couponName,
- couponDescription: couponDescription
- }
- });
- };
- const formattedAmount = formatMoney(walletBalance);
- return (
- <SafeAreaView className="flex-1 bg-white" edges={['top', 'right', 'left']}>
- <ScrollView className="flex-1 ">
- <View className="flex-1 mx-[5%]">
- <View style={{ marginTop: 25 }}>
- <Pressable
- onPress={() => {
- router.replace('/accountMainPage');
- }}
- >
- <CrossLogoSvg />
- </Pressable>
- <Text style={{ fontSize: 45, marginVertical: 25 }}>錢包</Text>
- </View>
- <View>
- <ImageBackground
- className="flex-col-reverse shadow-lg"
- style={{ height: 200 }}
- source={require('../../assets/walletCard1.png')}
- resizeMode="contain"
- >
- <View className="mx-[5%] pb-6">
- <Text className="text-white text-xl">餘額 (HKD)</Text>
- <View className="flex-row items-center justify-between ">
- <Text style={{ fontSize: 52 }} className=" text-white font-bold">
- {loading ? (
- <View className="items-center justify-center">
- <ActivityIndicator />
- </View>
- ) : (
- <>
- <Text>$</Text>
- {formattedAmount === 'LOADING' || amount == null ? (
- <ActivityIndicator />
- ) : (
- `${formattedAmount}`
- )}
- </>
- )}
- </Text>
- <Pressable
- className="rounded-2xl items-center justify-center p-3 px-5 pr-6 "
- style={{
- backgroundColor: 'rgba(231, 242, 248, 0.2)'
- }}
- onPress={() => {
- setAmountModalVisible(true);
- }}
- >
- <Text className="text-white font-bold">+ 增值</Text>
- </Pressable>
- </View>
- </View>
- </ImageBackground>
- </View>
- <View className="flex-row-reverse mt-2 mb-6">
- <Pressable
- onPress={() => {
- router.push({
- pathname: '/paymentRecord',
- params: { walletBalance: formatMoney(walletBalance) }
- });
- }}
- >
- <Text className="text-[#02677D] text-lg underline">訂單紀錄</Text>
- </Pressable>
- </View>
- </View>
- <View className="w-full h-1 bg-[#DBE4E8]" />
- {/* <View className="flex-row justify-between mx-[5%] pt-6 pb-3">
- <Text className="text-xl">優惠券</Text>
- <Pressable onPress={() => router.push('couponPage')}>
- <Text className="text-xl text-[#888888]">顯示所有</Text>
- </Pressable>
- </View> */}
- <View className="flex-1 flex-col mt-4 mx-[5%]">
- <NormalButton
- onPress={() => router.push('couponPage')}
- title={
- <Text className="text-white font-bold text-lg">查看所有優惠券</Text>
-
- }
- extendedStyle={{
- padding: 15
- }}
- />
- </View>
- </ScrollView>
- <AmountInputModal
- visible={amountModalVisible}
- onClose={() => setAmountModalVisible(false)}
- onConfirm={handleAmountConfirm}
- />
- </SafeAreaView>
- );
- };
- const styles = StyleSheet.create({
- button: {
- maxWidth: '100%',
- fontSize: 16,
- backgroundColor: '#025c72',
- justifyContent: 'center',
- alignItems: 'center',
- borderRadius: 12,
- padding: 20
- },
- buttonPressed: {
- backgroundColor: '#28495c'
- }
- });
- export default WalletPageComponent;
|