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

This commit is contained in:
OG T
2026-06-11 22:36:51 +08:00
parent ec7ac29cd1
commit 70d0dc0d4f
3 changed files with 156 additions and 61 deletions

View File

@@ -0,0 +1,138 @@
"use client";
import { Copy, ExternalLink, Wallet } from "lucide-react";
import { useState } from "react";
type WalletCheckoutProps = {
amountUsdc: string;
network: string;
walletAddress: string;
walletLabel: string;
chainId: string;
tokenAddress: string;
atomicAmount: string;
paymentUri: string;
proposalId: string;
capturedReference?: string;
};
type CopyRowProps = {
label: string;
value: string;
copyValue?: string;
copiedKey: string;
copied: string;
onCopy: (key: string, value: string) => void;
};
function CopyRow({ label, value, copyValue, copiedKey, copied, onCopy }: CopyRowProps) {
const valueToCopy = copyValue || value;
return (
<div className="grid gap-2 border-b border-emerald-300/10 py-3 last:border-b-0 md:grid-cols-[140px_1fr_auto] md:items-center">
<dt className="text-emerald-100/70">{label}</dt>
<dd className="break-all text-emerald-50">{value}</dd>
<button
type="button"
onClick={() => onCopy(copiedKey, valueToCopy)}
className="inline-flex h-8 items-center justify-center gap-1 rounded-md border border-emerald-300/30 px-2 text-xs font-semibold text-emerald-100 hover:bg-emerald-300/10"
>
<Copy className="h-3.5 w-3.5" />
{copied === copiedKey ? "已複製" : "複製"}
</button>
</div>
);
}
export function WalletCheckout({
amountUsdc,
network,
walletAddress,
walletLabel,
chainId,
tokenAddress,
atomicAmount,
paymentUri,
proposalId,
capturedReference,
}: WalletCheckoutProps) {
const [copied, setCopied] = useState("");
const paymentMemo = `VibeWork Proposal ${proposalId}`;
const walletReady = Boolean(walletAddress);
const copyValue = async (key: string, value: string) => {
try {
await navigator.clipboard.writeText(value);
setCopied(key);
window.setTimeout(() => setCopied((current) => (current === key ? "" : current)), 1600);
} catch {
setCopied("copy-failed");
}
};
return (
<div className="rounded-md border border-emerald-400/20 bg-emerald-400/10 p-4">
<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
</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"
>
<ExternalLink className="h-4 w-4" />
</a>
) : null}
</div>
{capturedReference ? (
<div className="mb-3 rounded-md border border-emerald-300/30 bg-emerald-300/10 px-3 py-2 text-sm text-emerald-100">
receipt{capturedReference}
</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} />
) : null}
</>
) : null}
</dl>
{copied === "copy-failed" ? (
<p className="mt-3 text-xs text-amber-100"></p>
) : null}
</div>
);
}

View File

@@ -10,8 +10,9 @@ import {
TREASURY_WALLET_LABEL,
usdcAtomicUnitsFromCents,
} from "@/lib/a2a-growth";
import { CheckCircle2, Copy, ExternalLink, Wallet } from "lucide-react";
import { CheckCircle2 } from "lucide-react";
import Link from "next/link";
import { WalletCheckout } from "./WalletCheckout";
export const dynamic = "force-dynamic";
@@ -138,65 +139,21 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
</div>
{payment === "wallet" ? (
<div className="mt-5 rounded-md border border-emerald-400/20 bg-emerald-400/10 p-4">
<div className="mb-3 flex items-center gap-2 text-sm font-semibold text-emerald-100">
<Wallet className="h-4 w-4" />
USDC wallet payment
</div>
<dl className="grid gap-3 text-sm">
<div>
<dt className="text-emerald-100/70">Amount</dt>
<dd className="mt-1 text-emerald-50">{formatUsdcAmount(proposalPackage.feeCents)} USDC</dd>
</div>
<div>
<dt className="text-emerald-100/70">Network</dt>
<dd className="mt-1 text-emerald-50">{TREASURY_USDC_NETWORK}</dd>
</div>
<div>
<dt className="text-emerald-100/70">Wallet</dt>
<dd className="mt-1 break-all text-emerald-50">
{TREASURY_USDC_ADDRESS || `${TREASURY_WALLET_LABEL} is not configured yet`}
</dd>
</div>
{TREASURY_USDC_ADDRESS ? (
<div>
<dt className="text-emerald-100/70">Chain / token</dt>
<dd className="mt-1 break-all text-emerald-50">
Chain ID {TREASURY_USDC_CHAIN_ID} · {TREASURY_USDC_TOKEN_ADDRESS}
</dd>
</div>
) : null}
{TREASURY_USDC_ADDRESS ? (
<div>
<dt className="text-emerald-100/70">USDC atomic amount</dt>
<dd className="mt-1 break-all text-emerald-50">{usdcAtomicUnitsFromCents(proposalPackage.feeCents)}</dd>
</div>
) : null}
{walletCaptured ? (
<div>
<dt className="text-emerald-100/70">Receipt</dt>
<dd className="mt-1 break-all text-emerald-50">{task?.stripe_payment_intent_id?.replace(/^wallet:/, "")}</dd>
</div>
) : null}
</dl>
{TREASURY_USDC_ADDRESS ? (
<div className="mt-3 inline-flex items-center gap-2 rounded-md border border-emerald-300/30 px-3 py-2 text-xs leading-5 text-emerald-100">
<Copy className="h-3.5 w-3.5" />
Proposal ID referral ledger
</div>
) : null}
{walletPaymentUri && !walletCaptured ? (
<a
href={walletPaymentUri}
className="mt-3 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"
>
<ExternalLink className="h-4 w-4" />
</a>
) : null}
<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="mt-4 rounded-md border border-zinc-700 bg-zinc-950/80 p-4">
<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
@@ -252,7 +209,7 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
<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="可填交易時間、付款人名稱或補充說明"
placeholder={`建議填VibeWork Proposal ${task?.id || taskId}`}
/>
</label>
<button

View File

@@ -82,7 +82,7 @@ export function sanitizeAgentId(value: string | null | undefined) {
}
export function getProposalPackage(id: string | null | undefined) {
return PROPOSAL_PACKAGES.find((item) => item.id === id) || PROPOSAL_PACKAGES[1];
return PROPOSAL_PACKAGES.find((item) => item.id === id) || PROPOSAL_PACKAGES[0];
}
export function buildDemandProposalUrl(params: {