fix: refresh webapp profile after activation

This commit is contained in:
3252a8
2026-05-24 23:25:21 +03:00
parent 7cffb4667f
commit 85ecd644b8
7 changed files with 110 additions and 8 deletions
+11
View File
@@ -430,6 +430,17 @@ async def account_telegram_link_route(request: web.Request) -> web.Response:
async def me_route(request: web.Request) -> web.Response:
user_id = _require_user_id(request)
settings: Settings = request.app["settings"]
fresh = str(request.query.get("fresh") or "").strip().lower() in {
"1",
"true",
"yes",
"on",
}
if fresh:
await _invalidate_webapp_user_caches(settings, user_id)
data = await _build_user_payload(request, user_id)
return web.json_response({"ok": True, **data})
data = await webapp_cached_user_payload(
settings,
"me",
+6
View File
@@ -1,6 +1,8 @@
# ruff: noqa: F401,F403,F405,I001
from ._runtime import * # noqa: F403,F405
from bot.app.web.webapp.cache_helpers import invalidate_webapp_user_caches
async def apply_promo_route(request: web.Request) -> web.Response:
user_id = _require_user_id(request)
@@ -329,6 +331,8 @@ async def activate_trial_route(request: web.Request) -> web.Response:
await session.rollback()
logger.exception("Failed to mark WebApp trial activation for ad attribution")
await invalidate_webapp_user_caches(settings, user_id)
return web.json_response(
{
"ok": True,
@@ -724,6 +728,8 @@ async def payment_status_route(request: web.Request) -> web.Response:
if not payment or payment.user_id != user_id:
return _json_error(404, "not_found", "Payment not found")
payment = await _refresh_yookassa_payment_status(request, session, payment)
if payment.status == "succeeded":
await invalidate_webapp_user_caches(request.app["settings"], user_id)
return web.json_response(
{
"ok": True,
+7 -6
View File
@@ -641,7 +641,7 @@
}
}
if (shouldRefreshProfile) {
await loadData();
await loadData({ fresh: true });
const shown = await maybeShowActivationSuccessDialog({
source: "watch",
paymentId: pending?.paymentId,
@@ -685,7 +685,7 @@
activationResumeLastCheckAt = now;
activationResumeRefreshBusy = true;
try {
await loadData();
await loadData({ fresh: true });
const shown = await maybeShowActivationSuccessDialog({ source: "resume" });
if (!shown) startPendingActivationWatch();
} catch (_error) {
@@ -1072,6 +1072,7 @@
getCsrfToken: () => csrfToken,
});
if (mode === "app" && screen !== "admin") {
if (hasPendingActivationHandoff()) await loadData({ fresh: true });
const shown = await maybeShowActivationSuccessDialog({ source: "boot" });
if (!shown) startPendingActivationWatch();
}
@@ -1115,8 +1116,8 @@
syncPasswordLoginPath(nextEnabled, replace);
}
async function loadData() {
const payload = await api("/me");
async function loadData(options = {}) {
const payload = await api(options?.fresh ? "/me?fresh=1" : "/me");
if (!payload.ok) throw new Error(payload.error || "load_failed");
data = payload;
billingStore.update((s) => ({
@@ -1458,7 +1459,7 @@
? t("wa_promo_activated_until", { date: response.end_date_text })
: t("wa_promo_activated");
promoIsError = false;
await loadData();
await loadData({ fresh: true });
} catch (error) {
promoStatus = error?.message || t("wa_promo_activation_failed");
promoIsError = true;
@@ -1481,7 +1482,7 @@
if (!response.ok) throw response;
trialActivationResult = response;
showToast(t("wa_trial_activated"));
await loadData();
await loadData({ fresh: true });
await maybeShowActivationSuccessDialog({ source: "trial", force: true });
} catch (error) {
const message = error?.message || t("wa_trial_activation_failed");
+1 -1
View File
@@ -751,7 +751,7 @@ export async function mockApi(path, options = {}, context = {}) {
return { ok: true, ticket, messages: clone(supportMessages[ticket.ticket_id] || []) };
}
if (cleanPath === "/support/unread") return { ok: true, unread: 1 };
if (path === "/me") return clone(DEV_MOCK.data);
if (cleanPath === "/me") return clone(DEV_MOCK.data);
if (path === "/subscription-guides") return clone(DEV_MOCK.data.subscription_guides);
if (cleanPath.startsWith("/subscription-guides/public/")) {
const shareToken = decodeURIComponent(cleanPath.split("/").pop() || "");
@@ -64,7 +64,7 @@ export function createBillingStore({
paymentPollToken += 1;
}
showToast(t("wa_payment_success", {}, "Payment successful"));
await loadData();
await loadData({ fresh: true });
if (
successContext.initialSubscriptionPayment &&
typeof onSubscriptionActivated === "function"
+43
View File
@@ -2,11 +2,23 @@ from types import SimpleNamespace
from unittest import IsolatedAsyncioTestCase
from unittest.mock import AsyncMock, patch
import bot.app.web.subscription_webapp # noqa: F401
from bot.app.web.webapp import billing as billing_module
from bot.payment_providers.base import WebAppPaymentContext
from bot.payment_providers.yookassa import create_webapp_payment
class _SessionFactory:
def __call__(self):
return self
async def __aenter__(self):
return SimpleNamespace()
async def __aexit__(self, exc_type, exc, tb):
return False
class WebAppPaymentStatusTests(IsolatedAsyncioTestCase):
async def test_yookassa_pending_payment_refresh_processes_succeeded_provider_status(self):
payment = SimpleNamespace(
@@ -123,3 +135,34 @@ class WebAppPaymentStatusTests(IsolatedAsyncioTestCase):
yookassa_service.create_payment.await_args.kwargs["save_payment_method"],
False,
)
async def test_payment_status_invalidates_profile_cache_for_succeeded_payment(self):
settings = SimpleNamespace()
payment = SimpleNamespace(payment_id=42, user_id=1001, status="succeeded")
request = SimpleNamespace(
app={"settings": settings, "async_session_factory": _SessionFactory()},
match_info={"payment_id": "42"},
)
with (
patch.object(billing_module, "_require_user_id", return_value=1001),
patch.object(
billing_module.payment_dal,
"get_payment_by_db_id",
AsyncMock(return_value=payment),
),
patch.object(
billing_module,
"_refresh_yookassa_payment_status",
AsyncMock(return_value=payment),
),
patch.object(
billing_module,
"invalidate_webapp_user_caches",
AsyncMock(),
) as invalidate_cache,
):
response = await billing_module.payment_status_route(request)
invalidate_cache.assert_awaited_once_with(settings, 1001)
self.assertEqual(response.status, 200)
+41
View File
@@ -0,0 +1,41 @@
import json
from types import SimpleNamespace
from unittest import IsolatedAsyncioTestCase
from unittest.mock import AsyncMock, patch
import bot.app.web.subscription_webapp # noqa: F401
from bot.app.web.webapp import account as account_module
class WebAppProfileCacheTests(IsolatedAsyncioTestCase):
async def test_me_route_fresh_bypasses_cache_and_invalidates_payload(self):
settings = SimpleNamespace(WEBAPP_ME_CACHE_TTL_SECONDS=60)
request = SimpleNamespace(app={"settings": settings}, query={"fresh": "1"})
with (
patch.object(account_module, "_require_user_id", return_value=42),
patch.object(
account_module,
"_invalidate_webapp_user_caches",
AsyncMock(),
) as invalidate_cache,
patch.object(
account_module,
"_build_user_payload",
AsyncMock(return_value={"user": {"id": 42}, "subscription": {"active": True}}),
) as build_payload,
patch.object(
account_module,
"webapp_cached_user_payload",
AsyncMock(),
) as cached_payload,
):
response = await account_module.me_route(request)
invalidate_cache.assert_awaited_once_with(settings, 42)
build_payload.assert_awaited_once_with(request, 42)
cached_payload.assert_not_awaited()
self.assertEqual(
json.loads(response.text),
{"ok": True, "user": {"id": 42}, "subscription": {"active": True}},
)