Files
gitlab-instance-0a899031_no…/app/page.tsx
2026-03-12 00:34:50 -05:00

372 lines
10 KiB
TypeScript
Raw 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 { useCallback, useEffect, useState } from "react";
import PocketBase from "pocketbase";
import { getPayEnv, getDeviceFromEnv } from "@/app/lib/payEnv";
import { getPaymentConfig } from "@/config/services.config";
import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
import { VipLayout } from "@/app/vip/components/VipLayout";
import { LandingPage } from "@/app/vip/components/LandingPage";
import { DashboardPage } from "@/app/vip/components/DashboardPage";
import styles from "@/app/vip/components/vip.module.css";
const PB_URL =
process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase.hackrobot.cn";
/** 微信内支付渠道zpay | xorpay */
const WECHAT_PAY_PROVIDER: "zpay" | "xorpay" = "xorpay";
function redirectToPayZpay(params: {
user_id: string;
return_url: string;
total_fee: number;
type: string;
channel: "alipay" | "wxpay";
device: "pc" | "h5" | "wechat";
}) {
const form = document.createElement("form");
form.method = "POST";
form.action = "/api/pay";
form.target = "_self";
form.style.display = "none";
const fields: [string, string][] = [
["user_id", params.user_id],
["return_url", params.return_url],
["total_fee", String(params.total_fee)],
["type", params.type],
["channel", params.channel],
["device", params.device],
["provider", "zpay"],
["html", "1"],
];
fields.forEach(([k, v]) => {
const input = document.createElement("input");
input.type = "hidden";
input.name = k;
input.value = v;
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
async function redirectToPayZpayH5(params: {
user_id: string;
return_url: string;
total_fee: number;
type: string;
channel: "alipay" | "wxpay";
}): Promise<string | null> {
const res = await fetch("/api/pay", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...params,
device: "h5",
provider: "zpay",
html: "0",
}),
});
const data = await res.json().catch(() => ({}));
if (!data?.ok || !data?.params || !data?.payUrl) {
alert(data?.error || "支付发起失败");
return null;
}
const orderId = data?.order_id?.trim() || null;
if (data?.redirect_url) {
window.open(data.redirect_url, "_blank");
return orderId;
}
const form = document.createElement("form");
form.method = "POST";
form.action = data.payUrl;
form.target = "_blank";
form.style.display = "none";
for (const [k, v] of Object.entries(data.params as Record<string, string>)) {
if (v == null) continue;
const input = document.createElement("input");
input.type = "hidden";
input.name = k;
input.value = String(v);
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
return orderId;
}
async function payWechatXorpay(params: {
user_id: string;
return_url: string;
total_fee: number;
type: string;
channel: "alipay" | "wxpay";
}) {
const res = await fetch("/api/pay", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...params,
total_fee: params.total_fee,
device: "wechat",
provider: "xorpay",
html: "0",
}),
});
const data = await res.json().catch(() => ({}));
if (!data?.ok || !data?.params || !data?.payUrl) {
alert(data?.error || "支付发起失败");
return;
}
const form = document.createElement("form");
form.method = "POST";
form.action = data.payUrl;
form.target = "_self";
form.style.display = "none";
for (const [k, v] of Object.entries(data.params as Record<string, string>)) {
if (v == null) continue;
const input = document.createElement("input");
input.type = "hidden";
input.name = k;
input.value = String(v);
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
}
function getStorage(key: string): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(key);
}
function setStorage(key: string, value: string): void {
if (typeof window === "undefined") return;
localStorage.setItem(key, value);
}
function removeStorage(key: string): void {
if (typeof window === "undefined") return;
localStorage.removeItem(key);
}
export default function Home() {
const [paidType, setPaidType] = useState<string | null>(null);
const [userName, setUserName] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [payPendingOrderId, setPayPendingOrderId] = useState<string | null>(null);
const savePaidType = useCallback((type: string | null) => {
if (type) setStorage("paidType", type);
else removeStorage("paidType");
setPaidType(type);
}, []);
const saveUserName = useCallback((name: string) => {
setStorage("userName", name);
setUserName(name);
}, []);
const getOrCreateUserId = useCallback(() => {
let userId = getStorage("userId");
if (userId?.startsWith("{") && userId.includes("data")) {
try {
const parsed = JSON.parse(userId) as { data?: string };
if (parsed?.data) {
userId = parsed.data;
setStorage("userId", userId);
}
} catch {
userId = null;
}
}
if (!userId) {
userId = `user${Date.now()}`;
setStorage("userId", userId);
}
return userId;
}, []);
const checkPaymentFromPB = useCallback(
async (userId: string): Promise<boolean> => {
try {
const pb = new PocketBase(PB_URL);
const records = await pb.collection("payments").getList(1, 1, {
filter: `user_id = "${userId}"`,
sort: "-created",
});
if (records.items.length > 0) {
const latestType = records.items[0].type as string;
const dbUserId = records.items[0].user_id as string;
setStorage("userId", dbUserId);
savePaidType(latestType);
saveUserName(dbUserId);
return latestType === "vip";
}
savePaidType(null);
return false;
} catch (err) {
console.error("查询 PocketBase 失败:", err);
return false;
}
},
[savePaidType, saveUserName]
);
const clearAllStorage = useCallback(() => {
try {
if (typeof window === "undefined") return;
const keys = ["userId", "paidType", "userName"];
keys.forEach((k) => {
try {
localStorage.removeItem(k);
} catch {
/* ignore */
}
});
setPaidType(null);
setUserName(null);
setLoading(false);
const path = window.location.pathname || "/";
window.location.replace(path);
} catch (error) {
console.error("清空存储失败:", error);
}
}, []);
const handlePay = useCallback(async () => {
const userId = getOrCreateUserId();
const returnUrl = `${window.location.origin}/paid`;
const env = getPayEnv();
const device = getDeviceFromEnv(env);
const totalFee = getPaymentConfig().defaultAmount ?? 8800;
const base = {
user_id: userId,
return_url: returnUrl,
total_fee: totalFee,
type: "vip",
channel: "wxpay" as const,
};
if (device === "wechat" && WECHAT_PAY_PROVIDER === "xorpay") {
payWechatXorpay(base);
} else if (device === "h5") {
const orderId = await redirectToPayZpayH5(base);
if (orderId) setPayPendingOrderId(orderId);
} else {
redirectToPayZpay({ ...base, device });
}
}, [getOrCreateUserId]);
const handleVerifySuccess = useCallback(() => {
window.location.reload();
}, []);
const env = getPayEnv();
const device = getDeviceFromEnv(env);
const isH5 = device === "h5";
usePayStatusPoll(
(orderId) => {
let uid = getStorage("userId");
if (!uid && orderId?.includes("_order_")) {
const prefix = orderId.split("_order_")[0];
const parts = prefix.split("_");
if (parts.length >= 2) uid = parts.slice(0, -1).join("_");
}
if (uid) saveUserName(uid);
savePaidType("vip");
if (isH5) setPayPendingOrderId(null);
window.location.reload();
},
() => {
if (isH5) setPayPendingOrderId(null);
},
isH5 ? payPendingOrderId : undefined
);
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("type")?.trim() === "clean") {
clearAllStorage();
return;
}
if (urlParams.get("paid") === "1") {
savePaidType("vip");
saveUserName(getStorage("userId") || "");
}
const init = async () => {
getOrCreateUserId();
const localType = getStorage("paidType");
const localUserName = getStorage("userName");
if (localType && localUserName) {
setPaidType(localType);
setUserName(localUserName);
setLoading(false);
return;
}
const userId = getStorage("userId");
if (userId) {
const found = await checkPaymentFromPB(userId);
if (found) {
setUserName(getStorage("userName"));
}
}
setLoading(false);
};
init();
}, [clearAllStorage, getOrCreateUserId, checkPaymentFromPB, savePaidType, saveUserName]);
useEffect(() => {
const syncFromStorage = () => {
const localType = getStorage("paidType");
const localUserName = getStorage("userName");
if (localType && localUserName && !paidType) {
setPaidType(localType);
setUserName(localUserName);
}
};
const onVisible = () => syncFromStorage();
const onPageShow = (e: PageTransitionEvent) => {
if (e.persisted) syncFromStorage();
};
if (typeof document !== "undefined" && document.addEventListener) {
document.addEventListener("visibilitychange", onVisible);
window.addEventListener("pageshow", onPageShow);
return () => {
document.removeEventListener("visibilitychange", onVisible);
window.removeEventListener("pageshow", onPageShow);
};
}
}, [paidType]);
const isLoggedIn = paidType === "vip";
if (loading) {
return (
<div className={styles.loading}>
<div className={styles.loadingSpinner} />
<p>...</p>
</div>
);
}
return (
<VipLayout isLoggedIn={isLoggedIn} userName={userName}>
{isLoggedIn ? (
<DashboardPage userName={userName} />
) : (
<LandingPage
onJoin={handlePay}
onVerify={checkPaymentFromPB}
onVerifySuccess={handleVerifySuccess}
/>
)}
</VipLayout>
);
}