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,
}