feat(ads): VK post-move interstitial + retire deprecated hint_balance/paid_account
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 23s
CI / ui (pull_request) Successful in 1m12s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m53s

Interstitial video after a confirmed play or a hint, VK-only, offline
banner-only. The gate is client-mirrored: the backend puts the config
cooldowns (global 5m / vs_ai 30m / hint 1m) and a suppressed flag (the
no-ads / no_banner gate, same as the banner) on Profile.ads via adsFor;
the client self-gates on a per-kind last-shown time in localStorage, with
the VITE_ADS_STUB contour "ad fired" toast. Never fires after a pass,
exchange or resign. maybeShowInterstitial + vkShowInterstitial; the codec
and gateway transcode carry the ads block.

Also retires the deprecated accounts.hint_balance / paid_account domain
usage (expand-contract, code only — the columns stay for a later DROP so
image rollback stays DB-safe): drop the Account fields and their scan, the
dead account.SpendHint, account.GrantHints and the admin grant-hints
action; the in-game hint display now comes wholly from the payments hint
benefit. Banner eligibility and account merge no longer read the legacy
flags.

Tests: ads.test.ts (the client-mirrored gate), the codec ads round-trip,
the gateway ads transcode test, and a profile ads-config integration test
(cooldowns + suppressed under no-ads / no_banner). Docs: PAYMENTS §10 (+ru)
interstitial, the decision amends, backend README, the plan.
This commit is contained in:
Ilia Denisov
2026-07-10 02:48:10 +02:00
parent e08d3301bd
commit 13be7c3d9a
33 changed files with 805 additions and 220 deletions
+15
View File
@@ -14,6 +14,7 @@ import type {
RobotBlockEntry,
RobotFriendRequestEntry,
Banner,
AdsConfig,
BannerCampaign,
BestMove,
BestMoveTile,
@@ -461,6 +462,7 @@ export function decodeProfile(buf: Uint8Array): Profile {
notificationsInAppOnly: p.notificationsInAppOnly(),
variantPreferences: decodeVariantPreferences(p),
banner: decodeBanner(p),
ads: decodeAds(p),
email: s(p.email()),
telegramLinked: p.telegramLinked(),
vkLinked: p.vkLinked(),
@@ -497,6 +499,19 @@ function bannerTriple(bg: string | null, fg: string | null, link: string | null)
return bg && fg && link ? { bg, fg, link } : null;
}
// decodeAds projects the optional post-move interstitial config of a Profile, or undefined when
// absent (an old backend); the client then shows no interstitial.
function decodeAds(p: fb.Profile): AdsConfig | undefined {
const a = p.ads();
if (!a) return undefined;
return {
cooldownGlobalS: a.cooldownGlobalS(),
cooldownVsAiS: a.cooldownVsAiS(),
cooldownHintS: a.cooldownHintS(),
suppressed: a.suppressed(),
};
}
// decodeBanner projects the optional advertising-banner block of a Profile, or
// undefined when the viewer is not eligible (the field is absent).
function decodeBanner(p: fb.Profile): Banner | undefined {