lovart3/components/lovart/wechat-pricing-button.tsx
Your Name f874b3464f
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add wechat pay channel to unified billing flow
2026-02-12 13:32:13 +08:00

203 lines
5.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { Copy, Loader2, QrCode } from "lucide-react";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { useEffect, useMemo, useState } from "react";
import { cn } from "@/lib/utils";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
export function WechatPricingButton({
planId,
planName,
enabled,
}: {
planId: string;
planName: string;
enabled: boolean;
}) {
const { data: session } = useSession();
const router = useRouter();
const [isPending, setIsPending] = useState(false);
const [open, setOpen] = useState(false);
const [outTradeNo, setOutTradeNo] = useState<string | null>(null);
const [qrCodeImageUrl, setQrCodeImageUrl] = useState<string | null>(null);
const [codeUrl, setCodeUrl] = useState<string | null>(null);
const [statusText, setStatusText] = useState("请使用微信扫码完成支付");
const canPoll = useMemo(() => open && Boolean(outTradeNo), [open, outTradeNo]);
useEffect(() => {
if (!canPoll || !outTradeNo) {
return;
}
let timer: number | undefined;
let stopped = false;
const poll = async () => {
try {
const response = await fetch(`/api/billing/wechat/order-status?outTradeNo=${encodeURIComponent(outTradeNo)}`);
if (!response.ok) {
return;
}
const data = (await response.json()) as { status?: string };
if (stopped || !data.status) {
return;
}
if (data.status === "paid") {
setStatusText("支付成功,正在跳转...");
stopped = true;
setTimeout(() => {
router.push(`/lovart/pricing?checkout=success&plan=${encodeURIComponent(planId)}`);
}, 800);
return;
}
if (data.status !== "created" && data.status !== "processing") {
setStatusText(`当前状态:${data.status}`);
}
} catch {
// ignore polling failure
}
if (!stopped) {
timer = window.setTimeout(poll, 3000);
}
};
void poll();
return () => {
stopped = true;
if (timer) {
window.clearTimeout(timer);
}
};
}, [canPoll, outTradeNo, planId, router]);
async function handleCheckout() {
if (isPending) {
return;
}
if (!enabled) {
window.alert("微信支付尚未配置,请先配置微信支付环境变量。");
return;
}
if (!session?.user || session.user.type === "guest") {
router.push("/login?callbackUrl=/lovart/pricing");
return;
}
setIsPending(true);
try {
const response = await fetch("/api/billing/wechat/checkout", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ planId }),
});
const data = (await response.json()) as {
error?: string;
outTradeNo?: string;
qrCodeImageUrl?: string;
codeUrl?: string;
};
if (!response.ok || !data.outTradeNo || !data.qrCodeImageUrl || !data.codeUrl) {
window.alert(data.error || "创建微信支付订单失败,请稍后重试。");
return;
}
setOutTradeNo(data.outTradeNo);
setQrCodeImageUrl(data.qrCodeImageUrl);
setCodeUrl(data.codeUrl);
setStatusText("请使用微信扫码完成支付");
setOpen(true);
} catch (error) {
console.error("[WechatPricingButton] checkout error", error);
window.alert("创建微信支付订单失败,请稍后重试。");
} finally {
setIsPending(false);
}
}
async function handleCopyLink() {
if (!codeUrl) {
return;
}
try {
await navigator.clipboard.writeText(codeUrl);
window.alert("支付链接已复制");
} catch {
window.alert("复制失败,请手动复制");
}
}
return (
<>
<button
className={cn(
"inline-flex w-full items-center justify-center gap-2 rounded-xl border border-emerald-300/30 bg-emerald-500/10 px-4 py-2 text-sm font-semibold text-emerald-100 hover:border-emerald-300/60",
isPending && "cursor-not-allowed opacity-75"
)}
disabled={isPending}
onClick={handleCheckout}
type="button"
>
{isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <QrCode className="h-4 w-4" />}
微信支付 {planName}
</button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-sm border-white/10 bg-[#0a1120] text-zinc-100">
<DialogHeader>
<DialogTitle>微信扫码支付</DialogTitle>
<DialogDescription className="text-zinc-300">{statusText}</DialogDescription>
</DialogHeader>
<div className="space-y-3">
{qrCodeImageUrl ? (
<div className="mx-auto w-fit rounded-xl bg-white p-2">
<img alt="wechat pay qrcode" className="h-56 w-56" src={qrCodeImageUrl} />
</div>
) : (
<div className="mx-auto flex h-56 w-56 items-center justify-center rounded-xl border border-dashed border-white/20 text-sm text-zinc-400">
正在生成二维码...
</div>
)}
<div className="rounded-lg border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-zinc-300">
<p>订单号:{outTradeNo || "-"}</p>
</div>
<button
className="inline-flex w-full items-center justify-center gap-2 rounded-lg border border-white/20 px-3 py-2 text-xs text-zinc-100 hover:border-white/40"
onClick={handleCopyLink}
type="button"
>
<Copy className="h-3.5 w-3.5" />
复制支付链接
</button>
</div>
</DialogContent>
</Dialog>
</>
);
}