Fix terminal subscription gate on unknown auth state
This commit is contained in:
@@ -127,7 +127,7 @@ const TERM = {
|
|||||||
logIn: { en: "Log in", zh: "登录" },
|
logIn: { en: "Log in", zh: "登录" },
|
||||||
createAccount: { en: "Create an account", zh: "注册账号" },
|
createAccount: { en: "Create an account", zh: "注册账号" },
|
||||||
learnAbout: { en: "Learn about PolyWeather", zh: "了解 PolyWeather" },
|
learnAbout: { en: "Learn about PolyWeather", zh: "了解 PolyWeather" },
|
||||||
proAccessRequired: { en: "Pro Access Required", zh: "需要付费订阅" },
|
proAccessRequired: { en: "Pro subscription required", zh: "需要开通 Pro" },
|
||||||
proDesc: {
|
proDesc: {
|
||||||
en: "The PolyWeather terminal is a paid product. Subscribe to unlock real-time weather-signal intelligence.",
|
en: "The PolyWeather terminal is a paid product. Subscribe to unlock real-time weather-signal intelligence.",
|
||||||
zh: "PolyWeather 决策台为付费产品。订阅以解锁实时天气信号情报。",
|
zh: "PolyWeather 决策台为付费产品。订阅以解锁实时天气信号情报。",
|
||||||
@@ -137,7 +137,7 @@ const TERM = {
|
|||||||
zh: "按月计费,随时可取消。通过 Polygon 链 USDC 支付。",
|
zh: "按月计费,随时可取消。通过 Polygon 链 USDC 支付。",
|
||||||
},
|
},
|
||||||
month: { en: "/ month", zh: "/ 月" },
|
month: { en: "/ month", zh: "/ 月" },
|
||||||
subscribeNow: { en: "Subscribe Now — $10/mo", zh: "立即订阅 — $10/月" },
|
subscribeNow: { en: "View Pro plans", zh: "查看订阅方案" },
|
||||||
subscribePrompt: {
|
subscribePrompt: {
|
||||||
en: "You need an active subscription to access the terminal.",
|
en: "You need an active subscription to access the terminal.",
|
||||||
zh: "你需要开通有效订阅才能访问决策台。",
|
zh: "你需要开通有效订阅才能访问决策台。",
|
||||||
@@ -1078,6 +1078,62 @@ function ScanTerminalScreen() {
|
|||||||
};
|
};
|
||||||
}, [loadAuthProfile]);
|
}, [loadAuthProfile]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!hydrated ||
|
||||||
|
canUseLocalFullAccess ||
|
||||||
|
!proAccess.authenticated ||
|
||||||
|
!proAccess.loading ||
|
||||||
|
proAccess.subscriptionActive ||
|
||||||
|
typeof fetch !== "function"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const supabaseEnabled = hasSupabasePublicEnv();
|
||||||
|
const retryAuthProfile = async () => {
|
||||||
|
try {
|
||||||
|
const payload = await loadTerminalAuthProfile({
|
||||||
|
getSession: () =>
|
||||||
|
supabaseEnabled
|
||||||
|
? getSupabaseBrowserClient().auth.getSession()
|
||||||
|
: Promise.resolve({ data: { session: null } }),
|
||||||
|
hasSupabasePublicEnv: supabaseEnabled,
|
||||||
|
loadAuthProfile,
|
||||||
|
});
|
||||||
|
if (cancelled) return;
|
||||||
|
setProAccess((prev) => mergeAccessStateWithAuthPayload(prev, payload));
|
||||||
|
} catch (error) {
|
||||||
|
if (cancelled) return;
|
||||||
|
setProAccess((prev) =>
|
||||||
|
prev.loading && prev.authenticated && !prev.subscriptionActive
|
||||||
|
? { ...prev, error: String(error) }
|
||||||
|
: prev,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstRetry = window.setTimeout(() => {
|
||||||
|
void retryAuthProfile();
|
||||||
|
}, 1500);
|
||||||
|
const interval = window.setInterval(() => {
|
||||||
|
void retryAuthProfile();
|
||||||
|
}, 5000);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
window.clearTimeout(firstRetry);
|
||||||
|
window.clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
canUseLocalFullAccess,
|
||||||
|
hydrated,
|
||||||
|
loadAuthProfile,
|
||||||
|
proAccess.authenticated,
|
||||||
|
proAccess.loading,
|
||||||
|
proAccess.subscriptionActive,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedRegionKey("all");
|
setSelectedRegionKey("all");
|
||||||
setLocalTimezoneOffsetSeconds(-new Date().getTimezoneOffset() * 60);
|
setLocalTimezoneOffsetSeconds(-new Date().getTimezoneOffset() * 60);
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { LockKeyhole, CreditCard, LogIn } from "lucide-react";
|
|||||||
|
|
||||||
const ACCESS_TERM = {
|
const ACCESS_TERM = {
|
||||||
signInToContinue: { en: "Sign in to continue", zh: "请先登录" },
|
signInToContinue: { en: "Sign in to continue", zh: "请先登录" },
|
||||||
proAccessRequired: { en: "Pro Access Required", zh: "需要付费订阅" },
|
proAccessRequired: { en: "Pro subscription required", zh: "需要开通 Pro" },
|
||||||
month: { en: "/ month", zh: "/ 月" },
|
month: { en: "/ 30 days", zh: "/ 30 天" },
|
||||||
subscribeNow: { en: "Subscribe Now — $10/mo", zh: "立即订阅 — $10/月" },
|
subscribeNow: { en: "View Pro plans", zh: "查看订阅方案" },
|
||||||
backToProduct: { en: "Back to product overview", zh: "返回产品介绍页" },
|
backToProduct: { en: "Back to product overview", zh: "返回产品介绍页" },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@@ -19,16 +19,16 @@ function t(key: keyof typeof ACCESS_TERM, isEn: boolean) {
|
|||||||
function SubscriptionGate({ isEn }: { isEn: boolean }) {
|
function SubscriptionGate({ isEn }: { isEn: boolean }) {
|
||||||
const features = isEn
|
const features = isEn
|
||||||
? [
|
? [
|
||||||
"Real-time METAR observations across 500+ stations",
|
"Real-time station, METAR, and runway signals",
|
||||||
"DEB forecast blends with 0–240h horizon",
|
"DEB forecast curves and model comparison",
|
||||||
"AI decision cards with Poly-score ranking",
|
"Full terminal grid with high-frequency refresh",
|
||||||
"Historical backtesting & weather signals",
|
"API and paid Telegram group access on paid Pro",
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
"500+ 气象站实时 METAR 实况",
|
"实时气象站、METAR 与跑道信号",
|
||||||
"DEB 智能融合预测(0–240 小时)",
|
"DEB 预测曲线与模型对比",
|
||||||
"AI 决策卡片 + Poly-score 排名",
|
"完整终端网格与高频刷新",
|
||||||
"历史回测与天气信号",
|
"付费 Pro 可进入 API 与付费 Telegram 群",
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,7 +57,7 @@ function SubscriptionGate({ isEn }: { isEn: boolean }) {
|
|||||||
|
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<div className="mb-6 flex items-baseline gap-1">
|
<div className="mb-6 flex items-baseline gap-1">
|
||||||
<span className="text-4xl font-black text-slate-900">$10</span>
|
<span className="text-4xl font-black text-slate-900">29.9 USDC</span>
|
||||||
<span className="text-base text-slate-500">
|
<span className="text-base text-slate-500">
|
||||||
{t("month", isEn)}
|
{t("month", isEn)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ export function runTests() {
|
|||||||
degraded_auth_profile: true,
|
degraded_auth_profile: true,
|
||||||
});
|
});
|
||||||
assert(
|
assert(
|
||||||
coldUnknown.subscriptionActive === false && coldUnknown.authenticated === true,
|
coldUnknown.subscriptionActive === false &&
|
||||||
"cold-start unknown subscription state must not fabricate Pro access",
|
coldUnknown.authenticated === true &&
|
||||||
|
coldUnknown.loading === true,
|
||||||
|
"cold-start unknown subscription state must keep the terminal gate loading instead of showing a false paywall",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,21 @@ function queuedDays(value: unknown) {
|
|||||||
return Math.max(0, Number(value ?? 0));
|
return Math.max(0, Number(value ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSubscriptionStatusUnknown(payload: AuthProfilePayload) {
|
||||||
|
return (
|
||||||
|
payload.subscription_active === null ||
|
||||||
|
payload.subscription_active === undefined ||
|
||||||
|
payload.degraded_auth_profile === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function createAccessStateFromAuthPayload(
|
export function createAccessStateFromAuthPayload(
|
||||||
payload: AuthProfilePayload,
|
payload: AuthProfilePayload,
|
||||||
): ProAccessState {
|
): ProAccessState {
|
||||||
|
const subscriptionUnknown =
|
||||||
|
Boolean(payload.authenticated) && isSubscriptionStatusUnknown(payload);
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: subscriptionUnknown,
|
||||||
authenticated: Boolean(payload.authenticated),
|
authenticated: Boolean(payload.authenticated),
|
||||||
userId: payload.user_id ?? null,
|
userId: payload.user_id ?? null,
|
||||||
subscriptionActive: payload.subscription_active === true,
|
subscriptionActive: payload.subscription_active === true,
|
||||||
@@ -41,10 +51,7 @@ export function mergeAccessStateWithAuthPayload(
|
|||||||
payload: AuthProfilePayload,
|
payload: AuthProfilePayload,
|
||||||
): ProAccessState {
|
): ProAccessState {
|
||||||
const next = createAccessStateFromAuthPayload(payload);
|
const next = createAccessStateFromAuthPayload(payload);
|
||||||
const subscriptionUnknown =
|
const subscriptionUnknown = isSubscriptionStatusUnknown(payload);
|
||||||
payload.subscription_active === null ||
|
|
||||||
payload.subscription_active === undefined ||
|
|
||||||
payload.degraded_auth_profile === true;
|
|
||||||
|
|
||||||
if (!subscriptionUnknown || !previous.subscriptionActive || !next.authenticated) {
|
if (!subscriptionUnknown || !previous.subscriptionActive || !next.authenticated) {
|
||||||
return next;
|
return next;
|
||||||
@@ -52,6 +59,7 @@ export function mergeAccessStateWithAuthPayload(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...next,
|
...next,
|
||||||
|
loading: false,
|
||||||
subscriptionActive: true,
|
subscriptionActive: true,
|
||||||
subscriptionPlanCode: previous.subscriptionPlanCode,
|
subscriptionPlanCode: previous.subscriptionPlanCode,
|
||||||
subscriptionExpiresAt: previous.subscriptionExpiresAt,
|
subscriptionExpiresAt: previous.subscriptionExpiresAt,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -1495,16 +1495,16 @@ class SupabaseEntitlementService:
|
|||||||
logger.warning(f"supabase subscription query error user_id={user_id}: {exc}")
|
logger.warning(f"supabase subscription query error user_id={user_id}: {exc}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _query_active_subscription_rows(
|
def _query_active_subscription_rows_result(
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
bypass_cache: bool = False,
|
bypass_cache: bool = False,
|
||||||
) -> List[Dict[str, object]]:
|
) -> Tuple[List[Dict[str, object]], bool]:
|
||||||
if not user_id:
|
if not user_id:
|
||||||
return []
|
return [], True
|
||||||
if not self.service_role_key:
|
if not self.service_role_key:
|
||||||
logger.warning("SUPABASE_SERVICE_ROLE_KEY is missing")
|
logger.warning("SUPABASE_SERVICE_ROLE_KEY is missing")
|
||||||
return []
|
return [], False
|
||||||
|
|
||||||
now_ts = time.time()
|
now_ts = time.time()
|
||||||
if not bypass_cache:
|
if not bypass_cache:
|
||||||
@@ -1513,7 +1513,7 @@ class SupabaseEntitlementService:
|
|||||||
if cached and now_ts - float(cached.get("ts") or 0) < self.sub_cache_ttl_sec:
|
if cached and now_ts - float(cached.get("ts") or 0) < self.sub_cache_ttl_sec:
|
||||||
rows = cached.get("rows")
|
rows = cached.get("rows")
|
||||||
if isinstance(rows, list):
|
if isinstance(rows, list):
|
||||||
return [row for row in rows if isinstance(row, dict)]
|
return [row for row in rows if isinstance(row, dict)], True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
@@ -1538,7 +1538,7 @@ class SupabaseEntitlementService:
|
|||||||
user_id,
|
user_id,
|
||||||
response.status_code,
|
response.status_code,
|
||||||
)
|
)
|
||||||
rows: List[Dict[str, object]] = []
|
return [], False
|
||||||
else:
|
else:
|
||||||
data = response.json() if response.content else []
|
data = response.json() if response.content else []
|
||||||
rows = [row for row in data if isinstance(row, dict)] if isinstance(data, list) else []
|
rows = [row for row in data if isinstance(row, dict)] if isinstance(data, list) else []
|
||||||
@@ -1551,10 +1551,21 @@ class SupabaseEntitlementService:
|
|||||||
"rows": rows,
|
"rows": rows,
|
||||||
"ts": now_ts,
|
"ts": now_ts,
|
||||||
}
|
}
|
||||||
return rows
|
return rows, True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(f"supabase active subscription rows query error user_id={user_id}: {exc}")
|
logger.warning(f"supabase active subscription rows query error user_id={user_id}: {exc}")
|
||||||
return []
|
return [], False
|
||||||
|
|
||||||
|
def _query_active_subscription_rows(
|
||||||
|
self,
|
||||||
|
user_id: str,
|
||||||
|
bypass_cache: bool = False,
|
||||||
|
) -> List[Dict[str, object]]:
|
||||||
|
rows, _ok = self._query_active_subscription_rows_result(
|
||||||
|
user_id,
|
||||||
|
bypass_cache=bypass_cache,
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
|
||||||
def _query_latest_subscription_any_status(
|
def _query_latest_subscription_any_status(
|
||||||
self,
|
self,
|
||||||
@@ -1726,10 +1737,25 @@ class SupabaseEntitlementService:
|
|||||||
user_id: str,
|
user_id: str,
|
||||||
respect_requirement: bool = True,
|
respect_requirement: bool = True,
|
||||||
bypass_cache: bool = False,
|
bypass_cache: bool = False,
|
||||||
|
unknown_on_error: bool = False,
|
||||||
) -> Dict[str, object]:
|
) -> Dict[str, object]:
|
||||||
if respect_requirement and not self.require_subscription:
|
if respect_requirement and not self.require_subscription:
|
||||||
return {}
|
return {}
|
||||||
rows = self._query_active_subscription_rows(user_id, bypass_cache=bypass_cache)
|
rows, query_ok = self._query_active_subscription_rows_result(
|
||||||
|
user_id,
|
||||||
|
bypass_cache=bypass_cache,
|
||||||
|
)
|
||||||
|
if not query_ok and unknown_on_error:
|
||||||
|
return {
|
||||||
|
"unknown": True,
|
||||||
|
"current": None,
|
||||||
|
"current_expires_at": None,
|
||||||
|
"current_starts_at": None,
|
||||||
|
"total_expires_at": None,
|
||||||
|
"queued_days": 0,
|
||||||
|
"queued_count": 0,
|
||||||
|
"rows": None,
|
||||||
|
}
|
||||||
return self._subscription_window_from_rows(rows)
|
return self._subscription_window_from_rows(rows)
|
||||||
|
|
||||||
def _subscription_window_from_rows(
|
def _subscription_window_from_rows(
|
||||||
|
|||||||
@@ -164,6 +164,29 @@ def test_subscription_window_query_selects_only_window_fields(monkeypatch):
|
|||||||
assert window["current"]["plan_code"] == "pro_monthly"
|
assert window["current"]["plan_code"] == "pro_monthly"
|
||||||
|
|
||||||
|
|
||||||
|
def test_subscription_window_can_report_unknown_on_transient_query_failure(monkeypatch):
|
||||||
|
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||||
|
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||||
|
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||||
|
|
||||||
|
service = SupabaseEntitlementService()
|
||||||
|
|
||||||
|
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||||
|
return _Response(503, {"message": "temporarily unavailable"})
|
||||||
|
|
||||||
|
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||||
|
|
||||||
|
window = service.get_subscription_window(
|
||||||
|
"user-1",
|
||||||
|
respect_requirement=False,
|
||||||
|
bypass_cache=True,
|
||||||
|
unknown_on_error=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert window["unknown"] is True
|
||||||
|
assert window["rows"] is None
|
||||||
|
|
||||||
|
|
||||||
def test_list_subscription_windows_selects_only_batch_window_fields(monkeypatch):
|
def test_list_subscription_windows_selects_only_batch_window_fields(monkeypatch):
|
||||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||||
|
|||||||
@@ -382,6 +382,51 @@ def test_auth_me_uses_subscription_window_as_required_subscription_gate(monkeypa
|
|||||||
assert payload["subscription_queued_days"] == 30
|
assert payload["subscription_queued_days"] == 30
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_me_preserves_unknown_subscription_window(monkeypatch):
|
||||||
|
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "enabled", True)
|
||||||
|
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "require_subscription", False)
|
||||||
|
monkeypatch.setattr(web_core, "_SUPABASE_AUTH_REQUIRED", False)
|
||||||
|
monkeypatch.setattr(routes, "_resolve_weekly_profile", lambda request: {"weekly_points": 0, "weekly_rank": None})
|
||||||
|
monkeypatch.setattr(routes, "_resolve_auth_points", lambda request: 0)
|
||||||
|
|
||||||
|
def _bind_identity(request):
|
||||||
|
request.state.auth_user_id = "user-1"
|
||||||
|
request.state.auth_email = "user@example.com"
|
||||||
|
|
||||||
|
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
|
||||||
|
monkeypatch.setattr(routes, "_bind_optional_supabase_identity", _bind_identity)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
routes.SUPABASE_ENTITLEMENT,
|
||||||
|
"get_subscription_window",
|
||||||
|
lambda user_id, respect_requirement=False, bypass_cache=False, unknown_on_error=False: {
|
||||||
|
"unknown": True,
|
||||||
|
"rows": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
routes.SUPABASE_ENTITLEMENT,
|
||||||
|
"get_latest_active_subscription",
|
||||||
|
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||||
|
AssertionError("unknown subscription window must not be downgraded to inactive"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
routes.SUPABASE_ENTITLEMENT,
|
||||||
|
"get_latest_subscription_any_status",
|
||||||
|
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||||
|
AssertionError("unknown subscription window must not be treated as subscription history"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get("/api/auth/me")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["authenticated"] is True
|
||||||
|
assert payload["subscription_active"] is None
|
||||||
|
assert payload["subscription_plan_code"] is None
|
||||||
|
|
||||||
|
|
||||||
def test_auth_me_uses_window_rows_for_non_required_latest_known_subscription(monkeypatch):
|
def test_auth_me_uses_window_rows_for_non_required_latest_known_subscription(monkeypatch):
|
||||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "enabled", True)
|
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "enabled", True)
|
||||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "require_subscription", False)
|
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "require_subscription", False)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ def get_auth_me_payload(request: Request) -> Dict[str, Any]:
|
|||||||
user_id,
|
user_id,
|
||||||
respect_requirement=False,
|
respect_requirement=False,
|
||||||
bypass_cache=True,
|
bypass_cache=True,
|
||||||
|
unknown_on_error=True,
|
||||||
)
|
)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
subscription_window = legacy_routes.SUPABASE_ENTITLEMENT.get_subscription_window(
|
subscription_window = legacy_routes.SUPABASE_ENTITLEMENT.get_subscription_window(
|
||||||
@@ -65,8 +66,15 @@ def get_auth_me_payload(request: Request) -> Dict[str, Any]:
|
|||||||
)
|
)
|
||||||
latest_subscription = None
|
latest_subscription = None
|
||||||
latest_known_subscription = None
|
latest_known_subscription = None
|
||||||
subscription_window_known = isinstance(subscription_window, dict)
|
subscription_window_unknown = (
|
||||||
if isinstance(subscription_window, dict):
|
isinstance(subscription_window, dict)
|
||||||
|
and subscription_window.get("unknown") is True
|
||||||
|
)
|
||||||
|
subscription_window_known = (
|
||||||
|
isinstance(subscription_window, dict)
|
||||||
|
and not subscription_window_unknown
|
||||||
|
)
|
||||||
|
if subscription_window_known:
|
||||||
current_subscription = subscription_window.get("current")
|
current_subscription = subscription_window.get("current")
|
||||||
if isinstance(current_subscription, dict):
|
if isinstance(current_subscription, dict):
|
||||||
latest_subscription = current_subscription
|
latest_subscription = current_subscription
|
||||||
@@ -79,6 +87,7 @@ def get_auth_me_payload(request: Request) -> Dict[str, Any]:
|
|||||||
if (
|
if (
|
||||||
not latest_subscription
|
not latest_subscription
|
||||||
and not latest_known_subscription
|
and not latest_known_subscription
|
||||||
|
and not subscription_window_unknown
|
||||||
and not subscription_window_known
|
and not subscription_window_known
|
||||||
and not subscription_required
|
and not subscription_required
|
||||||
):
|
):
|
||||||
@@ -89,13 +98,18 @@ def get_auth_me_payload(request: Request) -> Dict[str, Any]:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription_active = bool(latest_subscription)
|
subscription_active = (
|
||||||
if subscription_required and not subscription_active:
|
None if subscription_window_unknown else bool(latest_subscription)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
subscription_required
|
||||||
|
and subscription_active is False
|
||||||
|
):
|
||||||
raise HTTPException(status_code=403, detail="Subscription required")
|
raise HTTPException(status_code=403, detail="Subscription required")
|
||||||
|
|
||||||
if not latest_known_subscription:
|
if not subscription_window_unknown and not latest_known_subscription:
|
||||||
latest_known_subscription = latest_subscription
|
latest_known_subscription = latest_subscription
|
||||||
if not latest_known_subscription:
|
if not subscription_window_unknown and not latest_known_subscription:
|
||||||
latest_known_subscription = (
|
latest_known_subscription = (
|
||||||
legacy_routes.SUPABASE_ENTITLEMENT.get_latest_subscription_any_status(
|
legacy_routes.SUPABASE_ENTITLEMENT.get_latest_subscription_any_status(
|
||||||
user_id
|
user_id
|
||||||
@@ -113,7 +127,7 @@ def get_auth_me_payload(request: Request) -> Dict[str, Any]:
|
|||||||
subscription_is_trial = _subscription_row_is_trial(latest_known_subscription)
|
subscription_is_trial = _subscription_row_is_trial(latest_known_subscription)
|
||||||
subscription_starts_at = latest_known_subscription.get("starts_at")
|
subscription_starts_at = latest_known_subscription.get("starts_at")
|
||||||
subscription_expires_at = latest_known_subscription.get("expires_at")
|
subscription_expires_at = latest_known_subscription.get("expires_at")
|
||||||
if isinstance(subscription_window, dict):
|
if subscription_window_known:
|
||||||
subscription_total_expires_at = subscription_window.get("total_expires_at")
|
subscription_total_expires_at = subscription_window.get("total_expires_at")
|
||||||
subscription_queued_days = int(subscription_window.get("queued_days") or 0)
|
subscription_queued_days = int(subscription_window.get("queued_days") or 0)
|
||||||
subscription_queued_count = int(subscription_window.get("queued_count") or 0)
|
subscription_queued_count = int(subscription_window.get("queued_count") or 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user