feat: improve USDC wallet checkout clarity
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s

This commit is contained in:
OG T
2026-06-12 11:17:12 +08:00
parent 7b36c2496f
commit 0d547b792c
3 changed files with 141 additions and 39 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { Copy, ExternalLink, Wallet } from "lucide-react";
import { AlertTriangle, CheckCircle2, Copy, ExternalLink, QrCode, ShieldCheck, Wallet } from "lucide-react";
import { useState } from "react";
type WalletCheckoutProps = {
@@ -44,6 +44,12 @@ function CopyRow({ label, value, copyValue, copiedKey, copied, onCopy }: CopyRow
);
}
function shortAddress(value: string) {
if (!value) return "";
if (value.length <= 18) return value;
return `${value.slice(0, 10)}...${value.slice(-8)}`;
}
export function WalletCheckout({
amountUsdc,
network,
@@ -59,6 +65,19 @@ export function WalletCheckout({
const [copied, setCopied] = useState("");
const paymentMemo = `VibeWork Proposal ${proposalId}`;
const walletReady = Boolean(walletAddress);
const qrData = paymentUri || walletAddress;
const qrCodeUrl = qrData
? `https://api.qrserver.com/v1/create-qr-code/?size=220x220&margin=12&data=${encodeURIComponent(qrData)}`
: "";
const checkoutDetails = [
`VibeWork wallet checkout`,
`Amount: ${amountUsdc} USDC`,
`Network: ${network}`,
`Wallet: ${walletAddress}`,
`Chain ID: ${chainId}`,
`USDC token: ${tokenAddress}`,
`Memo: ${paymentMemo}`,
].join("\n");
const copyValue = async (key: string, value: string) => {
try {
@@ -71,26 +90,37 @@ export function WalletCheckout({
};
return (
<div className="rounded-md border border-emerald-400/20 bg-emerald-400/10 p-4">
<div className="rounded-lg border border-emerald-400/25 bg-emerald-400/10 p-4 shadow-2xl shadow-emerald-950/20 md:p-5">
<div className="mb-4 flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div className="flex items-start gap-3">
<Wallet className="mt-0.5 h-5 w-5 text-emerald-300" />
<div>
<h2 className="text-base font-semibold text-emerald-50">USDC wallet checkout</h2>
<p className="mt-1 text-sm leading-6 text-emerald-100/75">
使 Base USDC tx hash
tx hash Base USDC
</p>
</div>
</div>
{paymentUri && !capturedReference ? (
<a
href={paymentUri}
className="inline-flex h-11 items-center justify-center gap-2 rounded-md bg-emerald-300 px-4 text-sm font-semibold text-zinc-950 hover:bg-emerald-200"
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => copyValue("all", checkoutDetails)}
disabled={!walletReady}
className="inline-flex h-10 items-center justify-center gap-2 rounded-md border border-emerald-300/30 px-3 text-xs font-semibold text-emerald-100 hover:bg-emerald-300/10 disabled:cursor-not-allowed disabled:opacity-50"
>
<ExternalLink className="h-4 w-4" />
</a>
) : null}
<Copy className="h-4 w-4" />
{copied === "all" ? "已複製全部" : "複製付款資訊"}
</button>
{paymentUri && !capturedReference ? (
<a
href={paymentUri}
className="inline-flex h-10 items-center justify-center gap-2 rounded-md bg-emerald-300 px-3 text-xs font-semibold text-zinc-950 hover:bg-emerald-200"
>
<ExternalLink className="h-4 w-4" />
</a>
) : null}
</div>
</div>
{capturedReference ? (
@@ -99,36 +129,90 @@ export function WalletCheckout({
</div>
) : null}
<dl className="text-sm">
<CopyRow
label="Amount"
value={`${amountUsdc} USDC`}
copyValue={amountUsdc}
copiedKey="amount"
copied={copied}
onCopy={copyValue}
/>
<CopyRow label="Network" value={network} copiedKey="network" copied={copied} onCopy={copyValue} />
<CopyRow
label="Wallet"
value={walletReady ? walletAddress : `${walletLabel} is not configured yet`}
copyValue={walletAddress}
copiedKey="wallet"
copied={copied}
onCopy={copyValue}
/>
{walletReady ? (
<>
<CopyRow label="Chain ID" value={chainId} copiedKey="chain" copied={copied} onCopy={copyValue} />
<CopyRow label="USDC token" value={tokenAddress} copiedKey="token" copied={copied} onCopy={copyValue} />
<CopyRow label="Atomic amount" value={atomicAmount} copiedKey="atomic" copied={copied} onCopy={copyValue} />
<CopyRow label="Payment memo" value={paymentMemo} copiedKey="memo" copied={copied} onCopy={copyValue} />
{paymentUri ? (
<CopyRow label="Wallet link" value={paymentUri} copiedKey="uri" copied={copied} onCopy={copyValue} />
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_240px]">
<div className="rounded-md border border-emerald-300/15 bg-zinc-950/70 p-4">
<div className="grid gap-3 sm:grid-cols-3">
<div>
<div className="text-xs font-medium text-emerald-100/60"></div>
<div className="mt-1 text-2xl font-semibold text-emerald-200">{amountUsdc}</div>
<div className="text-xs text-emerald-100/60">USDC</div>
</div>
<div>
<div className="text-xs font-medium text-emerald-100/60"></div>
<div className="mt-2 text-sm font-semibold text-emerald-50">{network}</div>
<div className="text-xs text-emerald-100/60">Chain ID {chainId}</div>
</div>
<div>
<div className="text-xs font-medium text-emerald-100/60"></div>
<div className="mt-2 font-mono text-sm font-semibold text-emerald-50">
{walletReady ? shortAddress(walletAddress) : walletLabel}
</div>
<div className="text-xs text-emerald-100/60">Treasury</div>
</div>
</div>
<div className="mt-4 grid gap-2 text-xs leading-5 text-emerald-50/80 sm:grid-cols-3">
<div className="flex gap-2 rounded-md border border-emerald-300/10 bg-emerald-300/5 p-3">
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-emerald-300" />
<span> {amountUsdc} USDC</span>
</div>
<div className="flex gap-2 rounded-md border border-emerald-300/10 bg-emerald-300/5 p-3">
<ShieldCheck className="mt-0.5 h-4 w-4 shrink-0 text-emerald-300" />
<span> Base USDC</span>
</div>
<div className="flex gap-2 rounded-md border border-emerald-300/10 bg-emerald-300/5 p-3">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-200" />
<span> ETHUSDT token</span>
</div>
</div>
<dl className="mt-4 text-sm">
<CopyRow
label="Amount"
value={`${amountUsdc} USDC`}
copyValue={amountUsdc}
copiedKey="amount"
copied={copied}
onCopy={copyValue}
/>
<CopyRow label="Network" value={network} copiedKey="network" copied={copied} onCopy={copyValue} />
<CopyRow
label="Wallet"
value={walletReady ? walletAddress : `${walletLabel} is not configured yet`}
copyValue={walletAddress}
copiedKey="wallet"
copied={copied}
onCopy={copyValue}
/>
{walletReady ? (
<>
<CopyRow label="Chain ID" value={chainId} copiedKey="chain" copied={copied} onCopy={copyValue} />
<CopyRow label="USDC token" value={tokenAddress} copiedKey="token" copied={copied} onCopy={copyValue} />
<CopyRow label="Atomic amount" value={atomicAmount} copiedKey="atomic" copied={copied} onCopy={copyValue} />
<CopyRow label="Payment memo" value={paymentMemo} copiedKey="memo" copied={copied} onCopy={copyValue} />
{paymentUri ? (
<CopyRow label="Wallet link" value={paymentUri} copiedKey="uri" copied={copied} onCopy={copyValue} />
) : null}
</>
) : null}
</>
</dl>
</div>
{walletReady && qrCodeUrl ? (
<div className="rounded-md border border-emerald-300/15 bg-zinc-950/70 p-4">
<div className="mb-3 flex items-center gap-2 text-sm font-semibold text-emerald-50">
<QrCode className="h-4 w-4 text-emerald-300" />
</div>
<div className="rounded-md bg-white p-3">
<img src={qrCodeUrl} alt="VibeWork USDC payment QR code" className="mx-auto h-52 w-52" />
</div>
<p className="mt-3 text-xs leading-5 text-emerald-100/70">
</p>
</div>
) : null}
</dl>
</div>
{copied === "copy-failed" ? (
<p className="mt-3 text-xs text-amber-100"></p>

View File

@@ -218,6 +218,18 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
placeholder={`建議填VibeWork Proposal ${task?.id || taskId}`}
/>
</label>
<label className="flex gap-3 rounded-md border border-emerald-300/20 bg-emerald-300/10 p-3 text-xs leading-5 text-emerald-50">
<input
required
type="checkbox"
name="paymentConfirmed"
value="yes"
className="mt-1 h-4 w-4 shrink-0 accent-emerald-400"
/>
<span>
使 Base USDC VibeWork Treasury receipt paid conversion
</span>
</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"

View File

@@ -59,6 +59,7 @@ export async function submitWalletReceipt(formData: FormData) {
const network = cleanText(getString(formData, "network"), 80);
const amountCents = parseAmountCents(getString(formData, "amountUsdc"));
const note = cleanText(getString(formData, "note"), 500);
const paymentConfirmed = getString(formData, "paymentConfirmed") === "yes";
if (!taskId) {
throw new Error("缺少 Proposal ID。");
@@ -68,6 +69,10 @@ export async function submitWalletReceipt(formData: FormData) {
throw new Error("請提供有效的 tx hash 或付款 reference。");
}
if (!paymentConfirmed) {
throw new Error("請先確認已使用 Base USDC 轉帳正確金額到 VibeWork Treasury再提交 receipt。");
}
const task = await prisma.task.findUnique({
where: { id: taskId },
select: {
@@ -144,6 +149,7 @@ export async function submitWalletReceipt(formData: FormData) {
source,
campaign,
note: note || null,
payment_confirmed_by_proposer: paymentConfirmed,
idempotency_key: idempotencyKey,
already_verified: Boolean(existingCapture),
auto_verification: autoVerification,