Browse Source

Merge branch 'main' of http://116.198.244.231:3000/lifei/app into main

main
Your Name 3 weeks ago
parent
commit
f0a10c0095
  1. 671
      app/screens/RechargeScreen.tsx
  2. 29
      app/services/api/payApi.ts
  3. 967
      yarn.lock

671
app/screens/RechargeScreen.tsx

@ -1,5 +1,5 @@
// 支付组件
import React, { useState } from "react";
import React, { useState, useEffect, useCallback } from "react";
import {
View,
Text,
@ -8,6 +8,12 @@ import {
Image,
TouchableOpacity,
SafeAreaView,
ActivityIndicator,
Alert,
TextInput,
Linking,
Platform,
Clipboard,
} from "react-native";
import fontSize from "../utils/fontsizeUtils";
import widthUtils from "../utils/widthUtils";
@ -16,22 +22,60 @@ import CloseIcon from "../components/CloseIcon";
import CheckIcon from "../components/CheckIcon";
import BackIcon from "../components/BackIcon";
// 添加导航相关导入
import { useNavigation } from '@react-navigation/native';
import { navigationRef } from "../navigation/RootNavigation";
// 添加API服务
import { payApi } from "../services/api/payApi";
interface RechargeScreenProps {
onClose: () => void;
}
const RechargeScreen: React.FC<RechargeScreenProps> = ({ onClose }) => {
const RechargeScreen = ({ onClose }: RechargeScreenProps) => {
const [selectedPrice, setSelectedPrice] = useState("500,000");
const [selectedOperator, setSelectedOperator] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState(0);
const [showBlankPage, setShowBlankPage] = useState(false);
// 添加货币转换相关状态
const [isConverting, setIsConverting] = useState(false);
// 指定导航类型为any
const navigation = useNavigation<any>();
const [convertedAmount, setConvertedAmount] = useState<{
converted_amount: number;
item_key: string;
original_amount: number;
}[]>([]);
const [currentCurrency, setCurrentCurrency] = useState("USD");
const [phoneNumber, setPhoneNumber] = useState('22660962235');
const [isSubmitting, setIsSubmitting] = useState(false);
const [paymentParams, setPaymentParams] = useState<{
originalAmount: number;
amount: number;
currency: string;
payment_method: string;
selectedPriceLabel: string;
} | null>(null);
const handlePriceSelect = (price: string) => {
setSelectedPrice(price);
// 如果当前已选择了货币转换,则重新计算转换后的金额
if (selectedOperator === "currency" && currentCurrency !== "FCFA") {
handleCurrencyConversion(price, currentCurrency);
}
};
const handleOperatorSelect = (operator: string) => {
setSelectedOperator(operator === selectedOperator ? null : operator);
// 如果选择了货币支付方式,立即进行货币转换
if (operator === "currency" && operator !== selectedOperator) {
// 触发货币转换,使用默认的USD
handleCurrencySelect("USD");
}
};
const handleBackButton = () => {
@ -40,10 +84,341 @@ const RechargeScreen: React.FC<RechargeScreenProps> = ({ onClose }) => {
const handleButtonClick = () => {
if (selectedOperator) {
// 准备支付参数,方便后续发送
const params = {
originalAmount: parseFloat(selectedPrice.replace(/,/g, '')),
amount: parseFloat(selectedPrice.replace(/,/g, '')),
currency: "FCFA",
payment_method: "",
selectedPriceLabel: selectedPrice + " FCFA" // 选择的充值面额
};
// 根据selectedOperator确定支付方式
if (selectedOperator === "orange") {
params.payment_method = "Orange Money";
} else if (selectedOperator === "mtn") {
params.payment_method = "MTN MoMo";
} else if (selectedOperator === "balance") {
params.payment_method = "Balance";
} else if (selectedOperator === "currency") {
// 当选择了货币转换时
params.payment_method = "paypal";
params.currency = currentCurrency; // 使用选择的货币
// 使用转换后的金额,如果有
if (convertedAmount.length > 0) {
const convertedTotal = convertedAmount.find(item => item.item_key === 'total_amount');
if (convertedTotal) {
params.amount = convertedTotal.converted_amount;
}
}
}
// 保存支付参数
setPaymentParams(params);
// 打印支付信息
console.log("Payment Information:");
console.log("Selected Recharge Amount:", params.selectedPriceLabel);
console.log("Original Amount:", params.originalAmount, "FCFA");
console.log("Converted Amount:", params.amount);
console.log("Currency:", params.currency);
console.log("Payment Method:", params.payment_method);
setShowBlankPage(true);
}
};
// 提取一个专门用于货币转换的函数
const handleCurrencyConversion = (price: string, currency: string) => {
setIsConverting(true);
// 格式化金额,去除逗号
const amount = parseFloat(price.replace(/,/g, ''));
// 如果金额为0或无效,则不进行转换
if (!amount || isNaN(amount)) {
console.warn("Invalid amount for currency conversion");
setIsConverting(false);
return;
}
console.log(`Converting ${amount} FCFA to ${currency}...`);
// 调用货币转换API
const data = {
from_currency: 'FCFA',
to_currency: currency,
amounts: {
total_amount: amount,
domestic_shipping_fee: 0,
shipping_fee: 0,
},
};
payApi.convertCurrency(data)
.then((res) => {
if (res && res.converted_amounts_list && res.converted_amounts_list.length > 0) {
console.log("Conversion successful:", res.converted_amounts_list);
setConvertedAmount(res.converted_amounts_list);
} else {
console.error("Conversion response invalid:", res);
// 使用近似汇率作为备用
const fallbackRate = currency === "USD" ? 580.0 : 655.96; // 1 USD = 580 FCFA, 1 EUR = 655.96 FCFA
const convertedValue = amount / fallbackRate;
setConvertedAmount([{
converted_amount: convertedValue,
item_key: 'total_amount',
original_amount: amount
}]);
if (Platform.OS === 'web') {
console.warn("Using fallback conversion rate due to API error");
} else {
Alert.alert("Info", "Taux de conversion approximatif utilisé");
}
}
})
.catch((error) => {
console.error("Currency conversion failed:", error);
// 使用近似汇率作为备用
const fallbackRate = currency === "USD" ? 580.0 : 655.96;
const convertedValue = amount / fallbackRate;
setConvertedAmount([{
converted_amount: convertedValue,
item_key: 'total_amount',
original_amount: amount
}]);
if (Platform.OS === 'web') {
console.warn("Using fallback conversion rate due to API error");
} else {
Alert.alert("Info", "Taux de conversion approximatif utilisé");
}
})
.finally(() => {
setIsConverting(false);
});
};
// 修改货币选择函数,调用通用的转换函数
const handleCurrencySelect = (currency: string) => {
setCurrentCurrency(currency);
handleCurrencyConversion(selectedPrice, currency);
};
// 处理支付URL的函数
const openPaymentURL = useCallback((url: string) => {
console.log("Opening payment URL:", url);
// 判断运行平台
if (Platform.OS === 'web') {
try {
// Web平台首先尝试通过window.location跳转
// 保存当前URL作为回调地址,以便支付完成后返回
const currentUrl = window.location.href;
sessionStorage.setItem('payment_return_url', currentUrl);
// 尝试多种方式打开支付链接
// 方式1:直接修改当前窗口location
window.location.href = url;
// 方式2:如果直接跳转不生效,尝试打开新窗口
setTimeout(() => {
// 检查是否已经跳转
if (window.location.href === currentUrl) {
console.log("Direct navigation did not work, trying popup window...");
const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') {
console.warn("Popup blocked or failed to open. Trying iframe method.");
// 方式3:尝试使用iframe
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
// 显示手动跳转提示
alert(`La page de paiement n'a pas pu s'ouvrir automatiquement. Veuillez cliquer sur OK pour essayer à nouveau.`);
window.location.href = url;
}
}
}, 1000);
} catch (error) {
console.error("Failed to open payment URL on web:", error);
// 最后尝试
try {
window.open(url, '_self');
} catch (e) {
console.error("All payment URL opening methods failed:", e);
alert(`Impossible d'ouvrir la page de paiement. URL: ${url}`);
}
}
} else {
// 移动平台使用Linking打开
Linking.canOpenURL(url).then(supported => {
if (supported) {
Linking.openURL(url).catch(err => {
console.error("Error opening URL with Linking:", err);
// 尝试使用WebView组件导航
if (navigationRef.current) {
navigationRef.current.navigate('Pay', { payUrl: url });
} else {
Alert.alert(
"Erreur",
"Impossible d'ouvrir la page de paiement. Veuillez réessayer.",
[
{
text: "Réessayer",
onPress: () => Linking.openURL(url)
},
{ text: "Annuler" }
]
);
}
});
} else {
console.error("Cannot open URL: " + url);
Alert.alert(
"Erreur",
"Cette URL ne peut pas être ouverte. Veuillez contacter le support technique.",
[
{
text: "Copier l'URL",
onPress: () => {
Clipboard.setString(url);
Alert.alert("Info", "URL copiée dans le presse-papiers");
}
},
{ text: "Fermer" }
]
);
}
}).catch(err => {
console.error("Couldn't check if URL can be opened:", err);
Alert.alert("Erreur", "Impossible de vérifier si l'URL peut être ouverte");
});
}
}, []);
// 更新处理支付提交的函数
const handlePaySubmit = async () => {
if (!paymentParams) {
if (Platform.OS === 'web') {
alert("Veuillez sélectionner un mode de paiement");
} else {
Alert.alert("Erreur", "Veuillez sélectionner un mode de paiement");
}
return;
}
// 验证电话号码(添加更严格的验证)
if (!phoneNumber || phoneNumber.length < 8) {
if (Platform.OS === 'web') {
alert("Veuillez entrer un numéro de téléphone valide (au moins 8 chiffres)");
} else {
Alert.alert("Erreur", "Veuillez entrer un numéro de téléphone valide (au moins 8 chiffres)");
}
return;
}
// 显示提交中状态
setIsSubmitting(true);
try {
// 准备请求数据,添加电话号码
const rechargeData = {
amount: paymentParams.amount,
currency: paymentParams.currency,
payment_method: paymentParams.payment_method,
phone: phoneNumber // 注意:如果后端API不支持此参数,可能需要调整
};
console.log("Submitting recharge request:", rechargeData);
// 调用充值接口(使用可选链避免错误)
const response = await payApi.initiateRecharge(rechargeData);
console.log("Recharge response:", response);
if (response && response.success) {
const paymentInfo = response.payment;
// 存储交易ID,以便后续查询
if (Platform.OS === 'web') {
sessionStorage.setItem('recharge_transaction_id', paymentInfo.transaction_id || '');
sessionStorage.setItem('recharge_id', response.recharge_id?.toString() || '');
}
// 检查是否有支付URL
if (paymentInfo && paymentInfo.payment_url) {
// 记录重要信息到控制台
console.log("Transaction ID:", paymentInfo.transaction_id);
console.log("Recharge ID:", response.recharge_id);
console.log("Payment URL:", paymentInfo.payment_url);
// 打开支付页面
navigation.navigate("Pay", {
payUrl: paymentInfo.payment_url,
});
// Web平台添加额外提示,因为可能会被浏览器拦截
if (Platform.OS === 'web') {
setTimeout(() => {
alert(`Si la page de paiement ne s'ouvre pas automatiquement, veuillez cliquer sur le bouton "Continuer" pour procéder au paiement. Vous pouvez également copier ce lien: ${paymentInfo.payment_url}`);
}, 1500);
}
} else {
// 没有支付URL但交易成功的情况
if (Platform.OS === 'web') {
alert("Votre recharge a été traitée avec succès!");
onClose(); // 关闭充值页面
} else {
Alert.alert(
"Succès",
"Votre recharge a été traitée avec succès!",
[{ text: "OK", onPress: onClose }]
);
}
}
} else {
// 处理失败情况,显示错误消息
const errorMessage = response?.msg || "Une erreur s'est produite lors du traitement de la recharge. Veuillez réessayer.";
if (Platform.OS === 'web') {
alert(errorMessage);
} else {
Alert.alert("Erreur", errorMessage);
}
}
} catch (error) {
// 处理异常
console.error("Recharge error:", error);
let errorMessage = "Une erreur s'est produite lors du traitement de la recharge. Veuillez réessayer.";
// 尝试从错误对象中提取更具体的错误信息
if (error instanceof Error) {
errorMessage = error.message || errorMessage;
}
if (Platform.OS === 'web') {
alert(errorMessage);
} else {
Alert.alert("Erreur", errorMessage);
}
} finally {
// 无论成功失败,都取消提交状态
setIsSubmitting(false);
}
};
return (
<SafeAreaView style={styles.safeArea}>
<View style={showBlankPage ? styles.mainContainer1 : styles.mainContainer}>
@ -399,14 +774,31 @@ const RechargeScreen: React.FC<RechargeScreenProps> = ({ onClose }) => {
<>
<Text style={styles.subtitle}>Choisissez la devise</Text>
<View style={styles.row}>
<TouchableOpacity style={styles.currencyButtonActive}>
<Text style={styles.buttonTextWhite}>USD</Text>
<TouchableOpacity
style={currentCurrency === "USD" ? styles.currencyButtonActive : styles.currencyButtonInactive}
onPress={() => handleCurrencySelect("USD")}
>
<Text style={currentCurrency === "USD" ? styles.buttonTextWhite : styles.buttonTextDark}>USD</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.currencyButtonInactive}>
<Text style={styles.buttonTextDark}>FCFA</Text>
<TouchableOpacity
style={currentCurrency === "EUR" ? styles.currencyButtonActive : styles.currencyButtonInactive}
onPress={() => handleCurrencySelect("EUR")}
>
<Text style={currentCurrency === "EUR" ? styles.buttonTextWhite : styles.buttonTextDark}>EUR</Text>
</TouchableOpacity>
</View>
<Text style={styles.totalText}>Total: 121.97</Text>
{isConverting ? (
<View style={{flexDirection: 'row', alignItems: 'center', marginTop: 16}}>
<ActivityIndicator size="small" color="#ff5100" />
<Text style={{...styles.totalText, marginLeft: 10}}>Converting...</Text>
</View>
) : (
<Text style={styles.totalText}>
Total: {convertedAmount.length > 0
? `${currentCurrency === "USD" ? "$" : "€"}${convertedAmount.find(item => item.item_key === 'total_amount')?.converted_amount.toFixed(2) || '0.00'}`
: `$${(parseFloat(selectedPrice.replace(/,/g, '')) / 580).toFixed(2)}`}
</Text>
)}
</>
)}
</View>
@ -510,100 +902,93 @@ const RechargeScreen: React.FC<RechargeScreenProps> = ({ onClose }) => {
</View>
</>
) : (
<View style={styles.outerContainer}>
<View style={styles.paymentSection2}>
{/* Header with back button and title */}
{/* Transaction summary */}
<View style={styles.transactionSummaryContainer1}>
<View style={styles.transactionSummaryContainer}>
<View style={styles.flexContainerWithImages}>
<Image
source={require('../../assets/img/image_cb840273.png')}
style={styles.imageContainerStyled}
resizeMode="cover"
/>
<Image
source={require('../../assets/img/image_b05c3ffe.png')}
style={styles.imageContainerWithBorder}
resizeMode="cover"
/>
</View>
<View style={styles.amountContainer}>
<Text style={styles.amountLabel}>Montant</Text>
<View style={styles.amountContainer1}>
<Text style={styles.priceHeading}>500,000</Text>
<Text style={styles.priceLabel}>FCFA</Text>
<View style={styles.paymentConfirmContainer}>
{/* 充值金额信息 */}
<View style={styles.paymentSummaryCard}>
<Text style={styles.paymentSummaryTitle}>Résumé de la recharge</Text>
<View style={styles.paymentSummaryRow}>
<Text style={styles.paymentSummaryLabel}>Montant:</Text>
<Text style={styles.paymentSummaryValue}>
{paymentParams?.selectedPriceLabel || selectedPrice + " FCFA"}
</Text>
</View>
{paymentParams?.currency !== "FCFA" && (
<View style={styles.paymentSummaryRow}>
<Text style={styles.paymentSummaryLabel}>Montant converti:</Text>
<Text style={styles.paymentSummaryValueHighlight}>
{paymentParams?.currency === "USD" ? "$" : "€"}
{paymentParams?.amount.toFixed(2) || "0.00"}
</Text>
</View>
)}
<View style={styles.paymentSummaryRow}>
<Text style={styles.paymentSummaryLabel}>Méthode de paiement:</Text>
<Text style={styles.paymentSummaryValue}>
{paymentParams?.payment_method || "Non sélectionné"}
</Text>
</View>
</View>
</View>
{/* Mobile info section */}
<View style={styles.mobileInfoSection}>
<View style={styles.mobileNumberSection}>
<Text style={styles.mobileNumberLabel}>Indicatif pays et numéro de mobile</Text>
<View style={styles.infoCard}>
<View style={styles.flexRowWithIcon}>
<View style={styles.flexRowWithIcon}>
<Image
source={require('../../assets/img/image_de4ca740.png')}
style={styles.maskedImageWithText}
resizeMode="cover"
/>
<Text style={styles.highlightText}>+89</Text>
</View>
<View style={styles.svgContainer2}>
{/* <Svg width={12} height={12} viewBox="0 0 12 12" fill="none">
<ClipPath id="clip3_296_2233">
<Rect width={12} height={12} fill="white" x={12.0004} y={0} rotation={90} />
</ClipPath>
<G clipPath="url(#clip3_296_2233)">
<Path
d="M11.4464,3.658l-1.01,-1.035l-4.413,4.638l-4.4136,-4.638l-1.0098,1.035l5.4234,5.672z"
fill="black"
/>
</G>
</Svg> */}
</View>
<View style={styles.verticalDivider} />
{/* 电话号码输入 */}
<View style={styles.phoneInputContainer}>
<Text style={styles.phoneInputLabel}>Numéro de téléphone</Text>
<View style={styles.phoneInputWrapper}>
<View style={styles.countryCodeContainer}>
<Text style={styles.countryCodeText}>+89</Text>
</View>
<Text style={styles.statisticText}>22660962235</Text>
<TextInput
style={styles.phoneInput}
value={phoneNumber}
onChangeText={setPhoneNumber}
keyboardType="phone-pad"
placeholder="Entrez votre numéro"
placeholderTextColor="#999"
/>
</View>
</View>
{/* Mobile operators */}
<View style={styles.mobileOperatorsContainer}>
<Text style={styles.mobileNumberLabel}>Opérateurs pris en charge</Text>
<View style={styles.operatorSupportContainer}>
<Image
source={require('../../assets/img/image_7337a807.png')}
style={styles.imageContainerWithBorder1}
resizeMode="cover"
{/* 支持的运营商 */}
<View style={styles.supportedOperatorsContainer}>
<Text style={styles.supportedOperatorsTitle}>Opérateurs pris en charge</Text>
<View style={styles.operatorsRow}>
<Image
source={require("../../assets/img/image_7337a807.png")}
style={styles.operatorSmallIcon}
/>
<Image
source={require('../../assets/img/image_96b927ad.png')}
style={styles.imageContainerWithBorder1}
resizeMode="cover"
<Image
source={require("../../assets/img/image_96b927ad.png")}
style={styles.operatorSmallIcon}
/>
<Image
source={require('../../assets/img/image_1fee7e8b.png')}
style={styles.imageContainerWithBorder1}
resizeMode="cover"
<Image
source={require("../../assets/img/image_1fee7e8b.png")}
style={styles.operatorSmallIcon}
/>
</View>
</View>
{/* Payment button */}
<View style={styles.blueBoxContainer}>
<View style={styles.paymentNotice1}>
<Text style={styles.paymentNotice}>PAY 500,000 FCFA</Text>
</View>
</View>
{/* 支付按钮 */}
<TouchableOpacity
style={[styles.payButton, isSubmitting && styles.payButtonDisabled]}
onPress={handlePaySubmit}
disabled={isSubmitting}
>
{isSubmitting ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={styles.payButtonText}>
PAYER {paymentParams?.currency === "FCFA"
? paymentParams.originalAmount.toLocaleString() + " " + paymentParams.currency
: (paymentParams?.currency === "USD"
? "$" + paymentParams?.amount.toFixed(2)
: paymentParams?.amount.toFixed(2) + " €")
}
</Text>
)}
</TouchableOpacity>
</View>
</View>
</View>
</View>
)}
</View>
</SafeAreaView>
@ -1288,6 +1673,112 @@ const styles = StyleSheet.create({
color: 'white',
fontFamily: 'Montserrat-Bold',
},
paymentConfirmContainer: {
flex: 1,
padding: 20,
backgroundColor: "#fff",
},
paymentSummaryCard: {
backgroundColor: "#f5f9ff",
borderRadius: 10,
padding: 20,
marginBottom: 20,
},
paymentSummaryTitle: {
fontSize: fontSize(18),
fontWeight: "700",
marginBottom: 15,
color: "#333",
},
paymentSummaryRow: {
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 10,
},
paymentSummaryLabel: {
fontSize: fontSize(14),
color: "#666",
},
paymentSummaryValue: {
fontSize: fontSize(14),
fontWeight: "500",
color: "#333",
},
paymentSummaryValueHighlight: {
fontSize: fontSize(14),
fontWeight: "600",
color: "#ff5100",
},
phoneInputContainer: {
marginBottom: 20,
},
phoneInputLabel: {
fontSize: fontSize(16),
fontWeight: "500",
marginBottom: 10,
color: "#333",
},
phoneInputWrapper: {
flexDirection: "row",
borderWidth: 1,
borderColor: "#ddd",
borderRadius: 25,
overflow: "hidden",
},
countryCodeContainer: {
backgroundColor: "#f5f5f5",
paddingHorizontal: 15,
justifyContent: "center",
borderRightWidth: 1,
borderRightColor: "#ddd",
},
countryCodeText: {
fontSize: fontSize(16),
color: "#333",
},
phoneInput: {
flex: 1,
height: 50,
paddingHorizontal: 15,
fontSize: fontSize(16),
color: "#333",
},
supportedOperatorsContainer: {
marginBottom: 30,
},
supportedOperatorsTitle: {
fontSize: fontSize(16),
fontWeight: "500",
marginBottom: 10,
color: "#333",
},
operatorsRow: {
flexDirection: "row",
alignItems: "center",
},
operatorSmallIcon: {
width: 70,
height: 26,
resizeMode: "contain",
marginRight: 15,
},
payButton: {
backgroundColor: "#002fa7",
borderRadius: 25,
height: 50,
justifyContent: "center",
alignItems: "center",
marginTop: 20,
},
payButtonDisabled: {
backgroundColor: "#8da0d4",
opacity: 0.7,
},
payButtonText: {
color: "white",
fontSize: fontSize(16),
fontWeight: "700",
},
});
export default RechargeScreen;

29
app/services/api/payApi.ts

@ -30,6 +30,30 @@ export interface PayInfoBody {
currency: string;
}
// 新增充值接口请求体类型
export interface RechargeInitiateBody {
amount: number;
currency: string;
payment_method: string;
phone?: string;
}
// 新增充值接口返回类型
export interface RechargeInitiateResponse {
success: boolean;
recharge_id: number;
payment: {
success: boolean;
msg: string;
payment_url: string;
order_id: number;
method: string;
status: string | null;
transaction_id: string;
};
msg: string | null;
}
export interface ConvertCurrencyBody {
from_currency: string;
to_currency: string;
@ -69,4 +93,9 @@ export const payApi = {
paySuccessCallback: (paymentId:string,PayerID:string) => {
return apiService.get<PaySuccessCallbackBody>(`/api/payment/paypal/execute/?paymentId=${paymentId}&PayerID=${PayerID}`);
},
// 新增充值接口
initiateRecharge: (data: RechargeInitiateBody) => {
return apiService.post<RechargeInitiateResponse>('/api/recharge/initiate/', data);
},
};

967
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save