feat: Implement initial web frontend, comprehensive database management, and a bot-based weekly reward system.

This commit is contained in:
2569718930@qq.com
2026-03-13 16:47:25 +08:00
parent e780830f76
commit ac9e537070
10 changed files with 8261 additions and 43 deletions
+189 -27
View File
@@ -134,12 +134,34 @@ declare global {
type EvmProvider = {
request: (args: { method: string; params?: any[] | object }) => Promise<any>;
providers?: EvmProvider[];
connect?: (args?: any) => Promise<void>;
disconnect?: () => Promise<void>;
session?: unknown;
isMetaMask?: boolean;
isRabby?: boolean;
isOkxWallet?: boolean;
isBitKeep?: boolean;
};
type ProviderMode = "auto" | "walletconnect";
type ProviderSelection = {
provider: EvmProvider;
label: string;
mode: ProviderMode;
};
const WALLETCONNECT_PROJECT_ID = String(
process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "",
).trim();
const WALLETCONNECT_POLYGON_RPC_URL = String(
process.env.NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL ||
"https://polygon-bor-rpc.publicnode.com",
).trim();
let walletConnectProviderCache: EvmProvider | null = null;
let walletConnectProviderChainId: number | null = null;
// --- Helpers ---
type InfoRowProps = {
@@ -215,6 +237,61 @@ function getEvmWalletLabel(provider: EvmProvider | null): string {
return "EVM 钱包";
}
async function getWalletConnectProvider(
chainId: number,
rpcUrl: string,
): Promise<EvmProvider> {
if (!WALLETCONNECT_PROJECT_ID) {
throw new Error(
"WalletConnect 未配置:缺少 NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID。",
);
}
if (
walletConnectProviderCache &&
walletConnectProviderChainId === chainId
) {
return walletConnectProviderCache;
}
const { EthereumProvider } = await import(
"@walletconnect/ethereum-provider"
);
const rpcMap: Record<number, string> = {
[chainId]: rpcUrl || WALLETCONNECT_POLYGON_RPC_URL,
};
const origin =
typeof window !== "undefined"
? window.location.origin
: "https://polyweather-pro.vercel.app";
const provider = (await EthereumProvider.init({
projectId: WALLETCONNECT_PROJECT_ID,
chains: [chainId],
optionalChains: [chainId],
showQrModal: true,
methods: [
"eth_sendTransaction",
"personal_sign",
"eth_signTypedData",
"eth_signTypedData_v4",
"eth_sign",
"eth_call",
"eth_chainId",
"eth_accounts",
"eth_requestAccounts",
],
events: ["accountsChanged", "chainChanged", "disconnect"],
rpcMap,
metadata: {
name: "PolyWeather",
description: "PolyWeather Pro checkout",
url: origin,
icons: [`${origin}/favicon.ico`],
},
})) as unknown as EvmProvider;
walletConnectProviderCache = provider;
walletConnectProviderChainId = chainId;
return provider;
}
function toPaddedHex(value: bigint) {
return value.toString(16).padStart(64, "0");
}
@@ -366,6 +443,7 @@ export function AccountCenter() {
const [selectedPlanCode, setSelectedPlanCode] = useState("pro_monthly");
const [selectedTokenAddress, setSelectedTokenAddress] = useState("");
const [selectedWallet, setSelectedWallet] = useState("");
const [providerMode, setProviderMode] = useState<ProviderMode>("auto");
const [paymentBusy, setPaymentBusy] = useState(false);
const [paymentInfo, setPaymentInfo] = useState("");
const [paymentError, setPaymentError] = useState("");
@@ -373,6 +451,7 @@ export function AccountCenter() {
const [lastTxHash, setLastTxHash] = useState("");
const supabaseReady = hasSupabasePublicEnv();
const walletConnectEnabled = Boolean(WALLETCONNECT_PROJECT_ID);
/**
* Returns a valid access token, refreshing the session if the stored one
@@ -417,6 +496,45 @@ export function AccountCenter() {
[supabaseReady, getValidAccessToken],
);
const resolvePaymentProvider = useCallback(
async (mode: ProviderMode = "auto"): Promise<ProviderSelection> => {
const targetChainId = Number(paymentConfig?.chain_id || 137);
if (mode !== "walletconnect") {
const injected = getEvmProvider();
if (injected) {
return {
provider: injected,
label: getEvmWalletLabel(injected),
mode: "auto",
};
}
}
if (!walletConnectEnabled) {
throw new Error(
"未检测到浏览器扩展钱包,且 WalletConnect 未启用。请配置 NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID 或安装 EVM 钱包扩展。",
);
}
const wcProvider = await getWalletConnectProvider(
targetChainId,
WALLETCONNECT_POLYGON_RPC_URL,
);
const existingAccounts = (await wcProvider
.request({ method: "eth_accounts" })
.catch(() => [])) as string[];
if (!Array.isArray(existingAccounts) || existingAccounts.length === 0) {
if (typeof wcProvider.connect === "function") {
await wcProvider.connect({ chains: [targetChainId] });
}
}
return {
provider: wcProvider,
label: "WalletConnect",
mode: "walletconnect",
};
},
[paymentConfig?.chain_id, walletConnectEnabled],
);
const loadPaymentSnapshot = useCallback(async () => {
if (!backend?.authenticated) {
setPaymentConfig(null);
@@ -533,6 +651,15 @@ export function AccountCenter() {
};
const onSignOut = async () => {
if (walletConnectProviderCache?.disconnect) {
try {
await walletConnectProviderCache.disconnect();
} catch {
// ignore
}
walletConnectProviderCache = null;
walletConnectProviderChainId = null;
}
if (supabaseReady) {
try {
await getSupabaseBrowserClient().auth.signOut();
@@ -803,24 +930,20 @@ export function AccountCenter() {
}
};
const connectAndBindWallet = async () => {
const connectAndBindWallet = async (mode: ProviderMode = "auto") => {
setPaymentError("");
setPaymentInfo("");
if (!isAuthenticated) {
setPaymentError("请先登录后再绑定钱包。");
return;
}
const eth = getEvmProvider();
if (!eth) {
setPaymentError(
"未检测到浏览器钱包,请安装并启用 MetaMask / OKX Wallet / Rabby / Bitget 等 EVM 钱包扩展。",
);
return;
}
const walletLabel = getEvmWalletLabel(eth);
setPaymentBusy(true);
try {
const providerSelection = await resolvePaymentProvider(mode);
const eth = providerSelection.provider;
const walletLabel = providerSelection.label;
// Ensure we have a valid token BEFORE opening the wallet modal.
let accessToken: string;
try {
@@ -883,6 +1006,7 @@ export function AccountCenter() {
}
setPaymentInfo(`${walletLabel} 绑定成功: ${shortAddress(address)}`);
setProviderMode(providerSelection.mode);
await loadPaymentSnapshot();
} catch (error) {
setPaymentInfo("");
@@ -905,18 +1029,10 @@ export function AccountCenter() {
return;
}
const eth = getEvmProvider();
if (!eth) {
setPaymentError(
"未检测到浏览器钱包,请安装并启用 EVM 钱包扩展(MetaMask / OKX / Rabby 等)。",
);
return;
}
const payingWallet = String(
const fallbackWallet = String(
selectedWallet || walletAddress || boundWallets[0]?.address || "",
).toLowerCase();
if (!payingWallet) {
if (!fallbackWallet) {
setPaymentError("请先绑定钱包。");
return;
}
@@ -924,6 +1040,29 @@ export function AccountCenter() {
setPaymentBusy(true);
let approvedInThisRun = false;
try {
const providerSelection = await resolvePaymentProvider(providerMode);
const eth = providerSelection.provider;
const activeAccounts = (await eth.request({
method: "eth_requestAccounts",
})) as string[];
const activeAddress = String(activeAccounts?.[0] || "").toLowerCase();
if (!activeAddress) throw new Error("钱包账户为空");
const boundAddrSet = new Set(
boundWallets.map((row) => String(row.address || "").toLowerCase()),
);
if (boundAddrSet.size > 0 && !boundAddrSet.has(activeAddress)) {
throw new Error(
`当前连接钱包 ${shortAddress(activeAddress)} 未绑定,请先绑定该地址后支付。`,
);
}
const payingWallet = boundAddrSet.has(activeAddress)
? activeAddress
: fallbackWallet;
setSelectedWallet(payingWallet);
setProviderMode(providerSelection.mode);
// Ensure we have a valid token BEFORE switching chain / sending tx.
let accessToken: string;
try {
@@ -1112,7 +1251,7 @@ export function AccountCenter() {
return;
}
if (!hasPayingWallet) {
await connectAndBindWallet();
await connectAndBindWallet(providerMode);
return;
}
await createIntentAndPay();
@@ -1489,13 +1628,36 @@ export function AccountCenter() {
)}
</div>
<button
onClick={() => void connectAndBindWallet()}
disabled={paymentBusy || !isAuthenticated}
className="mt-6 w-full py-3 border border-white/10 bg-white/5 hover:bg-white/10 rounded-xl text-xs font-bold text-slate-300 transition-all flex items-center justify-center gap-2"
>
<PlusIcon className="w-4 h-4" /> (EVM)
</button>
<div className="mt-6 grid grid-cols-1 gap-2">
<button
onClick={() => {
setProviderMode("auto");
void connectAndBindWallet("auto");
}}
disabled={paymentBusy || !isAuthenticated}
className="w-full py-3 border border-white/10 bg-white/5 hover:bg-white/10 rounded-xl text-xs font-bold text-slate-300 transition-all flex items-center justify-center gap-2 disabled:opacity-60"
>
<PlusIcon className="w-4 h-4" /> EVM扩展
</button>
<button
onClick={() => {
setProviderMode("walletconnect");
void connectAndBindWallet("walletconnect");
}}
disabled={
paymentBusy || !isAuthenticated || !walletConnectEnabled
}
className="w-full py-3 border border-cyan-400/30 bg-cyan-500/10 hover:bg-cyan-500/20 rounded-xl text-xs font-bold text-cyan-300 transition-all flex items-center justify-center gap-2 disabled:opacity-60"
>
<CreditCard className="w-4 h-4" /> WalletConnect
</button>
{!walletConnectEnabled && (
<p className="text-[11px] text-slate-500">
WalletConnect
<code className="mx-1">NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID</code>
</p>
)}
</div>
</section>
</div>
</main>