feat: require compliance confirmation for paid features

Gate payment, redemption, subscription, and invitation reward flows behind an audited compliance acknowledgement.
This commit is contained in:
CaIon
2026-05-13 22:18:46 +08:00
parent aa56667b8f
commit 0526a22643
57 changed files with 1806 additions and 216 deletions
@@ -62,6 +62,7 @@ const RiskAcknowledgementModal = React.memo(function RiskAcknowledgementModal({
checklist = [],
inputPrompt = '',
requiredText = '',
requiredTextParts = [],
inputPlaceholder = '',
mismatchText = '',
cancelText = '',
@@ -72,24 +73,68 @@ const RiskAcknowledgementModal = React.memo(function RiskAcknowledgementModal({
const isMobile = useIsMobile();
const [checkedItems, setCheckedItems] = useState([]);
const [typedText, setTypedText] = useState('');
const [typedTextParts, setTypedTextParts] = useState([]);
const normalizedRequiredTextParts = useMemo(() => {
let inputIndex = 0;
return requiredTextParts.map((part) => {
if (part.type === 'input') {
const normalizedPart = { ...part, inputIndex };
inputIndex += 1;
return normalizedPart;
}
return part;
});
}, [requiredTextParts]);
const requiredTextInputCount = useMemo(
() =>
normalizedRequiredTextParts.filter((part) => part.type === 'input')
.length,
[normalizedRequiredTextParts],
);
const hasSegmentedRequiredText = requiredTextInputCount > 0;
const requiredTextToDisplay = hasSegmentedRequiredText
? normalizedRequiredTextParts.map((part) => part.text).join('')
: requiredText;
useEffect(() => {
if (!visible) return;
setCheckedItems(Array(checklist.length).fill(false));
setTypedText('');
}, [visible, checklist.length]);
setTypedTextParts(Array(requiredTextInputCount).fill(''));
}, [visible, checklist.length, requiredTextInputCount]);
const allChecked = useMemo(() => {
if (checklist.length === 0) return true;
return checkedItems.length === checklist.length && checkedItems.every(Boolean);
return (
checkedItems.length === checklist.length && checkedItems.every(Boolean)
);
}, [checkedItems, checklist.length]);
const typedMatched = useMemo(() => {
if (hasSegmentedRequiredText) {
return normalizedRequiredTextParts.every((part) => {
if (part.type === 'static') return true;
return (
typedTextParts[part.inputIndex ?? 0]?.trim() === part.text.trim()
);
});
}
if (!requiredText) return true;
return typedText.trim() === requiredText.trim();
}, [typedText, requiredText]);
}, [
hasSegmentedRequiredText,
normalizedRequiredTextParts,
requiredText,
typedText,
typedTextParts,
]);
const detailText = useMemo(() => detailItems.join(', '), [detailItems]);
const hasTypedRequiredText = hasSegmentedRequiredText
? typedTextParts.some((part) => part.trim() !== '')
: typedText.length > 0;
const canConfirm = allChecked && typedMatched;
const handleChecklistChange = useCallback((index, checked) => {
@@ -100,6 +145,14 @@ const RiskAcknowledgementModal = React.memo(function RiskAcknowledgementModal({
});
}, []);
const handleTextPartChange = useCallback((index, value) => {
setTypedTextParts((previous) => {
const next = [...previous];
next[index] = value;
return next;
});
}, []);
return (
<Modal
visible={visible}
@@ -134,7 +187,6 @@ const RiskAcknowledgementModal = React.memo(function RiskAcknowledgementModal({
}
>
<div className='flex flex-col gap-4'>
<RiskMarkdownBlock markdownContent={markdownContent} />
{detailItems.length > 0 ? (
@@ -176,7 +228,7 @@ const RiskAcknowledgementModal = React.memo(function RiskAcknowledgementModal({
</div>
) : null}
{requiredText ? (
{requiredTextToDisplay ? (
<div
className='flex flex-col gap-2 rounded-lg'
style={{
@@ -187,19 +239,51 @@ const RiskAcknowledgementModal = React.memo(function RiskAcknowledgementModal({
>
{inputPrompt ? <Text strong>{inputPrompt}</Text> : null}
<div className='font-mono text-xs break-all rounded-md p-2 bg-gray-50 border border-gray-200'>
{requiredText}
{requiredTextToDisplay}
</div>
<Input
value={typedText}
onChange={setTypedText}
placeholder={inputPlaceholder}
autoFocus={visible}
onCopy={(event) => event.preventDefault()}
onCut={(event) => event.preventDefault()}
onPaste={(event) => event.preventDefault()}
onDrop={(event) => event.preventDefault()}
/>
{!typedMatched && typedText ? (
{hasSegmentedRequiredText ? (
<div className='flex flex-wrap items-center gap-2'>
{normalizedRequiredTextParts.map((part, index) =>
part.type === 'static' ? (
<span
key={`static-${index}`}
className='select-none rounded-md border border-gray-200 bg-white px-2 py-1 font-mono text-sm text-gray-500'
>
{part.text}
</span>
) : (
<Input
key={`input-${index}`}
value={typedTextParts[part.inputIndex ?? 0] ?? ''}
onChange={(value) =>
handleTextPartChange(part.inputIndex ?? 0, value)
}
placeholder={
part.placeholder || part.text || inputPlaceholder
}
autoFocus={visible && part.inputIndex === 0}
onCopy={(event) => event.preventDefault()}
onCut={(event) => event.preventDefault()}
onPaste={(event) => event.preventDefault()}
onDrop={(event) => event.preventDefault()}
style={{ width: isMobile ? '100%' : 260 }}
/>
),
)}
</div>
) : (
<Input
value={typedText}
onChange={setTypedText}
placeholder={inputPlaceholder}
autoFocus={visible}
onCopy={(event) => event.preventDefault()}
onCut={(event) => event.preventDefault()}
onPaste={(event) => event.preventDefault()}
onDrop={(event) => event.preventDefault()}
/>
)}
{!typedMatched && hasTypedRequiredText ? (
<Text type='danger' size='small'>
{mismatchText}
</Text>
+196 -49
View File
@@ -18,15 +18,18 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState } from 'react';
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
import { Banner, Button, Card, Spin, Tabs } from '@douyinfe/semi-ui';
import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralPayment';
import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway';
import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe';
import SettingsPaymentGatewayCreem from '../../pages/Setting/Payment/SettingsPaymentGatewayCreem';
import SettingsPaymentGatewayWaffo from '../../pages/Setting/Payment/SettingsPaymentGatewayWaffo';
import SettingsPaymentGatewayWaffoPancake from '../../pages/Setting/Payment/SettingsPaymentGatewayWaffoPancake';
import { API, showError, toBoolean } from '../../helpers';
import { API, showError, showSuccess, toBoolean } from '../../helpers';
import { useTranslation } from 'react-i18next';
import RiskAcknowledgementModal from '../common/modals/RiskAcknowledgementModal';
const CURRENT_COMPLIANCE_TERMS_VERSION = 'v1';
const PaymentSetting = () => {
const { t } = useTranslation();
@@ -60,9 +63,59 @@ const PaymentSetting = () => {
WaffoPancakeCurrency: 'USD',
WaffoPancakeUnitPrice: 1.0,
WaffoPancakeMinTopUp: 1,
'payment_setting.compliance_confirmed': false,
'payment_setting.compliance_terms_version': '',
'payment_setting.compliance_confirmed_at': 0,
'payment_setting.compliance_confirmed_by': 0,
});
let [loading, setLoading] = useState(false);
const [complianceVisible, setComplianceVisible] = useState(false);
const complianceStatements = [
t('你已合法取得所接入模型 API、账号、密钥和额度的授权;'),
t(
'你承诺仅在已取得上游服务商、模型服务提供方或相关权利方合法授权的范围内使用其 API、账号、密钥、额度及服务能力,不进行未经授权的转售、倒卖、分销或其他违规商业化使用。',
),
t(
'如向中华人民共和国境内公众提供生成式人工智能服务,你将依法履行备案登记、安全评估、内容安全、投诉举报、生成合成内容标识、日志留存、个人信息保护等合规义务;',
),
t(
'你承诺不会利用本系统实施、协助实施或变相实施违反适用法律法规、监管要求、平台规则、社会公共利益或第三方合法权益的行为。',
),
t('你理解并自行承担部署、运营和收费行为产生的法律责任。'),
t(
'你理解本合规提醒仅用于风险提示,不构成法律意见、合规审查结论或对你使用本系统行为合法性的保证;你应根据实际业务场景自行咨询专业法律或合规顾问。',
),
];
const requiredComplianceText = t(
'我已阅读并理解上述合规提醒,知悉相关法律风险,并确认自行承担部署、运营和收费行为产生的法律责任',
);
const requiredComplianceTextParts = [
{
type: 'input',
text: t('我已阅读并理解上述合规提醒'),
},
{ type: 'static', text: t('') },
{
type: 'input',
text: t('知悉相关法律风险'),
},
{ type: 'static', text: t('') },
{
type: 'input',
text: t('并确认自行承担部署'),
},
{ type: 'static', text: t('、') },
{
type: 'input',
text: t('运营和收费行为产生的法律责任'),
},
];
const complianceConfirmed =
inputs['payment_setting.compliance_confirmed'] &&
inputs['payment_setting.compliance_terms_version'] ===
CURRENT_COMPLIANCE_TERMS_VERSION;
const getOptions = async () => {
const res = await API.get('/api/option/');
@@ -104,6 +157,16 @@ const PaymentSetting = () => {
newInputs['AmountDiscount'] = item.value;
}
break;
case 'payment_setting.compliance_confirmed':
newInputs[item.key] = toBoolean(item.value);
break;
case 'payment_setting.compliance_confirmed_at':
case 'payment_setting.compliance_confirmed_by':
newInputs[item.key] = parseInt(item.value) || 0;
break;
case 'payment_setting.compliance_terms_version':
newInputs[item.key] = item.value;
break;
case 'Price':
case 'MinTopUp':
case 'StripeUnitPrice':
@@ -154,59 +217,143 @@ const PaymentSetting = () => {
onRefresh();
}, []);
const confirmCompliance = async () => {
try {
const res = await API.post('/api/option/payment_compliance', {
confirmed: true,
});
if (res.data.success) {
showSuccess(t('合规声明确认成功'));
setComplianceVisible(false);
await onRefresh();
} else {
showError(res.data.message || t('确认失败'));
}
} catch (error) {
showError(t('确认失败'));
}
};
return (
<>
<Spin spinning={loading} size='large'>
<Card style={{ marginTop: '10px' }}>
<Tabs
type='card'
defaultActiveKey='general'
contentStyle={{ paddingTop: 24 }}
{!complianceConfirmed ? (
<Banner
type='warning'
title={t('需要确认合规声明')}
description={
<div className='flex flex-col gap-2'>
<span>
{t(
'确认前,支付、兑换码、订阅计划和邀请返利功能将保持锁定。',
)}
</span>
<Button
type='warning'
theme='solid'
onClick={() => setComplianceVisible(true)}
>
{t('确认合规声明')}
</Button>
</div>
}
closeIcon={null}
style={{ marginBottom: 16 }}
fullMode={false}
/>
) : (
<Banner
type='success'
title={t('合规声明已确认')}
description={t('确认时间:{{time}},确认用户:#{{userId}}', {
time: inputs['payment_setting.compliance_confirmed_at']
? new Date(
inputs['payment_setting.compliance_confirmed_at'] * 1000,
).toLocaleString()
: '-',
userId:
inputs['payment_setting.compliance_confirmed_by'] || '-',
})}
closeIcon={null}
style={{ marginBottom: 16 }}
fullMode={false}
/>
)}
<div
style={
complianceConfirmed
? undefined
: { opacity: 0.4, pointerEvents: 'none' }
}
>
<Tabs.TabPane tab={t('通用设置')} itemKey='general'>
<SettingsGeneralPayment
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t('易支付设置')} itemKey='epay'>
<SettingsPaymentGateway
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t('Stripe 设置')} itemKey='stripe'>
<SettingsPaymentGatewayStripe
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t('Creem 设置')} itemKey='creem'>
<SettingsPaymentGatewayCreem
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t('Waffo 设置')} itemKey='waffo'>
<SettingsPaymentGatewayWaffo
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
{/*<Tabs.TabPane tab={t('Waffo Pancake 设置')} itemKey='waffo-pancake'>*/}
{/* <SettingsPaymentGatewayWaffoPancake*/}
{/* options={inputs}*/}
{/* refresh={onRefresh}*/}
{/* hideSectionTitle*/}
{/* />*/}
{/*</Tabs.TabPane>*/}
</Tabs>
<Tabs
type='card'
defaultActiveKey='general'
contentStyle={{ paddingTop: 24 }}
>
<Tabs.TabPane tab={t('通用设置')} itemKey='general'>
<SettingsGeneralPayment
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t('易支付设置')} itemKey='epay'>
<SettingsPaymentGateway
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t('Stripe 设置')} itemKey='stripe'>
<SettingsPaymentGatewayStripe
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t('Creem 设置')} itemKey='creem'>
<SettingsPaymentGatewayCreem
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t('Waffo 设置')} itemKey='waffo'>
<SettingsPaymentGatewayWaffo
options={inputs}
refresh={onRefresh}
hideSectionTitle
/>
</Tabs.TabPane>
{/*<Tabs.TabPane tab={t('Waffo Pancake 设置')} itemKey='waffo-pancake'>*/}
{/* <SettingsPaymentGatewayWaffoPancake*/}
{/* options={inputs}*/}
{/* refresh={onRefresh}*/}
{/* hideSectionTitle*/}
{/* />*/}
{/*</Tabs.TabPane>*/}
</Tabs>
</div>
</Card>
<RiskAcknowledgementModal
visible={complianceVisible}
title={t('确认合规声明')}
markdownContent={t(
'该操作将启用支付、兑换码、订阅计划和邀请返利相关功能。请仔细阅读并确认以下声明。',
)}
checklist={complianceStatements}
inputPrompt={t('请输入以下文字以确认:')}
requiredText={requiredComplianceText}
requiredTextParts={requiredComplianceTextParts}
inputPlaceholder={t('请输入确认文案')}
mismatchText={t('输入内容与要求文案不一致')}
cancelText={t('取消')}
confirmText={t('确认并启用')}
onCancel={() => setComplianceVisible(false)}
onConfirm={confirmCompliance}
/>
</Spin>
</>
);
@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react';
import { Button } from '@douyinfe/semi-ui';
const SubscriptionsActions = ({ openCreate, t }) => {
const SubscriptionsActions = ({ openCreate, t, disabled = false }) => {
return (
<div className='flex gap-2 w-full md:w-auto'>
<Button
@@ -28,6 +28,7 @@ const SubscriptionsActions = ({ openCreate, t }) => {
className='w-full md:w-auto'
onClick={openCreate}
size='small'
disabled={disabled}
>
{t('新建套餐')}
</Button>
@@ -228,7 +228,11 @@ const renderPaymentConfig = (text, record, t, enableEpay) => {
);
};
const renderOperations = (text, record, { openEdit, setPlanEnabled, t }) => {
const renderOperations = (
text,
record,
{ openEdit, setPlanEnabled, t, complianceConfirmed },
) => {
const isEnabled = record?.plan?.enabled;
const handleToggle = () => {
@@ -256,11 +260,18 @@ const renderOperations = (text, record, { openEdit, setPlanEnabled, t }) => {
type='tertiary'
size='small'
onClick={() => openEdit(record)}
disabled={!complianceConfirmed}
>
{t('编辑')}
</Button>
{isEnabled ? (
<Button theme='light' type='danger' size='small' onClick={handleToggle}>
<Button
theme='light'
type='danger'
size='small'
onClick={handleToggle}
disabled={!complianceConfirmed}
>
{t('禁用')}
</Button>
) : (
@@ -269,6 +280,7 @@ const renderOperations = (text, record, { openEdit, setPlanEnabled, t }) => {
type='primary'
size='small'
onClick={handleToggle}
disabled={!complianceConfirmed}
>
{t('启用')}
</Button>
@@ -282,6 +294,7 @@ export const getSubscriptionsColumns = ({
openEdit,
setPlanEnabled,
enableEpay,
complianceConfirmed = true,
}) => {
return [
{
@@ -351,7 +364,12 @@ export const getSubscriptionsColumns = ({
fixed: 'right',
width: 160,
render: (text, record) =>
renderOperations(text, record, { openEdit, setPlanEnabled, t }),
renderOperations(text, record, {
openEdit,
setPlanEnabled,
t,
complianceConfirmed,
}),
},
];
};
@@ -35,6 +35,7 @@ const SubscriptionsTable = (subscriptionsData) => {
setPlanEnabled,
t,
enableEpay,
complianceConfirmed,
} = subscriptionsData;
const columns = useMemo(() => {
@@ -43,8 +44,9 @@ const SubscriptionsTable = (subscriptionsData) => {
openEdit,
setPlanEnabled,
enableEpay,
complianceConfirmed,
});
}, [t, openEdit, setPlanEnabled, enableEpay]);
}, [t, openEdit, setPlanEnabled, enableEpay, complianceConfirmed]);
const tableColumns = useMemo(() => {
return compactMode
+39 -3
View File
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useContext } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { Banner } from '@douyinfe/semi-ui';
import CardPro from '../../common/ui/CardPro';
import SubscriptionsTable from './SubscriptionsTable';
@@ -28,12 +28,14 @@ import { useSubscriptionsData } from '../../../hooks/subscriptions/useSubscripti
import { useIsMobile } from '../../../hooks/common/useIsMobile';
import { createCardProPagination } from '../../../helpers/utils';
import { StatusContext } from '../../../context/Status';
import { API } from '../../../helpers';
const SubscriptionsPage = () => {
const subscriptionsData = useSubscriptionsData();
const isMobile = useIsMobile();
const [statusState] = useContext(StatusContext);
const enableEpay = !!statusState?.status?.enable_online_topup;
const [complianceConfirmed, setComplianceConfirmed] = useState(true);
const {
showEdit,
@@ -47,6 +49,22 @@ const SubscriptionsPage = () => {
t,
} = subscriptionsData;
useEffect(() => {
const loadComplianceStatus = async () => {
try {
const res = await API.get('/api/user/topup/info');
if (res.data?.success) {
setComplianceConfirmed(
res.data.data?.payment_compliance_confirmed !== false,
);
}
} catch (error) {
// Keep the page usable if status loading fails; backend still enforces.
}
};
loadComplianceStatus();
}, []);
return (
<>
<AddEditSubscriptionModal
@@ -71,7 +89,11 @@ const SubscriptionsPage = () => {
<div className='flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full'>
{/* Mobile: actions first; Desktop: actions left */}
<div className='order-1 md:order-0 w-full md:w-auto'>
<SubscriptionsActions openCreate={openCreate} t={t} />
<SubscriptionsActions
openCreate={openCreate}
t={t}
disabled={!complianceConfirmed}
/>
</div>
<Banner
type='info'
@@ -94,7 +116,21 @@ const SubscriptionsPage = () => {
})}
t={t}
>
<SubscriptionsTable {...subscriptionsData} enableEpay={enableEpay} />
{!complianceConfirmed && (
<Banner
type='warning'
description={t(
'订阅套餐创建和变更已锁定,管理员需先在支付设置中确认合规声明。',
)}
closeIcon={null}
className='!rounded-lg mb-3'
/>
)}
<SubscriptionsTable
{...subscriptionsData}
enableEpay={enableEpay}
complianceConfirmed={complianceConfirmed}
/>
</CardPro>
</>
);
+12
View File
@@ -38,6 +38,7 @@ const InvitationCard = ({
setOpenTransfer,
affLink,
handleAffLinkClick,
complianceConfirmed = true,
}) => {
return (
<Card className='!rounded-2xl shadow-sm border-0'>
@@ -81,6 +82,7 @@ const InvitationCard = ({
theme='solid'
size='small'
disabled={
!complianceConfirmed ||
!userState?.user?.aff_quota ||
userState?.user?.aff_quota <= 0
}
@@ -91,6 +93,16 @@ const InvitationCard = ({
{t('划转到余额')}
</Button>
</div>
{!complianceConfirmed && (
<Text
style={{
color: 'rgba(255,255,255,0.8)',
fontSize: 12,
}}
>
{t('邀请奖励划转已禁用,管理员需先确认合规声明。')}
</Text>
)}
{/* 统计数据 */}
<div className='grid grid-cols-3 gap-6 mt-4'>
+58 -48
View File
@@ -96,6 +96,7 @@ const RechargeCard = ({
activeSubscriptions = [],
allSubscriptions = [],
reloadSubscriptionSelf,
enableRedemption = true,
}) => {
const onlineFormApiRef = useRef(null);
const redeemFormApiRef = useRef(null);
@@ -566,57 +567,66 @@ const RechargeCard = ({
</Card>
{/* 兑换码充值 */}
<Card
className='!rounded-xl w-full'
title={
<Text type='tertiary' strong>
{t('兑换码充值')}
</Text>
}
>
<Form
getFormApi={(api) => (redeemFormApiRef.current = api)}
initValues={{ redemptionCode: redemptionCode }}
{enableRedemption ? (
<Card
className='!rounded-xl w-full'
title={
<Text type='tertiary' strong>
{t('兑换码充值')}
</Text>
}
>
<Form.Input
field='redemptionCode'
noLabel={true}
placeholder={t('请输入兑换码')}
value={redemptionCode}
onChange={(value) => setRedemptionCode(value)}
prefix={<IconGift />}
suffix={
<div className='flex items-center gap-2'>
<Button
type='primary'
theme='solid'
onClick={topUp}
loading={isSubmitting}
>
{t('兑换额度')}
</Button>
</div>
}
showClear
style={{ width: '100%' }}
extraText={
topUpLink && (
<Text type='tertiary'>
{t('在找兑换码?')}
<Text
type='secondary'
underline
className='cursor-pointer'
onClick={openTopUpLink}
<Form
getFormApi={(api) => (redeemFormApiRef.current = api)}
initValues={{ redemptionCode: redemptionCode }}
>
<Form.Input
field='redemptionCode'
noLabel={true}
placeholder={t('请输入兑换码')}
value={redemptionCode}
onChange={(value) => setRedemptionCode(value)}
prefix={<IconGift />}
suffix={
<div className='flex items-center gap-2'>
<Button
type='primary'
theme='solid'
onClick={topUp}
loading={isSubmitting}
>
{t('购买兑换码')}
{t('兑换额度')}
</Button>
</div>
}
showClear
style={{ width: '100%' }}
extraText={
topUpLink && (
<Text type='tertiary'>
{t('在找兑换码?')}
<Text
type='secondary'
underline
className='cursor-pointer'
onClick={openTopUpLink}
>
{t('购买兑换码')}
</Text>
</Text>
</Text>
)
}
/>
</Form>
</Card>
)
}
/>
</Form>
</Card>
) : (
<Banner
type='warning'
description={t('兑换码功能已禁用,管理员需先确认合规声明。')}
closeIcon={null}
className='!rounded-xl'
/>
)}
</Space>
);
+13 -1
View File
@@ -110,6 +110,8 @@ const TopUp = () => {
const [topupInfo, setTopupInfo] = useState({
amount_options: [],
discount: {},
enable_redemption: true,
payment_compliance_confirmed: true,
});
const confirmPayMethods = [
@@ -645,7 +647,7 @@ const TopUp = () => {
? data.waffo_min_topup
: enableWaffoPancakeTopUp
? data.waffo_pancake_min_topup
: 1;
: 1;
setEnableOnlineTopUp(enableOnlineTopUp);
setEnableStripeTopUp(enableStripeTopUp);
setEnableCreemTopUp(enableCreemTopUp);
@@ -657,6 +659,14 @@ const TopUp = () => {
setMinTopUp(minTopUpValue);
setTopUpCount(minTopUpValue);
setTopUpLink(data.topup_link || '');
setTopupInfo((prev) => ({
...prev,
enable_redemption: data.enable_redemption !== false,
payment_compliance_confirmed:
data.payment_compliance_confirmed !== false,
payment_compliance_terms_version:
data.payment_compliance_terms_version || '',
}));
// 设置 Creem 产品
try {
@@ -984,6 +994,7 @@ const TopUp = () => {
activeSubscriptions={activeSubscriptions}
allSubscriptions={allSubscriptions}
reloadSubscriptionSelf={getSubscriptionSelf}
enableRedemption={topupInfo.enable_redemption !== false}
/>
<InvitationCard
t={t}
@@ -992,6 +1003,7 @@ const TopUp = () => {
setOpenTransfer={setOpenTransfer}
affLink={affLink}
handleAffLinkClick={handleAffLinkClick}
complianceConfirmed={topupInfo.payment_compliance_confirmed !== false}
/>
</div>
</div>