feat: Implement a new contract-based payment system with wallet binding, payment intents, and subscription plan management.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
applyAuthResponseCookies,
|
||||
buildBackendRequestHeaders,
|
||||
} from "@/lib/backend-auth";
|
||||
|
||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
context: { params: Promise<{ intentId: string }> },
|
||||
) {
|
||||
if (!API_BASE) {
|
||||
return NextResponse.json(
|
||||
{ error: "POLYWEATHER_API_BASE_URL is not configured" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
const { intentId } = await context.params;
|
||||
try {
|
||||
const auth = await buildBackendRequestHeaders(req);
|
||||
const res = await fetch(
|
||||
`${API_BASE}/api/payments/intents/${encodeURIComponent(intentId)}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: auth.headers,
|
||||
cache: "no-store",
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
const raw = await res.text();
|
||||
const response = NextResponse.json(
|
||||
{ error: `Backend returned ${res.status}`, detail: raw.slice(0, 350) },
|
||||
{ status: res.status },
|
||||
);
|
||||
return applyAuthResponseCookies(response, auth.response);
|
||||
}
|
||||
const data = await res.json();
|
||||
const response = NextResponse.json(data);
|
||||
return applyAuthResponseCookies(response, auth.response);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch payment intent", detail: String(error) },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,14 @@ type CreatedIntent = {
|
||||
};
|
||||
};
|
||||
|
||||
type IntentStatusResponse = {
|
||||
intent?: {
|
||||
intent_id?: string;
|
||||
status?: string;
|
||||
tx_hash?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ethereum?: EvmProvider;
|
||||
@@ -398,7 +406,7 @@ function normalizePaymentError(error: unknown): NormalizedPaymentError {
|
||||
?.trim();
|
||||
const lower = String(rawMessage || "").toLowerCase();
|
||||
|
||||
if (lower.includes("confirm pending")) {
|
||||
if (lower.includes("confirm pending") || lower.includes("payment pending timeout")) {
|
||||
return {
|
||||
message: "链上交易已提交,正在确认中,请稍后刷新查看状态。",
|
||||
pending: true,
|
||||
@@ -940,6 +948,55 @@ export function AccountCenter() {
|
||||
throw new Error(`transaction confirmation timeout: ${txHash}`);
|
||||
};
|
||||
|
||||
const pollIntentUntilConfirmed = useCallback(
|
||||
async (
|
||||
intentId: string,
|
||||
authHeaders: Record<string, string>,
|
||||
txHashHint = "",
|
||||
timeoutMs = 180000,
|
||||
pollMs = 5000,
|
||||
) => {
|
||||
const startedAt = Date.now();
|
||||
const shortTx = shortAddress(txHashHint);
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const statusRes = await fetch(`/api/payments/intents/${intentId}`, {
|
||||
method: "GET",
|
||||
headers: authHeaders,
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!statusRes.ok) {
|
||||
if (statusRes.status >= 500 || statusRes.status === 429) {
|
||||
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
||||
continue;
|
||||
}
|
||||
const raw = (await statusRes.text()).slice(0, 260);
|
||||
throw new Error(`query intent failed: ${raw}`);
|
||||
}
|
||||
|
||||
const statusJson = (await statusRes.json()) as IntentStatusResponse;
|
||||
const intent = statusJson.intent || {};
|
||||
const status = String(intent.status || "").toLowerCase();
|
||||
const txHash = String(intent.tx_hash || txHashHint || "").toLowerCase();
|
||||
if (status === "confirmed") {
|
||||
setPaymentError("");
|
||||
setPaymentInfo(`支付确认成功,交易: ${shortAddress(txHash)}`);
|
||||
await loadSnapshot();
|
||||
await loadPaymentSnapshot();
|
||||
return;
|
||||
}
|
||||
if (status === "failed" || status === "cancelled" || status === "expired") {
|
||||
throw new Error(`payment ${status}`);
|
||||
}
|
||||
setPaymentInfo(
|
||||
`交易已提交: ${shortTx},正在链上确认(状态: ${status || "submitted"})...`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
||||
}
|
||||
throw new Error("payment pending timeout");
|
||||
},
|
||||
[loadPaymentSnapshot, loadSnapshot],
|
||||
);
|
||||
|
||||
const signBindMessage = async (
|
||||
eth: EvmProvider,
|
||||
address: string,
|
||||
@@ -1296,8 +1353,11 @@ export function AccountCenter() {
|
||||
(lowerRaw.includes("confirmations not enough") ||
|
||||
lowerRaw.includes("tx indexed partially")));
|
||||
if (maybePending) {
|
||||
setPaymentInfo(`交易已提交: ${shortAddress(txHashNorm)},等待确认中。`);
|
||||
throw new Error(`confirm pending: ${raw}`);
|
||||
setPaymentInfo(
|
||||
`交易已提交: ${shortAddress(txHashNorm)},等待链上确认中...`,
|
||||
);
|
||||
await pollIntentUntilConfirmed(intentId, authHeaders, txHashNorm);
|
||||
return;
|
||||
}
|
||||
throw new Error(`confirm failed: ${raw}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user