@@ -2287,7 +2378,11 @@ export function AccountCenter() {
- {isSubscribed ? copy.proMember : copy.freeTier}
+ {isSubscribed
+ ? isTrialPlan
+ ? copy.trialBadge
+ : copy.proMember
+ : copy.freeTier}
diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css
index 1022dae7..0c7892a4 100644
--- a/frontend/components/dashboard/Dashboard.module.css
+++ b/frontend/components/dashboard/Dashboard.module.css
@@ -2654,6 +2654,35 @@
transform: translateY(-1px);
}
+.root :global(.account-renew-badge) {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px 10px;
+ border-radius: 999px;
+ border: 1px solid rgba(245, 158, 11, 0.34);
+ background: rgba(245, 158, 11, 0.1);
+ color: #fbbf24;
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.01em;
+ text-decoration: none;
+ transition: var(--transition);
+}
+
+.root :global(.account-renew-badge:hover) {
+ background: rgba(245, 158, 11, 0.18);
+ border-color: rgba(245, 158, 11, 0.6);
+ color: #fff6d5;
+ transform: translateY(-1px);
+}
+
+.root :global(.account-renew-badge.expired) {
+ border-color: rgba(239, 68, 68, 0.34);
+ background: rgba(239, 68, 68, 0.12);
+ color: #fca5a5;
+}
+
.root :global(.info-btn) {
background: rgba(99, 102, 241, 0.1);
border: 1px solid rgba(99, 102, 241, 0.3);
diff --git a/frontend/components/dashboard/HeaderBar.tsx b/frontend/components/dashboard/HeaderBar.tsx
index 5e1b3b58..4ba181e5 100644
--- a/frontend/components/dashboard/HeaderBar.tsx
+++ b/frontend/components/dashboard/HeaderBar.tsx
@@ -12,6 +12,20 @@ import {
hasSupabasePublicEnv,
} from "@/lib/supabase/client";
+function parseExpiryInfo(raw?: string | null) {
+ const text = String(raw || "").trim();
+ if (!text) return null;
+ const dt = new Date(text);
+ if (Number.isNaN(dt.getTime())) return null;
+ const diffMs = dt.getTime() - Date.now();
+ const daysLeft = Math.ceil(diffMs / 86_400_000);
+ return {
+ date: dt,
+ daysLeft,
+ expired: diffMs <= 0,
+ };
+}
+
export function HeaderBar() {
const store = useDashboardStore();
const { locale, setLocale, t } = useI18n();
@@ -56,6 +70,36 @@ export function HeaderBar() {
const accountAria = isAuthenticated
? t("header.accountAria")
: t("header.signInAria");
+ const expiryInfo = parseExpiryInfo(store.proAccess.subscriptionExpiresAt);
+ const isTrialPlan = /trial/i.test(
+ String(store.proAccess.subscriptionPlanCode || ""),
+ );
+ const showRenewReminder =
+ isAuthenticated &&
+ !store.proAccess.loading &&
+ (
+ (store.proAccess.subscriptionActive &&
+ expiryInfo &&
+ expiryInfo.daysLeft <= 3) ||
+ (!store.proAccess.subscriptionActive && Boolean(expiryInfo))
+ );
+ const renewReminderLabel = !showRenewReminder
+ ? ""
+ : !store.proAccess.subscriptionActive
+ ? isTrialPlan
+ ? locale === "en-US"
+ ? "Trial ended"
+ : "试用已结束"
+ : locale === "en-US"
+ ? "Pro expired"
+ : "Pro 已到期"
+ : isTrialPlan
+ ? locale === "en-US"
+ ? `Trial ${Math.max(expiryInfo?.daysLeft || 0, 0)}d left`
+ : `试用剩余 ${Math.max(expiryInfo?.daysLeft || 0, 0)} 天`
+ : locale === "en-US"
+ ? `Pro ${Math.max(expiryInfo?.daysLeft || 0, 0)}d left`
+ : `Pro 还剩 ${Math.max(expiryInfo?.daysLeft || 0, 0)} 天`;
return (
@@ -101,6 +145,20 @@ export function HeaderBar() {
{accountLabel}
+ {showRenewReminder ? (
+
+ {renewReminderLabel}
+
+ ) : null}
+
{t("header.live")}
diff --git a/frontend/hooks/useDashboardStore.tsx b/frontend/hooks/useDashboardStore.tsx
index 73863317..272b615e 100644
--- a/frontend/hooks/useDashboardStore.tsx
+++ b/frontend/hooks/useDashboardStore.tsx
@@ -72,6 +72,8 @@ function getInitialProAccessState(): ProAccessState {
loading: true,
authenticated: false,
subscriptionActive: false,
+ subscriptionPlanCode: null,
+ subscriptionExpiresAt: null,
points: 0,
error: null,
};
@@ -407,12 +409,16 @@ export function DashboardStoreProvider({
const payload = (await response.json()) as {
authenticated?: boolean;
subscription_active?: boolean | null;
+ subscription_plan_code?: string | null;
+ subscription_expires_at?: string | null;
points?: number;
};
setProAccess({
loading: false,
authenticated: Boolean(payload.authenticated),
subscriptionActive: payload.subscription_active === true,
+ subscriptionPlanCode: payload.subscription_plan_code ?? null,
+ subscriptionExpiresAt: payload.subscription_expires_at ?? null,
points: payload.points ?? 0,
error: null,
});
@@ -421,6 +427,8 @@ export function DashboardStoreProvider({
loading: false,
authenticated: false,
subscriptionActive: false,
+ subscriptionPlanCode: null,
+ subscriptionExpiresAt: null,
points: 0,
error: String(error),
});
diff --git a/frontend/lib/dashboard-types.ts b/frontend/lib/dashboard-types.ts
index e6086cce..56f8d552 100644
--- a/frontend/lib/dashboard-types.ts
+++ b/frontend/lib/dashboard-types.ts
@@ -436,6 +436,8 @@ export interface ProAccessState {
loading: boolean;
authenticated: boolean;
subscriptionActive: boolean;
+ subscriptionPlanCode: string | null;
+ subscriptionExpiresAt: string | null;
points: number;
error: string | null;
}
diff --git a/src/auth/supabase_entitlement.py b/src/auth/supabase_entitlement.py
index 68053718..9bd127a4 100644
--- a/src/auth/supabase_entitlement.py
+++ b/src/auth/supabase_entitlement.py
@@ -420,6 +420,12 @@ class SupabaseEntitlementService:
return None
return self._query_latest_active_subscription(user_id)
+ def get_latest_subscription_any_status(
+ self,
+ user_id: str,
+ ) -> Optional[Dict[str, object]]:
+ return self._query_latest_subscription_any_status(user_id)
+
def has_active_subscription(
self,
user_id: str,
diff --git a/web/routes.py b/web/routes.py
index 5973de2f..18017b54 100644
--- a/web/routes.py
+++ b/web/routes.py
@@ -333,11 +333,16 @@ async def auth_me(request: Request):
user_id,
respect_requirement=False,
)
+ latest_known_subscription = latest_subscription
+ if not latest_known_subscription:
+ latest_known_subscription = (
+ SUPABASE_ENTITLEMENT.get_latest_subscription_any_status(user_id)
+ )
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")
+ if isinstance(latest_known_subscription, dict):
+ subscription_plan_code = latest_known_subscription.get("plan_code")
+ subscription_starts_at = latest_known_subscription.get("starts_at")
+ subscription_expires_at = latest_known_subscription.get("expires_at")
except Exception:
subscription_active = None
subscription_plan_code = None