feat(waffo): Waffo payment gateway integration with configurable methods

- Add Waffo payment SDK integration (waffo-go v1.3.1)
- Backend: webhook handler, pay endpoint, order lock race-condition fix
- Settings: full Waffo config (API keys, sandbox/prod, currency, pay methods)
- Frontend: Waffo payment buttons in topup page, admin settings panel
- i18n: Waffo-related translations for en/fr/ja/ru/vi/zh-TW

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
zhongyuan.zhao
2026-03-17 18:04:58 +08:00
parent 620e066b39
commit 202a433f86
24 changed files with 1472 additions and 89 deletions
+59 -10
View File
@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useContext, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
API,
showError,
@@ -41,6 +42,7 @@ import TopupHistoryModal from './modals/TopupHistoryModal';
const TopUp = () => {
const { t } = useTranslation();
const [searchParams, setSearchParams] = useSearchParams();
const [userState, userDispatch] = useContext(UserContext);
const [statusState] = useContext(StatusContext);
@@ -69,6 +71,10 @@ const TopUp = () => {
const [creemOpen, setCreemOpen] = useState(false);
const [selectedCreemProduct, setSelectedCreemProduct] = useState(null);
// Waffo 相关状态
const [enableWaffoTopUp, setEnableWaffoTopUp] = useState(false);
const [waffoPayMethods, setWaffoPayMethods] = useState([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [open, setOpen] = useState(false);
const [payWay, setPayWay] = useState('');
@@ -256,7 +262,6 @@ const TopUp = () => {
showError(res);
}
} catch (err) {
console.log(err);
showError(t('支付请求失败'));
} finally {
setOpen(false);
@@ -302,7 +307,6 @@ const TopUp = () => {
showError(res);
}
} catch (err) {
console.log(err);
showError(t('支付请求失败'));
} finally {
setCreemOpen(false);
@@ -310,6 +314,37 @@ const TopUp = () => {
}
};
const waffoTopUp = async (payMethodIndex) => {
try {
if (topUpCount < minTopUp) {
showError(t('充值数量不能小于') + minTopUp);
return;
}
setPaymentLoading(true);
const requestBody = {
amount: parseInt(topUpCount),
};
if (payMethodIndex != null) {
requestBody.pay_method_index = payMethodIndex;
}
const res = await API.post('/api/user/waffo/pay', requestBody);
if (res !== undefined) {
const { message, data } = res.data;
if (message === 'success' && data?.payment_url) {
window.open(data.payment_url, '_blank');
} else {
showError(data || t('支付请求失败'));
}
} else {
showError(res);
}
} catch (e) {
showError(t('支付请求失败'));
} finally {
setPaymentLoading(false);
}
};
const processCreemCallback = (data) => {
// 与 Stripe 保持一致的实现方式
window.open(data.checkout_url, '_blank');
@@ -449,17 +484,20 @@ const TopUp = () => {
? data.min_topup
: enableStripeTopUp
? data.stripe_min_topup
: 1;
: data.enable_waffo_topup
? data.waffo_min_topup
: 1;
setEnableOnlineTopUp(enableOnlineTopUp);
setEnableStripeTopUp(enableStripeTopUp);
setEnableCreemTopUp(enableCreemTopUp);
const enableWaffoTopUp = data.enable_waffo_topup || false;
setEnableWaffoTopUp(enableWaffoTopUp);
setWaffoPayMethods(data.waffo_pay_methods || []);
setMinTopUp(minTopUpValue);
setTopUpCount(minTopUpValue);
// 设置 Creem 产品
try {
console.log(' data is ?', data);
console.log(' creem products is ?', data.creem_products);
const products = JSON.parse(data.creem_products || '[]');
setCreemProducts(products);
} catch (e) {
@@ -474,7 +512,6 @@ const TopUp = () => {
// 初始化显示实付金额
getAmount(minTopUpValue);
} catch (e) {
console.log('解析支付方式失败:', e);
setPayMethods([]);
}
@@ -487,10 +524,10 @@ const TopUp = () => {
setPresetAmounts(customPresets);
}
} else {
console.error('获取充值配置失败:', data);
showError(data || t('获取充值配置失败'));
}
} catch (error) {
console.error('获取充值配置异常:', error);
showError(t('获取充值配置异常'));
}
};
@@ -531,6 +568,15 @@ const TopUp = () => {
showSuccess(t('邀请链接已复制到剪切板'));
};
// URL 参数自动打开账单弹窗(支付回跳时触发)
useEffect(() => {
if (searchParams.get('show_history') === 'true') {
setOpenHistory(true);
searchParams.delete('show_history');
setSearchParams(searchParams, { replace: true });
}
}, []);
useEffect(() => {
// 始终获取最新用户数据,确保余额等统计信息准确
getUserQuota().then();
@@ -587,7 +633,7 @@ const TopUp = () => {
showError(res);
}
} catch (err) {
console.log(err);
// amount fetch failed silently
}
setAmountLoading(false);
};
@@ -613,7 +659,7 @@ const TopUp = () => {
showError(res);
}
} catch (err) {
console.log(err);
// amount fetch failed silently
} finally {
setAmountLoading(false);
}
@@ -740,6 +786,9 @@ const TopUp = () => {
enableCreemTopUp={enableCreemTopUp}
creemProducts={creemProducts}
creemPreTopUp={creemPreTopUp}
enableWaffoTopUp={enableWaffoTopUp}
waffoTopUp={waffoTopUp}
waffoPayMethods={waffoPayMethods}
presetAmounts={presetAmounts}
selectedPreset={selectedPreset}
selectPresetAmount={selectPresetAmount}