diff --git a/frontend/components/account/account-copy.ts b/frontend/components/account/account-copy.ts index 9802cec4..484a7968 100644 --- a/frontend/components/account/account-copy.ts +++ b/frontend/components/account/account-copy.ts @@ -193,8 +193,8 @@ export function createAccountCopy(isEn: boolean): Record { ? "Your wallet needs a small amount of POL for gas fees; USDC alone may not complete authorization or payment. Please confirm your wallet is on Polygon network and keep some POL before paying." : "钱包里需要少量 POL 作为 gas 手续费;只有 USDC 可能无法完成授权或支付。请确认当前钱包在 Polygon 网络,并预留一点 POL 后再支付。", paymentManualDesc: isEn - ? "Option 2: Transfer directly to the platform's receiver contract without binding a wallet. After the transfer, submit the transaction hash (Tx Hash) and the system will verify and activate Pro automatically." - : "方式二:无需将钱包绑定到账号,直接向平台收款合约转账。转账完成后提交交易哈希(Tx Hash)系统会自动验签并开通 Pro。", + ? "Option 2: Transfer directly to the platform's receiver address without binding a wallet. After the transfer, submit the transaction hash (Tx Hash) and the system will verify and activate Pro automatically." + : "方式二:无需将钱包绑定到账号,直接向平台收款地址转账。转账完成后提交交易哈希(Tx Hash)系统会自动验签并开通 Pro。", paymentManualTitle: isEn ? "Manual Transfer (No Wallet Binding)" : "手动转账(无需绑定钱包)", diff --git a/frontend/components/account/usePaymentFlow.ts b/frontend/components/account/usePaymentFlow.ts index f544dadb..7c00e9ef 100644 --- a/frontend/components/account/usePaymentFlow.ts +++ b/frontend/components/account/usePaymentFlow.ts @@ -480,11 +480,13 @@ export function usePaymentFlow(params: UsePaymentFlowParams) { ) || latestTokens.find((token) => Number(token.chain_id || targetChainId) === targetChainId); if (selectedLatestToken?.supports_contract_checkout === false) { - throw new Error( + setPaymentInfo( isEn ? `${selectedLatestToken.chain_name || chainIdToDisplayName(targetChainId)} ${selectedLatestToken.symbol || "USDC"} supports manual transfer only.` - : `${selectedLatestToken.chain_name || chainIdToDisplayName(targetChainId)} ${selectedLatestToken.symbol || "USDC"} 仅支持手动转账。`, + : `${selectedLatestToken.chain_name || chainIdToDisplayName(targetChainId)} ${selectedLatestToken.symbol || "USDC"} 当前仅支持手动转账。`, ); + await createManualPaymentIntent(); + return; } const expectedReceiver = String(selectedLatestToken?.receiver_contract || latestConfig.receiver_contract || "").toLowerCase(); assertExpectedPaymentReceiver(expectedReceiver, "payment receiver contract"); diff --git a/src/payments/contract_checkout.py b/src/payments/contract_checkout.py index 0b7d8555..0bac0934 100644 --- a/src/payments/contract_checkout.py +++ b/src/payments/contract_checkout.py @@ -109,6 +109,21 @@ def _env_bool(name: str, default: bool = False) -> bool: return str(raw).strip().lower() in {"1", "true", "yes", "on"} +def _config_bool(value: Any, default: bool = False) -> bool: + if value is None: + return default + if isinstance(value, bool): + return value + if isinstance(value, (int, float)): + return bool(value) + text = str(value).strip().lower() + if text in {"1", "true", "yes", "on"}: + return True + if text in {"0", "false", "no", "off"}: + return False + return default + + def _env_int(name: str, default: int) -> int: raw = os.getenv(name) if raw is None: @@ -650,9 +665,22 @@ class PaymentContractCheckoutService: ) except Exception: confirmations = None - supports_direct_transfer = bool(row.get("supports_direct_transfer", True)) - supports_contract_checkout = bool( - row.get("supports_contract_checkout", row.get("supports_contract", chain_id == self.chain_id)) + supports_direct_transfer = _config_bool( + row.get("supports_direct_transfer"), + True, + ) + contract_support_raw = row.get( + "supports_contract_checkout", + row.get("supports_contract"), + ) + same_direct_receiver = bool( + receiver_contract + and direct_receiver_address + and receiver_contract == direct_receiver_address + ) + supports_contract_checkout = _config_bool( + contract_support_raw, + bool(chain_id == self.chain_id and receiver_contract and not same_direct_receiver), ) return PaymentTokenConfig( code=code, diff --git a/tests/test_direct_payment.py b/tests/test_direct_payment.py index 46fb677e..f55255dd 100644 --- a/tests/test_direct_payment.py +++ b/tests/test_direct_payment.py @@ -1,4 +1,10 @@ -from src.payments.contract_checkout import PaymentContractCheckoutService, PaymentIntentRecord +import pytest + +from src.payments.contract_checkout import ( + PaymentCheckoutError, + PaymentContractCheckoutService, + PaymentIntentRecord, +) ETHEREUM_USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" @@ -69,6 +75,50 @@ def test_config_payload_exposes_polygon_and_ethereum_payment_routes( assert eth_token["supports_contract_checkout"] is False +def test_matching_direct_receiver_disables_contract_checkout_by_default( + monkeypatch, tmp_path +): + _setup_multichain_env(monkeypatch, tmp_path) + service = PaymentContractCheckoutService() + + polygon_token = next( + row for row in service.get_config_payload()["tokens"] if row["chain_id"] == 137 + ) + + assert polygon_token["direct_receiver_address"] == RECEIVER.lower() + assert polygon_token["supports_direct_transfer"] is True + assert polygon_token["supports_contract_checkout"] is False + with pytest.raises(PaymentCheckoutError) as exc: + service.create_intent("user-1", "pro_monthly", payment_mode="strict") + assert exc.value.status_code == 400 + assert "manual transfer only" in exc.value.detail + + +def test_explicit_contract_checkout_support_overrides_matching_direct_receiver( + monkeypatch, tmp_path +): + _setup_multichain_env(monkeypatch, tmp_path) + monkeypatch.setenv( + "POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON", + ( + "[" + f'{{"code":"usdc_polygon","symbol":"USDC","name":"USDC on Polygon",' + f'"chain_id":137,"chain_code":"polygon","chain_name":"Polygon",' + f'"address":"{POLYGON_USDC}","decimals":6,' + f'"receiver_contract":"{RECEIVER}",' + f'"direct_receiver_address":"{RECEIVER}",' + f'"supports_contract_checkout":true,' + f'"supports_direct_transfer":true,"is_default":true}}' + "]" + ), + ) + service = PaymentContractCheckoutService() + + token = service.get_config_payload()["tokens"][0] + + assert token["supports_contract_checkout"] is True + + def test_create_direct_intent_can_target_ethereum_usdc(monkeypatch, tmp_path): _setup_multichain_env(monkeypatch, tmp_path) service = PaymentContractCheckoutService()