mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-24 05:23:36 +00:00
feat(client): gate in-game ads by adblock detection + Admiral recovery (#4534)
## What Two related pieces, wired into the existing `window.adsEnabled` / `userMeResponse` ad flow: 1. **`AdGatekeeper`** — decides whether the *intrusive* in-game ad may show. Once a blocker is **ever** detected, the ad is suppressed **permanently** (terminal state, persisted to `localStorage["adblock-detected"]`). Ad-block users are highly ad-sensitive, so disabling the blocker does **not** unlock the ad — in this or any future session. Detection = a DOM bait probe, refined by Admiral's `measure.detected` signal (`adblocking && !whitelisted`) when it fires. Clean users are never latched. 2. **`Admiral.ts`** — injects the ad-recovery tag (command-queue stub + payload + GAM targeting shim) for **ad-eligible users only**. Paid/`adfree` users have `window.adsEnabled === false`, so Admiral never loads and its adblock popup can't fire for them. Only the in-game ad (`InGamePromo`) is gated — it now loads via `adGatekeeper.whenClear(...)`. Passive homepage/gutter ads are unchanged. ## Why - Paid users (any shop purchase → `adfree` for life) must never see ads *or* load Admiral. - Free adblock users get Admiral's recovery popup, but should never be hit with an intrusive in-game ad even if they disable their blocker. ## How it behaves | Visitor | Admiral | In-game ad | |---|---|---| | Paid (`adfree`) | never loaded | never shown | | Free, no adblock | loaded | shown | | Free, adblock on (or ever was) | loaded (recovery popup) | suppressed forever | | Free, adblock blocks Admiral too | callback never fires | bait fallback suppresses | ## Testing - **Unit:** `tests/AdGatekeeper.test.ts` (9 cases) — terminal latch, "disabling blocker doesn't unlock", cross-session persistence, seed path, no-false-positive. `tsc` clean, `eslint` clean. - **Manual (headless Chromium, real bootstrap):** free user → `adsEnabled: true`, Admiral tag injected + payload initialized, `persisted: null` (no false positive); simulated blocker → flag latches to `"1"`; reload with no blocker → still `"1"` (forever); reset clean afterward. ## Notes / follow-ups - The GAM targeting shim (block 3 of the provider's tag) is ported verbatim but is likely a no-op here since serving is via Playwire RAMP, not Google Ad Manager. Kept for fidelity; can drop if unused. - `ADMIRAL_PAYLOAD_SRC` is a disguised, rotating domain — re-sync from the provider when they reissue the tag. - Admiral's own popup is dashboard-configured and typically domain-locked; best verified on the production domain with a real blocker. - `res.subscribed` (Admiral's own ad-free pass) is intentionally ignored — OpenFront's ad-free is the server `adfree` flag. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { adGatekeeper } from "../../AdGatekeeper";
|
||||
import { Controller } from "../../Controller";
|
||||
import { GameView } from "../../view";
|
||||
|
||||
@@ -18,6 +19,7 @@ export class InGamePromo extends LitElement implements Controller {
|
||||
private bottomRailDestroyed: boolean = false;
|
||||
private cornerAdShown: boolean = false;
|
||||
private adCheckInterval: ReturnType<typeof setTimeout> | null = null;
|
||||
private adGateOff: (() => void) | null = null;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -56,12 +58,17 @@ export class InGamePromo extends LitElement implements Controller {
|
||||
|
||||
if (!window.adsEnabled) return;
|
||||
|
||||
this.shouldShow = true;
|
||||
this.requestUpdate();
|
||||
// Show the intrusive in-game ad only to users who have been blocker-free.
|
||||
// Once a blocker is ever detected the gate latches suppressed forever
|
||||
// (persisted across sessions), so whenClear never fires for those users.
|
||||
this.adGateOff = adGatekeeper.whenClear(() => {
|
||||
this.shouldShow = true;
|
||||
this.requestUpdate();
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
this.loadAd();
|
||||
this.checkForAds();
|
||||
this.updateComplete.then(() => {
|
||||
this.loadAd();
|
||||
this.checkForAds();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,6 +118,10 @@ export class InGamePromo extends LitElement implements Controller {
|
||||
}
|
||||
|
||||
public hideAd(): void {
|
||||
if (this.adGateOff) {
|
||||
this.adGateOff();
|
||||
this.adGateOff = null;
|
||||
}
|
||||
if (this.adCheckInterval) {
|
||||
clearInterval(this.adCheckInterval);
|
||||
this.adCheckInterval = null;
|
||||
|
||||
Reference in New Issue
Block a user