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
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:
+55
-1
@@ -4,7 +4,9 @@
|
||||
// flow is testable without waiting for a real ad — production never sets the flag, so a real ad that
|
||||
// fails to load there must not credit (no stub fallback in prod).
|
||||
import { executionContext } from './wallet';
|
||||
import { vkRewardedReady, vkShowRewarded } from './vk';
|
||||
import { vkRewardedReady, vkShowRewarded, vkShowInterstitial } from './vk';
|
||||
import { showToast } from './app.svelte';
|
||||
import type { AdsConfig } from './model';
|
||||
|
||||
/**
|
||||
* adsStubEnabled reports the contour test stub (VITE_ADS_STUB=1). Production never sets it, so it
|
||||
@@ -46,3 +48,55 @@ export async function showRewarded(): Promise<RewardedResult> {
|
||||
}
|
||||
return { watched: await vkShowRewarded(), stub: false };
|
||||
}
|
||||
|
||||
// The post-move interstitial gate is client-mirrored: the server sends the cooldowns + suppressed in
|
||||
// the profile (Profile.ads); the client tracks the last-shown time per kind in localStorage and
|
||||
// self-gates. Last-shown is per-device (cleared with storage) — acceptable for a frequency gate.
|
||||
const INTERSTITIAL_LAST_KEY = 'ads.interstitial.last';
|
||||
|
||||
// interstitialLast reads the last-shown epoch millis per kind (0 when absent/corrupt).
|
||||
function interstitialLast(): { move: number; hint: number } {
|
||||
try {
|
||||
const raw = localStorage.getItem(INTERSTITIAL_LAST_KEY);
|
||||
if (raw) {
|
||||
const j = JSON.parse(raw) as Partial<{ move: number; hint: number }>;
|
||||
return { move: j.move ?? 0, hint: j.hint ?? 0 };
|
||||
}
|
||||
} catch {
|
||||
/* a corrupt or unavailable store simply resets the gate */
|
||||
}
|
||||
return { move: 0, hint: 0 };
|
||||
}
|
||||
|
||||
function recordInterstitial(kind: 'move' | 'hint', now: number): void {
|
||||
try {
|
||||
const l = interstitialLast();
|
||||
l[kind] = now;
|
||||
localStorage.setItem(INTERSTITIAL_LAST_KEY, JSON.stringify(l));
|
||||
} catch {
|
||||
/* ignore a write failure — the worst case is one extra ad next time */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* maybeShowInterstitial shows a post-move (`kind='move'`, global / vs_ai cooldown) or post-hint
|
||||
* (`kind='hint'`, its own short cooldown) interstitial when the client-mirrored gate allows: not
|
||||
* suppressed (no-ads), online, inside VK (the stub bypasses VK), and the mirrored cooldown elapsed.
|
||||
* The stub (contour test) shows an "ad fired" toast. Returns whether an ad was shown. Fire-and-forget
|
||||
* — the caller does not await a result to proceed.
|
||||
*/
|
||||
export async function maybeShowInterstitial(
|
||||
ads: AdsConfig | undefined,
|
||||
kind: 'move' | 'hint',
|
||||
opts: { vsAi: boolean; online: boolean },
|
||||
): Promise<boolean> {
|
||||
if (!ads || ads.suppressed || !opts.online) return false;
|
||||
const stub = adsStubEnabled();
|
||||
if (!stub && executionContext() !== 'vk') return false;
|
||||
const cooldownS = kind === 'hint' ? ads.cooldownHintS : opts.vsAi ? ads.cooldownVsAiS : ads.cooldownGlobalS;
|
||||
const now = Date.now();
|
||||
if (now - interstitialLast()[kind] < cooldownS * 1000) return false;
|
||||
const shown = stub ? (showToast('ad fired'), true) : await vkShowInterstitial();
|
||||
if (shown) recordInterstitial(kind, now);
|
||||
return shown;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user