feat: Add account center with crypto payment checkout and wallet management.

This commit is contained in:
2569718930@qq.com
2026-03-13 21:08:57 +08:00
parent f27970f157
commit db2f22509b
2 changed files with 60 additions and 16 deletions
+41 -9
View File
@@ -151,6 +151,10 @@ type ProviderSelection = {
mode: ProviderMode; mode: ProviderMode;
}; };
type ConnectBindOptions = {
openOverlayAfterBind?: boolean;
};
const WALLETCONNECT_PROJECT_ID = String( const WALLETCONNECT_PROJECT_ID = String(
process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "", process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "",
).trim(); ).trim();
@@ -988,12 +992,15 @@ export function AccountCenter() {
} }
}; };
const connectAndBindWallet = async (mode: ProviderMode = "auto") => { const connectAndBindWallet = async (
mode: ProviderMode = "auto",
options: ConnectBindOptions = {},
): Promise<boolean> => {
setPaymentError(""); setPaymentError("");
setPaymentInfo(""); setPaymentInfo("");
if (!isAuthenticated) { if (!isAuthenticated) {
setPaymentError("请先登录后再绑定钱包。"); setPaymentError("请先登录后再绑定钱包。");
return; return false;
} }
setPaymentBusy(true); setPaymentBusy(true);
@@ -1009,7 +1016,7 @@ export function AccountCenter() {
} catch (tokenErr) { } catch (tokenErr) {
setPaymentError(normalizePaymentError(tokenErr).message); setPaymentError(normalizePaymentError(tokenErr).message);
setPaymentBusy(false); setPaymentBusy(false);
return; return false;
} }
const authHeaders: Record<string, string> = { const authHeaders: Record<string, string> = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -1028,9 +1035,12 @@ export function AccountCenter() {
if (existingWallet) { if (existingWallet) {
setWalletAddress(address); setWalletAddress(address);
setSelectedWallet(address); setSelectedWallet(address);
setPaymentInfo(`${walletLabel} 已绑定: ${shortAddress(address)}`); setPaymentInfo(
`${walletLabel} 已绑定: ${shortAddress(address)}。现在可点击“立即订阅并激活服务”。`,
);
if (options.openOverlayAfterBind) setShowOverlay(true);
setPaymentBusy(false); setPaymentBusy(false);
return; return true;
} }
setWalletAddress(address); setWalletAddress(address);
@@ -1063,12 +1073,17 @@ export function AccountCenter() {
throw new Error(`verify failed: ${raw}`); throw new Error(`verify failed: ${raw}`);
} }
setPaymentInfo(`${walletLabel} 绑定成功: ${shortAddress(address)}`); setPaymentInfo(
`${walletLabel} 绑定成功: ${shortAddress(address)}。现在可点击“立即订阅并激活服务”。`,
);
setProviderMode(providerSelection.mode); setProviderMode(providerSelection.mode);
if (options.openOverlayAfterBind) setShowOverlay(true);
await loadPaymentSnapshot(); await loadPaymentSnapshot();
return true;
} catch (error) { } catch (error) {
setPaymentInfo(""); setPaymentInfo("");
setPaymentError(normalizePaymentError(error).message); setPaymentError(normalizePaymentError(error).message);
return false;
} finally { } finally {
setPaymentBusy(false); setPaymentBusy(false);
} }
@@ -1272,8 +1287,19 @@ export function AccountCenter() {
); );
if (!confirmRes.ok) { if (!confirmRes.ok) {
const raw = (await confirmRes.text()).slice(0, 350); const raw = (await confirmRes.text()).slice(0, 350);
setPaymentInfo(`交易已提交: ${shortAddress(txHashNorm)},等待确认中。`); const lowerRaw = raw.toLowerCase();
throw new Error(`confirm pending: ${raw}`); const maybePending =
(confirmRes.status === 404 &&
!lowerRaw.includes("payment intent not found")) ||
confirmRes.status === 408 ||
(confirmRes.status === 409 &&
(lowerRaw.includes("confirmations not enough") ||
lowerRaw.includes("tx indexed partially")));
if (maybePending) {
setPaymentInfo(`交易已提交: ${shortAddress(txHashNorm)},等待确认中。`);
throw new Error(`confirm pending: ${raw}`);
}
throw new Error(`confirm failed: ${raw}`);
} }
setPaymentInfo(`支付确认成功,交易: ${shortAddress(txHashNorm)}`); setPaymentInfo(`支付确认成功,交易: ${shortAddress(txHashNorm)}`);
@@ -1309,7 +1335,13 @@ export function AccountCenter() {
return; return;
} }
if (!hasPayingWallet) { if (!hasPayingWallet) {
await connectAndBindWallet(providerMode); setPaymentInfo("请先完成钱包绑定,正在拉起绑定流程...");
const bound = await connectAndBindWallet(providerMode, {
openOverlayAfterBind: true,
});
if (!bound) return;
setPaymentInfo("钱包已绑定,正在创建订单并发起支付...");
await createIntentAndPay();
return; return;
} }
await createIntentAndPay(); await createIntentAndPay();
+19 -7
View File
@@ -1559,12 +1559,27 @@ class PaymentContractCheckoutService:
raise PaymentCheckoutError(503, "cannot connect payment rpc") raise PaymentCheckoutError(503, "cannot connect payment rpc")
if int(w3.eth.chain_id) != int(self.chain_id): if int(w3.eth.chain_id) != int(self.chain_id):
raise PaymentCheckoutError(503, "payment rpc chain mismatch") raise PaymentCheckoutError(503, "payment rpc chain mismatch")
# Wait for receipt first to avoid transient RPC lag on eth_getTransaction.
receipt = self._wait_receipt(tx_hash_text)
if int(receipt.get("status") or 0) != 1:
raise PaymentCheckoutError(400, "tx reverted")
try: try:
tx = w3.eth.get_transaction(tx_hash_text) tx = w3.eth.get_transaction(tx_hash_text)
except Exception: except Exception:
raise PaymentCheckoutError(404, "tx not found on chain") tx = None
tx_to = _normalize_address(tx.get("to"))
tx_from = _normalize_address(tx.get("from")) tx_get = getattr(tx, "get", None)
tx_to_raw = tx_get("to") if callable(tx_get) else None
tx_from_raw = tx_get("from") if callable(tx_get) else None
tx_to = _normalize_address(
tx_to_raw or receipt.get("to")
)
tx_from = _normalize_address(
tx_from_raw or receipt.get("from")
)
if not tx_to or not tx_from:
raise PaymentCheckoutError(409, "tx indexed partially; retry confirm")
if tx_to != intent.receiver_address: if tx_to != intent.receiver_address:
raise PaymentCheckoutError( raise PaymentCheckoutError(
400, 400,
@@ -1578,9 +1593,6 @@ class PaymentContractCheckoutService:
) )
else: else:
self._require_user_wallet(user_id, tx_from) self._require_user_wallet(user_id, tx_from)
receipt = self._wait_receipt(tx_hash_text)
if int(receipt.get("status") or 0) != 1:
raise PaymentCheckoutError(400, "tx reverted")
block_number = int(receipt.get("blockNumber") or 0) block_number = int(receipt.get("blockNumber") or 0)
latest_block = int(w3.eth.block_number) latest_block = int(w3.eth.block_number)
confirmations = max(0, latest_block - block_number + 1) if block_number else 0 confirmations = max(0, latest_block - block_number + 1) if block_number else 0
@@ -1631,7 +1643,7 @@ class PaymentContractCheckoutService:
"block_number": block_number, "block_number": block_number,
"status": "confirmed", "status": "confirmed",
"raw_receipt": json.loads(Web3.to_json(receipt)), "raw_receipt": json.loads(Web3.to_json(receipt)),
"raw_tx": json.loads(Web3.to_json(tx)), "raw_tx": json.loads(Web3.to_json(tx)) if tx is not None else None,
"updated_at": now_iso, "updated_at": now_iso,
}, },
prefer="resolution=merge-duplicates,return=representation", prefer="resolution=merge-duplicates,return=representation",