254 lines
11 KiB
TypeScript
254 lines
11 KiB
TypeScript
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}. 這筆費用是提案 intake、AI 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>
|
||
);
|
||
}
|