feat: streamline wallet checkout payment
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:
138
apps/web/src/app/propose/success/WalletCheckout.tsx
Normal file
138
apps/web/src/app/propose/success/WalletCheckout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user