Files
agent-bounty-protocol/apps/web/src/app/propose/success/page.tsx
OG T c8ab251669
All checks were successful
CI and Production Smoke / smoke (push) Successful in 8s
feat: auto-verify USDC wallet receipts
2026-06-12 00:43:42 +08:00

254 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { prisma } from "@/lib/prisma";
import { submitWalletReceipt } from "@/app/propose/wallet/actions";
import {
buildUsdcPaymentUri,
getProposalPackage,
TREASURY_USDC_ADDRESS,
TREASURY_USDC_CHAIN_ID,
TREASURY_USDC_NETWORK,
TREASURY_USDC_TOKEN_ADDRESS,
TREASURY_WALLET_LABEL,
usdcAtomicUnitsFromCents,
} from "@/lib/a2a-growth";
import { CheckCircle2 } from "lucide-react";
import Link from "next/link";
import { WalletCheckout } from "./WalletCheckout";
export const dynamic = "force-dynamic";
type SearchParams = Promise<Record<string, string | string[] | undefined>>;
function getParam(params: Record<string, string | string[] | undefined>, key: string) {
const value = params[key];
return Array.isArray(value) ? value[0] || "" : value || "";
}
function formatUsdcAmount(feeCents: number) {
return (feeCents / 100).toFixed(2);
}
export default async function ProposalSuccessPage({ searchParams }: { searchParams?: SearchParams }) {
const params = searchParams ? await searchParams : {};
const taskId = getParam(params, "task_id");
const payment = getParam(params, "payment") || "stripe";
const receiptStatus = getParam(params, "receipt");
const proposalPackage = getProposalPackage(getParam(params, "package"));
const walletPaymentUri = buildUsdcPaymentUri(proposalPackage.feeCents);
const task = taskId
? await prisma.task.findUnique({
where: { id: taskId },
select: {
id: true,
title: true,
status: true,
reward_amount: true,
reward_currency: true,
referred_by_agent: true,
stripe_payment_intent_id: true,
},
})
: null;
const latestWalletReceipt = taskId
? await prisma.auditEvent.findFirst({
where: {
action: "DEMAND_PROPOSAL_WALLET_RECEIPT_SUBMITTED",
entityType: "TASK",
entityId: taskId,
},
orderBy: { createdAt: "desc" },
select: {
metadata: true,
createdAt: true,
},
})
: null;
const stripeCaptured = payment === "stripe" && Boolean(task?.stripe_payment_intent_id);
const walletCaptured = payment === "wallet" && Boolean(task?.stripe_payment_intent_id?.startsWith("wallet:"));
const receiptMetadata =
typeof latestWalletReceipt?.metadata === "object" &&
latestWalletReceipt.metadata !== null &&
!Array.isArray(latestWalletReceipt.metadata)
? (latestWalletReceipt.metadata as Record<string, unknown>)
: {};
const latestReceiptReference =
typeof receiptMetadata.raw_payment_reference === "string"
? receiptMetadata.raw_payment_reference
: typeof receiptMetadata.payment_reference === "string"
? receiptMetadata.payment_reference.replace(/^wallet:/, "")
: "";
return (
<main className="min-h-screen bg-zinc-950 px-5 py-10 text-zinc-100">
<section className="mx-auto max-w-3xl">
<Link href="/" className="text-sm font-medium text-zinc-400 hover:text-white">
VibeWork AI
</Link>
<div className="mt-8 rounded-lg border border-zinc-800 bg-zinc-900/80 p-6 shadow-2xl shadow-black/30">
<div className="mb-5 flex items-center gap-3">
<CheckCircle2 className="h-7 w-7 text-emerald-300" />
<div>
<p className="text-sm font-medium text-emerald-200">
{payment === "wallet"
? walletCaptured
? "USDC 付款已確認"
: "待 USDC 轉帳確認"
: stripeCaptured
? "提案付款已確認"
: "付款返回成功,等待 webhook 入帳確認"}
</p>
<h1 className="text-2xl font-semibold text-white"> VibeWork private draft</h1>
</div>
</div>
{task ? (
<dl className="grid gap-4 rounded-md border border-zinc-800 bg-zinc-950 p-4 text-sm md:grid-cols-2">
<div>
<dt className="text-zinc-500">Proposal ID</dt>
<dd className="mt-1 break-all text-zinc-100">{task.id}</dd>
</div>
<div>
<dt className="text-zinc-500">Status</dt>
<dd className="mt-1 text-zinc-100">{task.status}</dd>
</div>
<div>
<dt className="text-zinc-500">Estimated bounty budget</dt>
<dd className="mt-1 text-zinc-100">
${(task.reward_amount / 100).toFixed(2)} {task.reward_currency}
</dd>
</div>
<div>
<dt className="text-zinc-500">Referral Agent</dt>
<dd className="mt-1 break-all text-zinc-100">{task.referred_by_agent || "direct"}</dd>
</div>
</dl>
) : (
<div className="rounded-md border border-amber-400/30 bg-amber-400/10 px-4 py-3 text-sm text-amber-100">
proposal draft使
</div>
)}
<div className="mt-5 rounded-md border border-sky-400/20 bg-sky-400/10 p-4">
<p className="text-sm font-semibold text-sky-100">{proposalPackage.name}</p>
<p className="mt-1 text-sm leading-6 text-sky-100/80">
Routing fee: {proposalPackage.label}. intakeAI scoping referral attribution bounty payout
</p>
<p className="mt-2 text-sm leading-6 text-sky-100/70">
bounty
</p>
</div>
{payment === "wallet" ? (
<div className="mt-5 grid gap-4">
<WalletCheckout
amountUsdc={formatUsdcAmount(proposalPackage.feeCents)}
network={TREASURY_USDC_NETWORK}
walletAddress={TREASURY_USDC_ADDRESS}
walletLabel={TREASURY_WALLET_LABEL}
chainId={TREASURY_USDC_CHAIN_ID}
tokenAddress={TREASURY_USDC_TOKEN_ADDRESS}
atomicAmount={usdcAtomicUnitsFromCents(proposalPackage.feeCents)}
paymentUri={walletPaymentUri}
proposalId={task?.id || taskId}
capturedReference={walletCaptured ? task?.stripe_payment_intent_id?.replace(/^wallet:/, "") : ""}
/>
{!walletCaptured ? (
<div className="rounded-md border border-zinc-700 bg-zinc-950/80 p-4">
<h2 className="text-sm font-semibold text-white"> receipt</h2>
<p className="mt-1 text-xs leading-5 text-zinc-400">
tx hash paid conversion referral ledger
</p>
{receiptStatus === "submitted" || latestWalletReceipt ? (
<div className="mt-3 rounded-md border border-amber-300/30 bg-amber-300/10 px-3 py-2 text-xs leading-5 text-amber-100">
wallet receipt
{latestReceiptReference ? `${latestReceiptReference}` : ""} VibeWork
</div>
) : null}
{receiptStatus === "auto_verified" ? (
<div className="mt-3 rounded-md border border-emerald-300/30 bg-emerald-300/10 px-3 py-2 text-xs leading-5 text-emerald-100">
Base USDC paid conversion
</div>
) : null}
{receiptStatus === "already_verified" ? (
<div className="mt-3 rounded-md border border-emerald-300/30 bg-emerald-300/10 px-3 py-2 text-xs leading-5 text-emerald-100">
wallet receipt
</div>
) : null}
<form action={submitWalletReceipt} className="mt-4 grid gap-3">
<input type="hidden" name="taskId" value={task?.id || taskId} />
<input type="hidden" name="amountUsdc" value={formatUsdcAmount(proposalPackage.feeCents)} />
<label className="grid gap-2 text-xs font-medium text-zinc-300">
Tx hash / payment reference
<input
required
name="paymentReference"
defaultValue={latestReceiptReference}
className="h-10 rounded-md border border-zinc-700 bg-black px-3 text-sm text-white outline-none focus:border-emerald-400"
placeholder="0x... 或交易 reference"
/>
</label>
<div className="grid gap-3 md:grid-cols-2">
<label className="grid gap-2 text-xs font-medium text-zinc-300">
Payer wallet
<input
name="payerWallet"
className="h-10 rounded-md border border-zinc-700 bg-black px-3 text-sm text-white outline-none focus:border-emerald-400"
placeholder="0x..."
/>
</label>
<label className="grid gap-2 text-xs font-medium text-zinc-300">
Network
<input
name="network"
defaultValue={TREASURY_USDC_NETWORK}
className="h-10 rounded-md border border-zinc-700 bg-black px-3 text-sm text-white outline-none focus:border-emerald-400"
placeholder="Base / Ethereum / Polygon"
/>
</label>
</div>
<label className="grid gap-2 text-xs font-medium text-zinc-300">
Note
<input
name="note"
className="h-10 rounded-md border border-zinc-700 bg-black px-3 text-sm text-white outline-none focus:border-emerald-400"
placeholder={`建議填VibeWork Proposal ${task?.id || taskId}`}
/>
</label>
<button
type="submit"
className="inline-flex h-10 items-center justify-center rounded-md bg-emerald-400 px-4 text-sm font-semibold text-zinc-950 hover:bg-emerald-300"
>
receipt
</button>
</form>
</div>
) : null}
</div>
) : null}
<div className="mt-6 flex flex-wrap gap-3">
{task ? (
<Link
href={`/tasks/${task.id}`}
className="inline-flex h-11 items-center justify-center rounded-md border border-zinc-700 px-4 text-sm font-medium text-zinc-100 hover:border-sky-400"
>
draft
</Link>
) : null}
<Link
href="/propose"
className="inline-flex h-11 items-center justify-center rounded-md bg-sky-400 px-4 text-sm font-semibold text-zinc-950 hover:bg-sky-300"
>
</Link>
</div>
</div>
</section>
</main>
);
}