feat: improve USDC wallet checkout clarity
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s
This commit is contained in:
@@ -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>請勿轉入 ETH、USDT 或其他 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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user