diff --git a/frontend/components/account/AccountCenter.tsx b/frontend/components/account/AccountCenter.tsx index 08ae5448..839de583 100644 --- a/frontend/components/account/AccountCenter.tsx +++ b/frontend/components/account/AccountCenter.tsx @@ -55,6 +55,9 @@ type AuthMeResponse = { auth_required?: boolean; subscription_required?: boolean; subscription_active?: boolean | null; + subscription_plan_code?: string | null; + subscription_starts_at?: string | null; + subscription_expires_at?: string | null; }; type PaymentPlan = { @@ -236,7 +239,8 @@ function buildBalanceOfCalldata(owner: string) { } function formatTokenUnits(amount: bigint, decimals: number) { - const safeDecimals = Number.isFinite(decimals) && decimals >= 0 ? Math.floor(decimals) : 6; + const safeDecimals = + Number.isFinite(decimals) && decimals >= 0 ? Math.floor(decimals) : 6; const base = 10n ** BigInt(safeDecimals); const whole = amount / base; const fraction = amount % base; @@ -303,7 +307,9 @@ function normalizePaymentError(error: unknown): NormalizedPaymentError { } const insufficientGas = - (code === -32000 && /insufficient funds/.test(lower) && /(gas|fee|native|pol|matic)/.test(lower)) || + (code === -32000 && + /insufficient funds/.test(lower) && + /(gas|fee|native|pol|matic)/.test(lower)) || /not enough pol|insufficient (pol|matic)|insufficient funds for gas|network fee|网络费|手续费/.test( lower, ); @@ -548,7 +554,15 @@ export function AccountCenter() { const initials = (displayName.slice(0, 2) || "PW").toUpperCase(); const joinedAt = formatTime(user?.created_at, "zh-CN"); const isSubscribed = Boolean(backend?.subscription_active); - const proExpiry = user?.user_metadata?.pro_expiry || "暂无 Pro 订阅"; + const expiryRaw = String( + backend?.subscription_expires_at || user?.user_metadata?.pro_expiry || "", + ).trim(); + const expiryFormatted = formatTime(expiryRaw, "zh-CN"); + const proExpiry = isSubscribed + ? expiryFormatted !== "--" + ? expiryFormatted + : expiryRaw || "已开通(待同步)" + : "暂无 Pro 订阅"; // Points Logic const backendPointsRaw = Number(backend?.points); @@ -609,7 +623,9 @@ export function AccountCenter() { : Number(paymentConfig?.token_decimals ?? 6), })); if (clean.length) return clean; - const fallbackAddress = String(paymentConfig?.token_address || "").toLowerCase(); + const fallbackAddress = String( + paymentConfig?.token_address || "", + ).toLowerCase(); if (!fallbackAddress.startsWith("0x")) return []; return [ { @@ -632,11 +648,14 @@ export function AccountCenter() { "", ).toLowerCase(); const selectedPaymentToken = - availableTokenList.find((row) => row.address === resolvedSelectedTokenAddress) || - availableTokenList[0]; + availableTokenList.find( + (row) => row.address === resolvedSelectedTokenAddress, + ) || availableTokenList[0]; const selectedTokenLabel = selectedPaymentToken?.symbol || - (resolvedSelectedTokenAddress.startsWith("0x") ? shortAddress(resolvedSelectedTokenAddress) : "USDC"); + (resolvedSelectedTokenAddress.startsWith("0x") + ? shortAddress(resolvedSelectedTokenAddress) + : "USDC"); const paymentFeatureReady = Boolean( paymentConfig?.enabled && paymentConfig?.configured, ); @@ -1295,17 +1314,24 @@ export function AccountCenter() { 会员权限详情 - + +
@@ -1423,7 +1449,9 @@ export function AccountCenter() { : "bg-white/5 border-white/10 text-slate-400 hover:bg-white/10" }`} > -
{token.symbol}
+
+ {token.symbol} +
{token.name}
diff --git a/src/auth/supabase_entitlement.py b/src/auth/supabase_entitlement.py index 3f385561..704348be 100644 --- a/src/auth/supabase_entitlement.py +++ b/src/auth/supabase_entitlement.py @@ -144,23 +144,29 @@ class SupabaseEntitlementService: logger.warning(f"supabase auth user check failed: {exc}") return None - def _query_active_subscription(self, user_id: str) -> bool: + def _query_latest_active_subscription( + self, + user_id: str, + ) -> Optional[Dict[str, object]]: if not user_id: - return False + return None if not self.service_role_key: logger.warning("SUPABASE_SERVICE_ROLE_KEY is missing") - return False + return None now_ts = time.time() with self._sub_cache_lock: cached = self._sub_cache.get(user_id) if cached and now_ts - float(cached.get("ts") or 0) < self.sub_cache_ttl_sec: - return bool(cached.get("active")) + row = cached.get("row") + if isinstance(row, dict): + return row + return None try: now_iso = datetime.now(timezone.utc).isoformat() params = { - "select": "id,user_id,status,expires_at", + "select": "id,user_id,status,plan_code,starts_at,expires_at", "user_id": f"eq.{user_id}", "status": "eq.active", "expires_at": f"gt.{now_iso}", @@ -179,20 +185,35 @@ class SupabaseEntitlementService: user_id, response.status_code, ) - active = False + row = None else: data = response.json() if response.content else [] - active = isinstance(data, list) and len(data) > 0 + row = data[0] if isinstance(data, list) and data else None + if not isinstance(row, dict): + row = None with self._sub_cache_lock: self._sub_cache[user_id] = { - "active": active, + "active": bool(row), + "row": row, "ts": now_ts, } - return active + return row except Exception as exc: logger.warning(f"supabase subscription query error user_id={user_id}: {exc}") - return False + return None + + def _query_active_subscription(self, user_id: str) -> bool: + return self._query_latest_active_subscription(user_id) is not None + + def get_latest_active_subscription( + self, + user_id: str, + respect_requirement: bool = True, + ) -> Optional[Dict[str, object]]: + if respect_requirement and not self.require_subscription: + return None + return self._query_latest_active_subscription(user_id) def has_active_subscription( self, diff --git a/web/app.py b/web/app.py index 4c9269c2..a82621ce 100644 --- a/web/app.py +++ b/web/app.py @@ -1155,14 +1155,25 @@ async def auth_me(request: Request): and SUPABASE_ENTITLEMENT.require_subscription ) subscription_active = None + subscription_plan_code = None + subscription_starts_at = None + subscription_expires_at = None if SUPABASE_ENTITLEMENT.enabled and user_id: try: - subscription_active = SUPABASE_ENTITLEMENT.has_active_subscription( + latest_subscription = SUPABASE_ENTITLEMENT.get_latest_active_subscription( user_id, respect_requirement=False, ) + subscription_active = bool(latest_subscription) + if isinstance(latest_subscription, dict): + subscription_plan_code = latest_subscription.get("plan_code") + subscription_starts_at = latest_subscription.get("starts_at") + subscription_expires_at = latest_subscription.get("expires_at") except Exception: subscription_active = None + subscription_plan_code = None + subscription_starts_at = None + subscription_expires_at = None points = _resolve_auth_points(request) weekly_profile = _resolve_weekly_profile(request) @@ -1186,6 +1197,9 @@ async def auth_me(request: Request): "auth_required": bool(SUPABASE_ENTITLEMENT.enabled and _SUPABASE_AUTH_REQUIRED), "subscription_required": subscription_required, "subscription_active": subscription_active, + "subscription_plan_code": subscription_plan_code, + "subscription_starts_at": subscription_starts_at, + "subscription_expires_at": subscription_expires_at, }