You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
734 lines
22 KiB
734 lines
22 KiB
import React from "react"; |
|
import { |
|
View, |
|
Text, |
|
TouchableOpacity, |
|
StyleSheet, |
|
TextInput, |
|
ScrollView, |
|
Alert, |
|
ActivityIndicator, |
|
Platform, |
|
StatusBar, |
|
SafeAreaView, |
|
BackHandler, |
|
Modal, |
|
FlatList, |
|
} from "react-native"; |
|
import useCreateOrderStore from "../../store/createOrder"; |
|
import BackIcon from "../../components/BackIcon"; |
|
import { |
|
useNavigation, |
|
useRoute, |
|
RouteProp, |
|
useFocusEffect, |
|
CommonActions, |
|
} from "@react-navigation/native"; |
|
import { NativeStackNavigationProp } from "@react-navigation/native-stack"; |
|
import { useState, useEffect } from "react"; |
|
import useUserStore from "../../store/user"; |
|
import { Order } from "../../services/api/orders"; |
|
import { payApi } from "../../services/api/payApi"; |
|
import { settingApi } from "../../services/api/setting"; |
|
import { CountryList } from "../../constants/countries"; |
|
import { useTranslation } from "react-i18next"; |
|
import fontSize from "../../utils/fontsizeUtils"; |
|
import { getBurialPointData } from "../../store/burialPoint"; |
|
import useBurialPointStore from "../../store/burialPoint"; |
|
import AsyncStorage from "@react-native-async-storage/async-storage"; |
|
|
|
// 定义本地存储的国家数据类型 |
|
interface LocalCountryData { |
|
code: string; |
|
flag: string; |
|
name: string; |
|
phoneCode: string; |
|
userCount: number; |
|
} |
|
|
|
// Define the param list for navigation |
|
type RootStackParamList = { |
|
PreviewOrder: { |
|
data: Order; |
|
payMethod: string; |
|
currency: string; |
|
amount: number; |
|
}; |
|
Pay: { payUrl: string; method: string; order_id: string }; |
|
OrderDetails: { orderId?: number }; |
|
PaymentSuccessScreen: any; |
|
MainTabs: undefined; |
|
}; |
|
|
|
export const PreviewOrder = () => { |
|
const { orderData, setOrderData } = useCreateOrderStore(); |
|
const navigation = |
|
useNavigation<NativeStackNavigationProp<RootStackParamList>>(); |
|
const [phoneNumber, setPhoneNumber] = useState(""); |
|
const [showPhoneInput, setShowPhoneInput] = useState(false); |
|
const [countryList, setCountryList] = useState<CountryList[]>([]); |
|
const [selectedCountry, setSelectedCountry] = useState<CountryList | null>(null); |
|
const [localSelectedCountry, setLocalSelectedCountry] = useState<LocalCountryData | null>(null); |
|
const [showCountryModal, setShowCountryModal] = useState(false); |
|
const [loadingCountries, setLoadingCountries] = useState(false); |
|
const route = useRoute<RouteProp<RootStackParamList, "PreviewOrder">>(); |
|
const [loading, setLoading] = useState(false); |
|
const { user } = useUserStore(); |
|
const { t } = useTranslation(); |
|
const { logPreviewOrder } = useBurialPointStore(); |
|
|
|
useEffect(() => { |
|
if (!user.user_id) { |
|
return Alert.alert(t("order.preview.login_required")); |
|
} |
|
if (route.params.payMethod === "mobile_money") { |
|
setShowPhoneInput(true); |
|
// 获取国家列表 |
|
loadCountryList(); |
|
} else { |
|
setShowPhoneInput(false); |
|
} |
|
}, [route.params.payMethod, user.user_id, t]); |
|
|
|
// 获取国家列表 |
|
const loadCountryList = async () => { |
|
setLoadingCountries(true); |
|
try { |
|
// 首先尝试读取本地存储的国家数据 |
|
const savedLocalCountry = await AsyncStorage.getItem("@selected_country"); |
|
if (savedLocalCountry) { |
|
try { |
|
const parsedLocalCountry: LocalCountryData = JSON.parse(savedLocalCountry); |
|
setLocalSelectedCountry(parsedLocalCountry); |
|
console.log('使用本地存储的国家:', parsedLocalCountry); |
|
} catch (e) { |
|
console.error("解析本地存储国家数据失败:", e); |
|
} |
|
} |
|
|
|
const response = await settingApi.getSendSmsCountryList(); |
|
if (response && Array.isArray(response)) { |
|
setCountryList(response); |
|
|
|
// 如果没有本地存储的国家,则使用API返回的数据进行匹配 |
|
if (!savedLocalCountry) { |
|
// 如果用户有国家信息,自动选择对应的国家 |
|
if (user?.country_en) { |
|
const userCountry = response.find((country: CountryList) => |
|
country.name_en.toLowerCase() === user.country_en.toLowerCase() |
|
); |
|
if (userCountry) { |
|
setSelectedCountry(userCountry); |
|
} |
|
} |
|
} |
|
} |
|
} catch (error) { |
|
console.error('获取国家列表失败:', error); |
|
} finally { |
|
setLoadingCountries(false); |
|
} |
|
}; |
|
|
|
// 格式化电话号码 |
|
const formatPhoneNumber = (phone: string, localCountry: LocalCountryData | null, apiCountry: CountryList | null): string => { |
|
if (!phone) return phone; |
|
|
|
// 移除电话号码中的空格、破折号等 |
|
const cleanPhone = phone.replace(/[\s\-\(\)]/g, ''); |
|
|
|
// 如果已经有+号开头,直接返回 |
|
if (cleanPhone.startsWith('+')) { |
|
return cleanPhone; |
|
} |
|
|
|
// 优先使用本地存储的国家数据的 phoneCode |
|
let countryCode = ''; |
|
if (localCountry?.phoneCode) { |
|
countryCode = localCountry.phoneCode; |
|
} else if (apiCountry?.country) { |
|
countryCode = `+${apiCountry.country}`; |
|
} else { |
|
return phone; // 如果都没有,返回原始电话号码 |
|
} |
|
|
|
// 如果电话号码以0开头,移除0 |
|
const phoneWithoutLeadingZero = cleanPhone.startsWith('0') ? cleanPhone.substring(1) : cleanPhone; |
|
|
|
return `${countryCode}${phoneWithoutLeadingZero}`; |
|
}; |
|
|
|
// 处理系统返回键 |
|
useFocusEffect( |
|
React.useCallback(() => { |
|
const onBackPress = () => { |
|
// 拦截系统返回键事件,导航到OrderDetails页面 |
|
navigation.navigate("OrderDetails", { orderId: 1 }); |
|
return true; // 返回true表示已处理返回事件 |
|
}; |
|
|
|
// 添加返回键监听(Android) |
|
BackHandler.addEventListener("hardwareBackPress", onBackPress); |
|
|
|
return () => |
|
BackHandler.removeEventListener("hardwareBackPress", onBackPress); |
|
}, [navigation]) |
|
); |
|
|
|
const handleSubmit = () => { |
|
if (showPhoneInput && !phoneNumber) { |
|
Alert.alert(t("error"), t("order.preview.phone_required") || "Phone number is required for Mobile Money"); |
|
return; |
|
} |
|
|
|
if (showPhoneInput && !localSelectedCountry && !selectedCountry) { |
|
Alert.alert(t("error"), "请选择国家"); |
|
return; |
|
} |
|
|
|
console.log(route.params.currency); |
|
|
|
// 格式化电话号码,添加国家前缀 |
|
const formattedPhone = showPhoneInput && phoneNumber |
|
? formatPhoneNumber(phoneNumber, localSelectedCountry, selectedCountry) |
|
: ''; |
|
|
|
const data = { |
|
order_id: route.params.data.order_id, |
|
method: route.params.payMethod, |
|
currency: route.params.currency |
|
? route.params.currency |
|
: route.params.data.currency, |
|
amount: route.params.data.actual_amount, |
|
...(showPhoneInput && formattedPhone && { |
|
extra: { phone_number: formattedPhone } |
|
}), |
|
}; |
|
|
|
console.log('发送的电话号码:', formattedPhone); |
|
setLoading(true); |
|
|
|
payApi |
|
.getPayInfo(data) |
|
.then((res) => { |
|
if (res.success) { |
|
// 记录埋点数据 |
|
logPreviewOrder( |
|
navigation.getState().routes[navigation.getState().index - 1] |
|
?.name as string |
|
); |
|
console.log("埋点数据: " +getBurialPointData()); |
|
|
|
// 所有成功的支付都跳转到 ProfileScreen |
|
navigation.dispatch( |
|
CommonActions.reset({ |
|
index: 0, |
|
routes: [ |
|
{ |
|
name: 'MainTabs', |
|
state: { |
|
routes: [ |
|
{ name: 'Home' }, |
|
{ name: 'productCollection' }, |
|
{ name: 'Chat' }, |
|
{ name: 'Cart' }, |
|
{ name: 'Profile' }, |
|
], |
|
index: 4, // Profile tab 的索引 |
|
}, |
|
}, |
|
], |
|
}) |
|
); |
|
} else { |
|
// 处理失败情况 |
|
if (route.params.payMethod === "balance") { |
|
Alert.alert(t("order.preview.Insufficient_balance")); |
|
} else { |
|
Alert.alert(t("order.preview.payment_failed")); |
|
} |
|
} |
|
}) |
|
.catch((err) => { |
|
setLoading(false); |
|
Alert.alert(t("order.preview.payment_failed")); |
|
}) |
|
.finally(() => { |
|
setLoading(false); |
|
}); |
|
}; |
|
|
|
// 获取显示的国家代码 |
|
const getDisplayCountryCode = () => { |
|
if (loadingCountries) return "..."; |
|
if (localSelectedCountry?.phoneCode) { |
|
return localSelectedCountry.phoneCode; |
|
} |
|
if (selectedCountry?.country) { |
|
return `+${selectedCountry.country}`; |
|
} |
|
return '+86'; // 默认值 |
|
}; |
|
|
|
return ( |
|
<SafeAreaView style={[styles.safeArea]}> |
|
<StatusBar barStyle="dark-content" backgroundColor="#fff" /> |
|
<View style={styles.safeAreaContent}> |
|
<View style={styles.container}> |
|
<View style={styles.titleContainer}> |
|
<View style={styles.backIconContainer}> |
|
<TouchableOpacity onPress={() => navigation.goBack()}> |
|
<BackIcon size={fontSize(20)} /> |
|
</TouchableOpacity> |
|
</View> |
|
|
|
<Text style={styles.titleHeading}> |
|
{t("order.preview.pay_now")} |
|
</Text> |
|
</View> |
|
|
|
<ScrollView style={styles.scrollContainer}> |
|
<View style={styles.mainContent}> |
|
{/* Payment Method Section */} |
|
<View style={styles.section}> |
|
<Text style={styles.sectionTitle}> |
|
{t("order.preview.payment_method")} |
|
</Text> |
|
<View style={styles.paymentMethodContainer}> |
|
<Text style={styles.paymentMethodText}> |
|
{route.params.payMethod} |
|
</Text> |
|
</View> |
|
|
|
{showPhoneInput && ( |
|
<View style={styles.phoneInputContainer}> |
|
<Text style={styles.phoneInputLabel}> |
|
{t("order.preview.enter_phone")} |
|
</Text> |
|
|
|
{/* 电话号码输入行 - 国家选择器在左侧 */} |
|
<View style={styles.phoneInputRow}> |
|
{/* 国家代码选择器 */} |
|
<TouchableOpacity |
|
style={styles.countryCodeSelector} |
|
onPress={() => setShowCountryModal(true)} |
|
disabled={loadingCountries} |
|
> |
|
<Text style={styles.countryCodeText}> |
|
{getDisplayCountryCode()} |
|
</Text> |
|
<Text style={styles.countryCodeArrow}>▼</Text> |
|
</TouchableOpacity> |
|
|
|
{/* 分隔线 */} |
|
<View style={styles.phoneSeparator} /> |
|
|
|
{/* 电话号码输入框 */} |
|
<TextInput |
|
style={styles.phoneInput} |
|
value={phoneNumber} |
|
onChangeText={setPhoneNumber} |
|
placeholder={t("order.preview.phone_placeholder")} |
|
keyboardType="phone-pad" |
|
/> |
|
</View> |
|
</View> |
|
)} |
|
</View> |
|
|
|
{/* Order Info Section */} |
|
<View style={styles.section}> |
|
<Text style={styles.sectionTitle}> |
|
{t("order.preview.payment_info")} |
|
</Text> |
|
|
|
<View style={styles.infoRow}> |
|
<Text style={styles.infoLabel}> |
|
{t("order.preview.name")} |
|
</Text> |
|
<Text style={styles.infoValue}> |
|
{route.params?.data.receiver_name || "N/A"} |
|
</Text> |
|
</View> |
|
|
|
<View style={styles.infoRow}> |
|
<Text style={styles.infoLabel}> |
|
{t("order.preview.phone")} |
|
</Text> |
|
<Text style={styles.infoValue}> |
|
{route.params?.data.receiver_phone || "N/A"} |
|
</Text> |
|
</View> |
|
|
|
{route.params.data?.whatsapp_number && ( |
|
<View style={styles.infoRow}> |
|
<Text style={styles.infoLabel}>WhatsApp</Text> |
|
<Text style={styles.infoValue}> |
|
{route.params.data?.whatsapp_number} |
|
</Text> |
|
</View> |
|
)} |
|
|
|
<View style={styles.infoRow}> |
|
<Text style={styles.infoLabel}> |
|
{t("order.preview.country")} |
|
</Text> |
|
<Text style={styles.infoValue}> |
|
{route.params?.data.receiver_country || "N/A"} |
|
</Text> |
|
</View> |
|
|
|
<View style={styles.infoRow}> |
|
<Text style={styles.infoLabel}> |
|
{t("order.preview.shipping_address")} |
|
</Text> |
|
<Text style={styles.infoValue}> |
|
{route.params?.data.receiver_address || "N/A"} |
|
</Text> |
|
</View> |
|
|
|
{/* <View style={styles.infoRow}> |
|
<Text style={styles.infoLabel}>{t("order.preview.order_amount")}</Text> |
|
<Text style={styles.infoValue}> |
|
{route.params.amount || "N/A"} {route.params.data.currency} |
|
</Text> |
|
</View> |
|
|
|
<View style={styles.infoRow}> |
|
<Text style={styles.infoLabel}>{t("order.preview.shipping_fee")}</Text> |
|
<Text style={styles.infoValue}> |
|
{( |
|
route.params.data.domestic_shipping_fee + |
|
route.params.data.shipping_fee |
|
).toFixed(2) || "N/A"}{" "} |
|
{route.params.data.currency} |
|
</Text> |
|
</View> */} |
|
</View> |
|
|
|
{/* Order Summary Section */} |
|
<View style={styles.section}> |
|
<Text style={styles.sectionTitle}> |
|
{t("order.preview.order_total")} |
|
</Text> |
|
<View style={styles.totalRow}> |
|
<Text style={styles.totalLabel}> |
|
{t("order.preview.total_amount")} |
|
</Text> |
|
<Text style={styles.totalValue}> |
|
{route.params.data.actual_amount}{" "} |
|
{route.params.data.currency} |
|
</Text> |
|
</View> |
|
</View> |
|
</View> |
|
</ScrollView> |
|
|
|
<View style={styles.submitButtonContainer}> |
|
<TouchableOpacity |
|
style={[ |
|
styles.primaryButtonStyle, |
|
(!showPhoneInput || (showPhoneInput && phoneNumber && (localSelectedCountry || selectedCountry))) && !loading |
|
? {} |
|
: styles.disabledButtonStyle, |
|
]} |
|
onPress={handleSubmit} |
|
disabled={(showPhoneInput && (!phoneNumber || (!localSelectedCountry && !selectedCountry))) || loading} |
|
> |
|
{loading ? ( |
|
<ActivityIndicator size="small" color="#ffffff" /> |
|
) : ( |
|
<Text style={styles.buttonText}> |
|
{t("order.preview.confirm_payment")} |
|
</Text> |
|
)} |
|
</TouchableOpacity> |
|
</View> |
|
</View> |
|
</View> |
|
|
|
{/* 国家选择模态框 */} |
|
<Modal |
|
visible={showCountryModal} |
|
animationType="slide" |
|
transparent={true} |
|
onRequestClose={() => setShowCountryModal(false)} |
|
> |
|
<View style={styles.modalOverlay}> |
|
<View style={styles.modalContent}> |
|
<View style={styles.modalHeader}> |
|
<Text style={styles.modalTitle}>选择国家</Text> |
|
<TouchableOpacity |
|
onPress={() => setShowCountryModal(false)} |
|
style={styles.modalCloseButton} |
|
> |
|
<Text style={styles.modalCloseText}>✕</Text> |
|
</TouchableOpacity> |
|
</View> |
|
|
|
<FlatList |
|
data={countryList} |
|
keyExtractor={(item) => item.country.toString()} |
|
renderItem={({ item }) => ( |
|
<TouchableOpacity |
|
style={[ |
|
styles.countryItem, |
|
selectedCountry?.country === item.country && styles.selectedCountryItem |
|
]} |
|
onPress={() => { |
|
setSelectedCountry(item); |
|
setLocalSelectedCountry(null); // 清除本地存储的选择,使用API的数据 |
|
setShowCountryModal(false); |
|
}} |
|
> |
|
<Text style={[ |
|
styles.countryItemText, |
|
selectedCountry?.country === item.country && styles.selectedCountryItemText |
|
]}> |
|
{item.name_en} (+{item.country}) |
|
</Text> |
|
</TouchableOpacity> |
|
)} |
|
style={styles.countryList} |
|
/> |
|
</View> |
|
</View> |
|
</Modal> |
|
</SafeAreaView> |
|
); |
|
}; |
|
|
|
const styles = StyleSheet.create({ |
|
safeArea: { |
|
flex: 1, |
|
backgroundColor: "#fff", |
|
}, |
|
safeAreaContent: { |
|
flex: 1, |
|
paddingTop: Platform.OS === "android" ? 0 : 0, |
|
}, |
|
container: { |
|
flex: 1, |
|
backgroundColor: "#fff", |
|
}, |
|
scrollContainer: { |
|
flex: 1, |
|
}, |
|
mainContent: { |
|
padding: 15, |
|
}, |
|
submitButtonContainer: { |
|
paddingHorizontal: 16, |
|
paddingVertical: 20, |
|
backgroundColor: "white", |
|
borderTopWidth: 1, |
|
borderTopColor: "#eaeaea", |
|
}, |
|
primaryButtonStyle: { |
|
width: "100%", |
|
height: 50, |
|
justifyContent: "center", |
|
alignItems: "center", |
|
backgroundColor: "#002fa7", |
|
borderRadius: 25, |
|
shadowColor: "#002fa7", |
|
shadowOffset: { width: 0, height: 3 }, |
|
shadowOpacity: 0.2, |
|
shadowRadius: 5, |
|
elevation: 5, |
|
}, |
|
buttonText: { |
|
color: "white", |
|
fontWeight: "600", |
|
fontSize: fontSize(16), |
|
fontFamily: "PingFang SC", |
|
}, |
|
disabledButtonStyle: { |
|
backgroundColor: "#c0c0c0", |
|
shadowOpacity: 0, |
|
}, |
|
titleContainer: { |
|
width: "100%", |
|
padding: 15, |
|
flexDirection: "row", |
|
alignItems: "center", |
|
justifyContent: "center", |
|
position: "relative", |
|
backgroundColor: "#fff", |
|
borderBottomWidth: 1, |
|
borderBottomColor: "#eaeaea", |
|
}, |
|
backIconContainer: { |
|
position: "absolute", |
|
left: 15, |
|
backgroundColor: "#fff", |
|
}, |
|
titleHeading: { |
|
fontWeight: "600", |
|
fontSize: fontSize(16), |
|
lineHeight: 22, |
|
fontFamily: "PingFang SC", |
|
color: "black", |
|
}, |
|
section: { |
|
marginBottom: 20, |
|
backgroundColor: "#fff", |
|
borderRadius: 8, |
|
padding: 15, |
|
shadowColor: "#000", |
|
shadowOffset: { |
|
width: 0, |
|
height: 1, |
|
}, |
|
shadowOpacity: 0.22, |
|
shadowRadius: 2.22, |
|
elevation: 3, |
|
}, |
|
sectionTitle: { |
|
fontSize: fontSize(18), |
|
fontWeight: "600", |
|
marginBottom: 15, |
|
color: "#000", |
|
}, |
|
paymentMethodContainer: { |
|
marginTop: 5, |
|
}, |
|
paymentMethodText: { |
|
fontSize: fontSize(17), |
|
color: "#FF6000", |
|
fontWeight: "500", |
|
}, |
|
phoneInputContainer: { |
|
marginTop: 15, |
|
}, |
|
phoneInputLabel: { |
|
fontSize: fontSize(15), |
|
marginBottom: 5, |
|
color: "#333", |
|
}, |
|
phoneInput: { |
|
flex: 1, |
|
paddingHorizontal: 12, |
|
paddingVertical: 15, |
|
fontSize: fontSize(15), |
|
color: "#333", |
|
}, |
|
infoRow: { |
|
flexDirection: "row", |
|
justifyContent: "space-between", |
|
marginBottom: 10, |
|
paddingBottom: 10, |
|
borderBottomWidth: 1, |
|
borderBottomColor: "#f0f0f0", |
|
}, |
|
infoLabel: { |
|
fontSize: fontSize(15), |
|
color: "#666", |
|
flex: 1, |
|
}, |
|
infoValue: { |
|
fontSize: fontSize(15), |
|
color: "#333", |
|
flex: 2, |
|
textAlign: "right", |
|
}, |
|
totalRow: { |
|
flexDirection: "row", |
|
justifyContent: "space-between", |
|
alignItems: "center", |
|
paddingVertical: 12, |
|
}, |
|
totalLabel: { |
|
fontSize: fontSize(16), |
|
fontWeight: "600", |
|
color: "#333", |
|
fontFamily: "PingFang SC", |
|
}, |
|
totalValue: { |
|
fontSize: fontSize(20), |
|
fontWeight: "700", |
|
color: "#002fa7", |
|
fontFamily: "PingFang SC", |
|
}, |
|
phoneInputRow: { |
|
flexDirection: "row", |
|
alignItems: "center", |
|
borderWidth: 1, |
|
borderColor: "#ddd", |
|
borderRadius: 5, |
|
overflow: "hidden", |
|
}, |
|
countryCodeSelector: { |
|
paddingHorizontal: 12, |
|
paddingVertical: 15, |
|
flexDirection: "row", |
|
alignItems: "center", |
|
justifyContent: "space-between", |
|
minWidth: 80, |
|
backgroundColor: "#f8f8f8", |
|
}, |
|
countryCodeText: { |
|
fontSize: fontSize(15), |
|
color: "#333", |
|
fontWeight: "600", |
|
marginRight: 5, |
|
}, |
|
countryCodeArrow: { |
|
fontSize: fontSize(10), |
|
color: "#666", |
|
}, |
|
phoneSeparator: { |
|
width: 1, |
|
height: 30, |
|
backgroundColor: "#ddd", |
|
}, |
|
modalOverlay: { |
|
flex: 1, |
|
backgroundColor: "rgba(0, 0, 0, 0.5)", |
|
justifyContent: "center", |
|
alignItems: "center", |
|
}, |
|
modalContent: { |
|
backgroundColor: "white", |
|
padding: 20, |
|
borderRadius: 20, |
|
width: "80%", |
|
maxHeight: "80%", |
|
}, |
|
modalHeader: { |
|
flexDirection: "row", |
|
alignItems: "center", |
|
justifyContent: "space-between", |
|
marginBottom: 10, |
|
}, |
|
modalTitle: { |
|
fontSize: fontSize(18), |
|
fontWeight: "600", |
|
}, |
|
modalCloseButton: { |
|
padding: 5, |
|
}, |
|
modalCloseText: { |
|
fontSize: fontSize(16), |
|
fontWeight: "600", |
|
}, |
|
countryItem: { |
|
padding: 10, |
|
borderWidth: 1, |
|
borderColor: "#ddd", |
|
borderRadius: 5, |
|
}, |
|
selectedCountryItem: { |
|
backgroundColor: "#002fa7", |
|
}, |
|
countryItemText: { |
|
fontSize: fontSize(15), |
|
color: "#333", |
|
}, |
|
countryList: { |
|
flex: 1, |
|
}, |
|
selectedCountryItemText: { |
|
color: "white", |
|
}, |
|
});
|
|
|