From 04fda60fb4ac29fddf8f0b5b25bffac0b65a896a Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 13 Mar 2026 11:51:01 +0800 Subject: [PATCH] feat: implement account center for user profile management, payments, and wallet binding. --- frontend/components/account/AccountCenter.tsx | 69 ++++++++++++++++--- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/frontend/components/account/AccountCenter.tsx b/frontend/components/account/AccountCenter.tsx index e835fb91..2bd1c200 100644 --- a/frontend/components/account/AccountCenter.tsx +++ b/frontend/components/account/AccountCenter.tsx @@ -246,24 +246,47 @@ export function AccountCenter() { const supabaseReady = hasSupabasePublicEnv(); + /** + * Returns a valid access token, refreshing the session if the stored one + * is missing or close to expiry. Throws if the user is not authenticated. + */ + const getValidAccessToken = useCallback(async (): Promise => { + if (!supabaseReady) throw new Error("Supabase 未配置,无法获取登录凭证。"); + const client = getSupabaseBrowserClient(); + // First try the cached session. + const { + data: { session: cached }, + } = await client.auth.getSession(); + const cachedToken = String(cached?.access_token || "").trim(); + if (cachedToken) return cachedToken; + // Session missing or expired — force a refresh. + const { + data: { session: refreshed }, + error, + } = await client.auth.refreshSession(); + const refreshedToken = String(refreshed?.access_token || "").trim(); + if (refreshedToken) return refreshedToken; + throw new Error( + error?.message + ? `登录会话已失效 (${error.message}),请退出后重新登录。` + : "登录会话已失效,请退出后重新登录。", + ); + }, [supabaseReady]); + const buildAuthedHeaders = useCallback( async (withJson = false): Promise> => { const headers: Record = {}; if (withJson) headers["Content-Type"] = "application/json"; if (!supabaseReady) return headers; try { - const client = getSupabaseBrowserClient(); - const { - data: { session }, - } = await client.auth.getSession(); - const accessToken = String(session?.access_token || "").trim(); - if (accessToken) headers.Authorization = `Bearer ${accessToken}`; + const token = await getValidAccessToken(); + headers.Authorization = `Bearer ${token}`; } catch { - // no-op + // Non-authenticated page load — silently skip. } return headers; }, - [supabaseReady], + [supabaseReady, getValidAccessToken], ); const loadPaymentSnapshot = useCallback(async () => { @@ -590,14 +613,26 @@ export function AccountCenter() { setPaymentBusy(true); try { + // Ensure we have a valid token BEFORE opening the wallet modal. + let accessToken: string; + try { + accessToken = await getValidAccessToken(); + } catch (tokenErr) { + setPaymentError(String(tokenErr)); + setPaymentBusy(false); + return; + } + const authHeaders: Record = { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }; + const accounts = (await eth.request({ method: "eth_requestAccounts", })) as string[]; const address = String(accounts?.[0] || "").toLowerCase(); if (!address) throw new Error("钱包账户为空"); - const authHeaders = await buildAuthedHeaders(true); - setWalletAddress(address); const challengeRes = await fetch("/api/payments/wallets/challenge", { method: "POST", @@ -667,7 +702,19 @@ export function AccountCenter() { setPaymentBusy(true); try { - const authHeaders = await buildAuthedHeaders(true); + // Ensure we have a valid token BEFORE switching chain / sending tx. + let accessToken: string; + try { + accessToken = await getValidAccessToken(); + } catch (tokenErr) { + setPaymentError(String(tokenErr)); + setPaymentBusy(false); + return; + } + const authHeaders: Record = { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }; const targetChainId = Number(paymentConfig.chain_id || 137); await ensureTargetChain(eth, targetChainId);