Skip to content

Add video ads during ranked matchmaking#3043

Draft
evanpelle wants to merge 1 commit intov29from
playwire
Draft

Add video ads during ranked matchmaking#3043
evanpelle wants to merge 1 commit intov29from
playwire

Conversation

@evanpelle
Copy link
Collaborator

@evanpelle evanpelle commented Jan 26, 2026

Description:

If the user has not made a purchase, then display a video ad while they wait for matchmaking. If the video ad does not load, assume they have adblock and request they disable adblock or make a purchase to play ranked.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

evan

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 26, 2026

Walkthrough

Adds a VideoAd LitElement and Ramp/Bolt ad integration; matchmaking now coordinates ad midpoint/completion and includes a waitBeforeJoin promise in the join-lobby event; global window.ramp/window.Bolt types extended; joinLobby accepts a waitBeforeJoin promise.

Changes

Cohort / File(s) Summary
Global API Surface
src/client/Main.ts
Extended Window.ramp with onPlayerReady, addUnits(), displayUnits(); extended Window.Bolt with on() and ad event constants (BOLT_AD_*, SHOW_HIDDEN_CONTAINER).
Video Ad Component
src/client/components/VideoAd.ts
New VideoAd LitElement: responsive container, Ramp/Bolt wiring, ad load timeout (ad-block detection), public showVideoAd() and callbacks onComplete, onMidpoint, onAdBlocked.
Matchmaking Flow Integration
src/client/Matchmaking.ts
Adds ad state, resolvers, handlers (handleAdComplete, handleAdMidpoint, handleAdBlocked), waitForAdComplete()/waitForAdMidpoint() helpers, renderVideoAd() and waits for ad midpoint/completion; join-lobby event now includes waitBeforeJoin.
Game Join Flow Control
src/client/ClientGameRunner.ts
joinLobby signature extended with waitBeforeJoin: Promise<void> = Promise.resolve(); message handling made async and onPrestart/onJoin deferred until waitBeforeJoin resolves.
Localization
resources/lang/en.json
Added matchmaking_modal.ad_blocked_title and matchmaking_modal.ad_blocked_message localization keys.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant MM as Matchmaking UI
    participant VideoAd as VideoAd Component
    participant RampBolt as Ramp/Bolt API
    participant ClientGR as ClientGameRunner

    User->>MM: Click "Join Ranked"
    activate MM
    MM->>VideoAd: render & showVideoAd()
    activate VideoAd
    VideoAd->>RampBolt: wait for Ramp, addUnits(), displayUnits()
    RampBolt-->>VideoAd: ad events (started, midpoint, complete) or error
    alt Ad Blocked / Error
        VideoAd->>MM: onAdBlocked()
        MM->>MM: show ad-blocked UI
        VideoAd-->>MM: resolve waitBeforeJoin (blocked path)
    else Ad Plays
        RampBolt-->>VideoAd: midpoint
        VideoAd->>MM: onMidpoint()
        RampBolt-->>VideoAd: complete
        VideoAd->>MM: onComplete()
    end
    MM->>ClientGR: dispatch join-lobby (detail.waitBeforeJoin = waitForAdComplete())
    activate ClientGR
    ClientGR->>ClientGR: await waitBeforeJoin
    ClientGR->>ClientGR: call onPrestart() and onJoin() after wait resolves
    deactivate ClientGR
    deactivate MM
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🎬 A short ad hums before the fight,
Ramp queues units in morning light,
Midpoint met, completion clear,
Promise settles — the match draws near,
Players join; the game takes flight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add video ads during ranked matchmaking' directly and clearly describes the main change in the changeset.
Description check ✅ Passed The description explains the feature's purpose and behavior, relating directly to the changeset which adds video ad integration to the matchmaking flow.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/client/VideoAd.ts`:
- Around line 60-88: The component removes the video container when isVisible is
false so subsequent showVideoAd() calls have no mount point; change
showVideoAd() to set this.isVisible = true before queuing/adding units and
modify render() to always render the <div id="precontent-video-location"> but
toggle its visibility via a CSS class or inline style (e.g.,
hidden/opacity/visibility) instead of returning html``; update any hide logic to
hide the container (set a hidden flag or class) rather than removing it so
window.ramp can mount the player on subsequent calls.
- Around line 8-33: The component currently shows the overlay immediately and
polls ramp indefinitely; change the initial state of isVisible to false, update
connectedCallback to wait for the adsEnabled decision (listen for the
"userMeResponse" event or check an existing adsEnabled flag) before calling
showVideoAd, and modify showVideoAd to add a max poll count (~100 attempts at
100ms = ~10s) so the interval clears and aborts if window.ramp or
window.ramp.que never appears; ensure loadVideoAd is still called when ramp
becomes available and also ensure isVisible is reset to true when starting an ad
so the overlay can show again after completion.
- Around line 35-58: Add a guard to prevent registering Bolt listeners more than
once in loadVideoAd(): introduce a private boolean flag (e.g.,
boltListenersAttached) on the VideoAd class, check it at the start of
loadVideoAd()/inside window.ramp.onPlayerReady and return early if true, and set
it to true immediately after successfully calling the three window.Bolt.on(...)
registrations for VIDEO_AD_UNIT_TYPE so listeners are only bound once across
disconnect/reconnect cycles.
🧹 Nitpick comments (2)
startup.sh (1)

88-90: Simplify the redundant DOMAIN/SUBDOMAIN check.

Both branches execute the same command now, so the if can be removed.

♻️ Suggested cleanup
-if [ "$DOMAIN" = openfront.dev ] && [ "$SUBDOMAIN" != main ]; then
-    exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
-else
-    exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
-fi
+exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
src/client/Main.ts (1)

182-200: Make ramp and Bolt globals optional to match the runtime guards already in code.

These window globals are loaded asynchronously and may not exist (ad-free users, CrazyGames, script timing). The type definitions currently mark them as required, but the codebase already guards against their absence with checks like if (!window.ramp) and if (window.Bolt) throughout VideoAd.ts, GutterAds.ts, and InGameHeaderAd.ts. Making them optional in the type definition will align TypeScript with the actual runtime behavior and ensure safe access patterns.

Suggested type tweaks
     ramp: {
       que: Array<() => void>;
       passiveMode: boolean;
       spaAddAds: (ads: Array<{ type: string; selectorId: string }>) => void;
       destroyUnits: (adType: string) => void;
       settings?: {
         slots?: any;
       };
       spaNewPage: (url?: string) => void;
       // Video ad methods
-      onPlayerReady: (() => void) | null;
-      addUnits: (units: Array<{ type: string }>) => Promise<void>;
-      displayUnits: () => void;
+      onPlayerReady?: (() => void) | null;
+      addUnits?: (units: Array<{ type: string }>) => Promise<void>;
+      displayUnits?: () => void;
     };
-    Bolt: {
+    Bolt?: {

Comment on lines 8 to 66
@state()
private isVisible: boolean = true;

createRenderRoot() {
return this;
}

connectedCallback() {
super.connectedCallback();
this.showVideoAd();
}

public showVideoAd(): void {
if (!window.ramp) {
// Wait for ramp to be available
const checkRamp = setInterval(() => {
if (window.ramp && window.ramp.que) {
clearInterval(checkRamp);
this.loadVideoAd();
}
}, 100);
return;
}

this.loadVideoAd();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "VideoAd.ts" -type f | head -5

Repository: openfrontio/OpenFrontIO

Length of output: 90


🏁 Script executed:

git ls-files "**/*.ts" | grep -i video | head -20

Repository: openfrontio/OpenFrontIO

Length of output: 88


🏁 Script executed:

rg "window\.ramp" --type ts -A 3 -B 3

Repository: openfrontio/OpenFrontIO

Length of output: 3489


🏁 Script executed:

rg "adsEnabled" --type ts -A 2 -B 2

Repository: openfrontio/OpenFrontIO

Length of output: 1425


🏁 Script executed:

cat -n src/client/VideoAd.ts

Repository: openfrontio/OpenFrontIO

Length of output: 2941


🏁 Script executed:

rg "onPlayerReady" --type ts -B 2 -A 2

Repository: openfrontio/OpenFrontIO

Length of output: 712


Gate video ads and add a ramp timeout to prevent stuck full-screen overlay.

The overlay displays immediately on component mount, and the ramp availability check polls indefinitely with no timeout. If Ramp never loads or ads are disabled in the app, users see a black screen they cannot close. Additionally, VideoAd lacks the adsEnabled gate that GutterAds and InGameHeaderAd already use, and it doesn't listen for the userMeResponse event to wait for the ads decision.

Also, isVisible is set to false on ad completion but never reset to true, so the component cannot display ads a second time if needed.

Please apply these fixes:

  • Wait for adsEnabled to be determined before calling showVideoAd()
  • Add a maximum poll count to showVideoAd() so the check stops after ~10 seconds
  • Initialize isVisible to false to avoid showing the overlay before ads are allowed
Suggested implementation
 `@state`()
-private isVisible: boolean = true;
+private isVisible = false;
+private rampCheckAttempts = 0;
+private readonly maxRampChecks = 100;

 connectedCallback() {
   super.connectedCallback();
-  this.showVideoAd();
+  if (window.adsEnabled === undefined) {
+    const onUserMe = () => {
+      document.removeEventListener("userMeResponse", onUserMe as EventListener);
+      if (window.adsEnabled) this.showVideoAd();
+    };
+    document.addEventListener("userMeResponse", onUserMe as EventListener);
+    return;
+  }
+  if (window.adsEnabled) this.showVideoAd();
 }

 public showVideoAd(): void {
+  if (window.adsEnabled === false) return;
   if (!window.ramp) {
     const checkRamp = setInterval(() => {
+      this.rampCheckAttempts += 1;
       if (window.ramp && window.ramp.que) {
         clearInterval(checkRamp);
         this.loadVideoAd();
+      } else if (this.rampCheckAttempts >= this.maxRampChecks) {
+        clearInterval(checkRamp);
+        console.warn("[VideoAd] ramp not available, skipping ad");
+        this.isVisible = false;
       }
     }, 100);
     return;
   }
🤖 Prompt for AI Agents
In `@src/client/VideoAd.ts` around lines 8 - 33, The component currently shows the
overlay immediately and polls ramp indefinitely; change the initial state of
isVisible to false, update connectedCallback to wait for the adsEnabled decision
(listen for the "userMeResponse" event or check an existing adsEnabled flag)
before calling showVideoAd, and modify showVideoAd to add a max poll count (~100
attempts at 100ms = ~10s) so the interval clears and aborts if window.ramp or
window.ramp.que never appears; ensure loadVideoAd is still called when ramp
becomes available and also ensure isVisible is reset to true when starting an ad
so the overlay can show again after completion.

Comment on lines 35 to 121
private loadVideoAd(): void {
// Set up event listeners when player is ready
window.ramp.onPlayerReady = () => {
if (window.Bolt) {
window.Bolt.on(VIDEO_AD_UNIT_TYPE, window.Bolt.BOLT_AD_COMPLETE, () => {
console.log("[VideoAd] Ad completed");
this.isVisible = false;
});

window.Bolt.on(VIDEO_AD_UNIT_TYPE, window.Bolt.BOLT_AD_ERROR, () => {
console.log("[VideoAd] Ad error/no fill");
this.isVisible = false;
});

window.Bolt.on(
VIDEO_AD_UNIT_TYPE,
window.Bolt.SHOW_HIDDEN_CONTAINER ?? "showHiddenContainer",
() => {
console.log("[VideoAd] Ad finished");
this.isVisible = false;
},
);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Playwire RAMP onPlayerReady API documentation multiple listeners

💡 Result:

Playwire’s RAMP “onPlayerReady” API docs don’t appear to be publicly accessible (the RAMP portal is gated behind a login), so there isn’t a public reference that confirms whether onPlayerReady supports multiple listeners out of the box. [1]

What is publicly documented is Playwire’s React RAMP wrapper, which shows the supported integration surface (loading <Ramp /> once, and placing <RampUnit /> components), but it does not document a player-event API like onPlayerReady or listener semantics. [2]

Practical guidance (when you need “multiple listeners” anyway)

If your integration exposes onPlayerReady as a single callback property (common pattern: player.onPlayerReady = fn), you typically can’t attach multiple independent listeners without overwriting. The usual solution is to register one onPlayerReady handler and fan out yourself:

  • keep an array of callbacks
  • call them all from the single onPlayerReady you wire to RAMP

If you can share the snippet you’re using (or the exact object where onPlayerReady lives—e.g., ramp, ramp.player, window.ramp, etc.), I can tell you which pattern applies and the safest way to attach multiple handlers.

Sources: [1] [2]


🏁 Script executed:

# Search for onPlayerReady usage in the codebase
rg "onPlayerReady" -t ts -t js

# Check VideoAd.ts structure to understand initialization
fd VideoAd.ts -x wc -l {}

Repository: openfrontio/OpenFrontIO

Length of output: 216


🏁 Script executed:

# Search for window.ramp usage patterns
rg "window\.ramp" -t ts -t js -A 2 -B 2

# Check if loadVideoAd is called multiple times
rg "loadVideoAd" -t ts -t js

Repository: openfrontio/OpenFrontIO

Length of output: 3072


🏁 Script executed:

# Read VideoAd.ts around the showVideoAd method and full context
sed -n '20,75p' src/client/VideoAd.ts

Repository: openfrontio/OpenFrontIO

Length of output: 1562


🏁 Script executed:

# Also check if there are other places that might call loadVideoAd indirectly
rg "showVideoAd" -t ts -t js -B 2 -A 2

Repository: openfrontio/OpenFrontIO

Length of output: 420


Prevent duplicate Bolt listener registration on reconnection.

If the component is disconnected and reconnected, loadVideoAd() executes again, registering the same three Bolt.on() listeners multiple times. Add a guard flag to ensure listeners are bound only once.

🛠️ Suggested fix
+  private boltListenersBound = false;
+
   private loadVideoAd(): void {
     // Set up event listeners when player is ready
     window.ramp.onPlayerReady = () => {
+      if (this.boltListenersBound) return;
+      this.boltListenersBound = true;
       if (window.Bolt) {
         window.Bolt.on(VIDEO_AD_UNIT_TYPE, window.Bolt.BOLT_AD_COMPLETE, () => {
           console.log("[VideoAd] Ad completed");
           this.isVisible = false;
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private loadVideoAd(): void {
// Set up event listeners when player is ready
window.ramp.onPlayerReady = () => {
if (window.Bolt) {
window.Bolt.on(VIDEO_AD_UNIT_TYPE, window.Bolt.BOLT_AD_COMPLETE, () => {
console.log("[VideoAd] Ad completed");
this.isVisible = false;
});
window.Bolt.on(VIDEO_AD_UNIT_TYPE, window.Bolt.BOLT_AD_ERROR, () => {
console.log("[VideoAd] Ad error/no fill");
this.isVisible = false;
});
window.Bolt.on(
VIDEO_AD_UNIT_TYPE,
window.Bolt.SHOW_HIDDEN_CONTAINER ?? "showHiddenContainer",
() => {
console.log("[VideoAd] Ad finished");
this.isVisible = false;
},
);
}
};
private boltListenersBound = false;
private loadVideoAd(): void {
// Set up event listeners when player is ready
window.ramp.onPlayerReady = () => {
if (this.boltListenersBound) return;
this.boltListenersBound = true;
if (window.Bolt) {
window.Bolt.on(VIDEO_AD_UNIT_TYPE, window.Bolt.BOLT_AD_COMPLETE, () => {
console.log("[VideoAd] Ad completed");
this.isVisible = false;
});
window.Bolt.on(VIDEO_AD_UNIT_TYPE, window.Bolt.BOLT_AD_ERROR, () => {
console.log("[VideoAd] Ad error/no fill");
this.isVisible = false;
});
window.Bolt.on(
VIDEO_AD_UNIT_TYPE,
window.Bolt.SHOW_HIDDEN_CONTAINER ?? "showHiddenContainer",
() => {
console.log("[VideoAd] Ad finished");
this.isVisible = false;
},
);
}
};
🤖 Prompt for AI Agents
In `@src/client/VideoAd.ts` around lines 35 - 58, Add a guard to prevent
registering Bolt listeners more than once in loadVideoAd(): introduce a private
boolean flag (e.g., boltListenersAttached) on the VideoAd class, check it at the
start of loadVideoAd()/inside window.ramp.onPlayerReady and return early if
true, and set it to true immediately after successfully calling the three
window.Bolt.on(...) registrations for VIDEO_AD_UNIT_TYPE so listeners are only
bound once across disconnect/reconnect cycles.

Comment on lines 60 to 185
// Queue the video ad initialization
window.ramp.que.push(() => {
const pwUnits = [{ type: VIDEO_AD_UNIT_TYPE }];

window.ramp
.addUnits(pwUnits)
.then(() => {
window.ramp.displayUnits();
})
.catch((e: Error) => {
console.error("[VideoAd] Error adding units:", e);
window.ramp.displayUnits();
});
});
}

render() {
if (!this.isVisible) {
return html``;
}

// Provide a container for the Playwire video player to render into
return html`
<div
id="precontent-video-location"
class="fixed inset-0 z-[99999] flex items-center justify-center"
style="background: rgba(0,0,0,0.9);"
></div>
`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

showVideoAd() can’t show a second ad after it hides itself.

Once isVisible becomes false, the container is removed and never shown again. If showVideoAd() is called later, there is no mount point. Set isVisible = true before loading, and keep the container in the DOM while hidden.

🛠️ Possible fix
   private loadVideoAd(): void {
+    this.isVisible = true;
     // Queue the video ad initialization
     window.ramp.que.push(() => {
       const pwUnits = [{ type: VIDEO_AD_UNIT_TYPE }];
@@
-  render() {
-    if (!this.isVisible) {
-      return html``;
-    }
-
-    // Provide a container for the Playwire video player to render into
-    return html`
-      <div
-        id="precontent-video-location"
-        class="fixed inset-0 z-[99999] flex items-center justify-center"
-        style="background: rgba(0,0,0,0.9);"
-      ></div>
-    `;
-  }
+  render() {
+    return html`
+      <div
+        id="precontent-video-location"
+        class="fixed inset-0 z-[99999] items-center justify-center"
+        style="background: rgba(0,0,0,0.9); display: ${this.isVisible
+          ? "flex"
+          : "none"};"
+        aria-hidden=${this.isVisible ? "false" : "true"}
+      ></div>
+    `;
+  }
🤖 Prompt for AI Agents
In `@src/client/VideoAd.ts` around lines 60 - 88, The component removes the video
container when isVisible is false so subsequent showVideoAd() calls have no
mount point; change showVideoAd() to set this.isVisible = true before
queuing/adding units and modify render() to always render the <div
id="precontent-video-location"> but toggle its visibility via a CSS class or
inline style (e.g., hidden/opacity/visibility) instead of returning html``;
update any hide logic to hide the container (set a hidden flag or class) rather
than removing it so window.ramp can mount the player on subsequent calls.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/client/VideoAd.ts`:
- Around line 20-29: The polling interval started in showVideoAd (currently
using const checkRamp = setInterval(...)) isn't cleared if the component
unmounts; change it to store the interval ID on the instance (e.g.,
this.checkRampInterval) and use that stored ID to clearInterval when ramp is
found, and also clearInterval in the component's disconnectedCallback method to
prevent the interval from running after unmount; update references to
clearInterval(checkRamp) to clearInterval(this.checkRampInterval) and ensure
loadVideoAd is only called after clearing the stored interval.
- Around line 69-72: The catch block for addUnits in VideoAd currently calls
window.ramp.displayUnits() even when addUnits fails; remove the call to
displayUnits from the error handler so that only the error is logged (keep the
console.error/ logging) and ensure displayUnits is invoked only from the
successful resolution path of addUnits (look for the promise chain around
addUnits and the surrounding code in VideoAd to place displayUnits after the
successful .then).

Comment on lines +20 to +62
public showVideoAd(): void {
if (!window.ramp) {
// Wait for ramp to be available
const checkRamp = setInterval(() => {
if (window.ramp && window.ramp.que) {
clearInterval(checkRamp);
this.loadVideoAd();
}
}, 100);
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing cleanup of polling interval if component unmounts.

If the component disconnects before window.ramp becomes available, the setInterval continues running and may call methods on a detached component.

Store the interval ID and clear it in disconnectedCallback.

Suggested fix
+  private rampCheckInterval?: ReturnType<typeof setInterval>;
+
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    if (this.rampCheckInterval) {
+      clearInterval(this.rampCheckInterval);
+      this.rampCheckInterval = undefined;
+    }
+  }
+
   public showVideoAd(): void {
     if (!window.ramp) {
       // Wait for ramp to be available
-      const checkRamp = setInterval(() => {
+      this.rampCheckInterval = setInterval(() => {
         if (window.ramp && window.ramp.que) {
-          clearInterval(checkRamp);
+          clearInterval(this.rampCheckInterval);
+          this.rampCheckInterval = undefined;
           this.loadVideoAd();
         }
       }, 100);
       return;
     }
🤖 Prompt for AI Agents
In `@src/client/VideoAd.ts` around lines 20 - 29, The polling interval started in
showVideoAd (currently using const checkRamp = setInterval(...)) isn't cleared
if the component unmounts; change it to store the interval ID on the instance
(e.g., this.checkRampInterval) and use that stored ID to clearInterval when ramp
is found, and also clearInterval in the component's disconnectedCallback method
to prevent the interval from running after unmount; update references to
clearInterval(checkRamp) to clearInterval(this.checkRampInterval) and ensure
loadVideoAd is only called after clearing the stored interval.

Comment on lines +69 to +135
.catch((e: Error) => {
console.error("[VideoAd] Error adding units:", e);
window.ramp.displayUnits();
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the VideoAd.ts file to understand the full context
cd src/client && wc -l VideoAd.ts

Repository: openfrontio/OpenFrontIO

Length of output: 80


🏁 Script executed:

# Get the context around lines 60-91 in VideoAd.ts
cat -n src/client/VideoAd.ts | sed -n '55,95p'

Repository: openfrontio/OpenFrontIO

Length of output: 1211


🏁 Script executed:

# Search for other usages of displayUnits() in the codebase
rg "displayUnits" --type ts

Repository: openfrontio/OpenFrontIO

Length of output: 237


🌐 Web query:

Playwire RAMP queue addUnits displayUnits error handling best practice

💡 Result:

What to do (best-practice pattern)

  1. Always gate RAMP calls behind the command queue (window.ramp.que)

    • Initialize window.ramp = window.ramp || {}; window.ramp.que = window.ramp.que || [];
    • Push functions onto the queue; RAMP later drains the queue and then replaces que.push so queued functions execute immediately. (This is how RAMP avoids “API not ready yet” races in async loading.) [2]
  2. Call order: addUnits() first, then displayUnits()

    • Treat addUnits() as “declare/register what units should exist on this page/view”
    • Treat displayUnits() as “render/fill now that the DOM is ready”
    • In SPAs, do this per route/view: update page context → addUnits()displayUnits() (and optionally refresh specific slots, if your integration supports it). [2]
  3. Only displayUnits() after the containers exist

    • If your containers are injected later (SPA hydration, infinite scroll), delay displayUnits() until:
      • the target element exists, and
      • it’s attached to the document (not a detached node)
    • Common approaches: MutationObserver, a “waitForElement” promise, or your framework’s “after render” hook.
  4. Error handling: fail “closed” (don’t spam) and keep the page usable

    • Wrap each queued function body with try/catch
    • Add idempotency guards so you don’t re-add/re-display the same units repeatedly on re-renders (a very common SPA bug):
      • e.g., keep a Set of unit IDs/types already registered for the current view.
    • Add light telemetry (your logger / console.warn) so you can debug without breaking UX.
  5. Timeout protection for “never becomes ready”

    • Because RAMP loads additional scripts asynchronously (the core loader injects the main PageOS script async) you should assume “might never finish” (blocked script, adblock, network). [1]
    • Put a max wait (e.g., 3–5s) around your own “wait for element then display” flow, and then just give up silently.

Reference implementation (robust queue + try/catch + SPA-safe guards)

// 1) Safe bootstrap
window.ramp = window.ramp || {};
window.ramp.que = window.ramp.que || [];

const rampSafe = (fn, label) => () => {
  try { fn(); }
  catch (e) { console.warn(`[RAMP] ${label} failed`, e); }
};

// Optional: per-view dedupe
const registered = new Set();

function registerAndDisplay(units) {
  window.ramp.que.push(rampSafe(() => {
    // 2) addUnits first (deduped)
    const toAdd = units.filter(u => {
      const key = u.id || u.type;           // adapt to your unit shape
      if (!key || registered.has(key)) return false;
      registered.add(key);
      return true;
    });

    if (toAdd.length) window.ramp.addUnits(toAdd);

    // 3) then displayUnits (only after containers exist in your code)
    window.ramp.displayUnits();
  }, "registerAndDisplay"));
}

// SPA route change:
// - clear dedupe set if your “view” is truly new
// registered.clear();
// - wait for DOM containers, then:
registerAndDisplay([{ type: "in_content" }, { type: "sidebar" }]);

Why this works: it matches how RAMP expects calls to be queued and later executed when the library is ready, and it prevents the most common SPA failure modes (double-registration, calling display before DOM exists, and uncaught exceptions breaking future queued work). [2]


Sources: RAMP core loads its main runtime asynchronously by injecting PageOS (pageos.js) [1], and RAMP processes a command queue (window.ramp.que) by executing queued functions and then making subsequent que.push execute immediately; it also exposes/uses methods like window.ramp.addUnits(...) in that flow. [2]


Remove displayUnits() call from the error handler

When addUnits() fails, the units aren't registered. Calling displayUnits() afterward tries to display units that don't exist. The recommended pattern is: call displayUnits() only after successful addUnits().

Current problematic pattern:
.catch((e: Error) => {
  console.error("[VideoAd] Error adding units:", e);
  window.ramp.displayUnits();  // ← Remove this
});

Just log the error and skip displaying when units fail to add.

🤖 Prompt for AI Agents
In `@src/client/VideoAd.ts` around lines 69 - 72, The catch block for addUnits in
VideoAd currently calls window.ramp.displayUnits() even when addUnits fails;
remove the call to displayUnits from the error handler so that only the error is
logged (keep the console.error/ logging) and ensure displayUnits is invoked only
from the successful resolution path of addUnits (look for the promise chain
around addUnits and the surrounding code in VideoAd to place displayUnits after
the successful .then).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/ClientGameRunner.ts (1)

67-116: Await or safely fire-and-forget onPrestart.

onPrestart is now async but is called without await or error handling. If it rejects (or waits on waitUntil), you can get unhandled rejections and racey UI state. Since onmessage is async already, it’s safer to await it (or explicitly catch/log).

💡 Possible fix
-      onPrestart();
+      await onPrestart();
...
-      onPrestart();
+      await onPrestart();
🤖 Fix all issues with AI agents
In `@src/client/Matchmaking.ts`:
- Around line 54-78: waitForAdComplete currently treats any falsy
window.adsEnabled as "ads disabled" and resolves early; change it so it only
resolves immediately when window.adsEnabled === false or this.videoAdComplete
=== true, and otherwise wait for either the ad to finish (via
adCompleteResolve/handleAdComplete) or for a definitive ads-ready signal such as
the existing userMeResponse (or a dedicated ads-ready event/promise). Update
waitForAdComplete to listen for userMeResponse (or an ads-ready flag) when
window.adsEnabled is undefined, and only resolve when that response indicates
ads are disabled or when handleAdComplete triggers adCompleteResolve; also
ensure renderVideoAd still uses window.adsEnabled === true to render the
<video-ad>.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/Matchmaking.ts (1)

265-272: Resolve pending ad waits when the modal closes.

If the modal closes while waiting for midpoint/complete, the promises can hang forever. Resolve and clear them on close so the awaiting code can exit cleanly.

♻️ Proposed fix
  protected onClose(): void {
    this.connected = false;
    this.socket?.close();
+   if (this.adMidpointResolve) {
+     this.adMidpointResolve();
+     this.adMidpointResolve = null;
+   }
+   if (this.adCompleteResolve) {
+     this.adCompleteResolve();
+     this.adCompleteResolve = null;
+   }
    if (this.gameCheckInterval) {
      clearInterval(this.gameCheckInterval);
      this.gameCheckInterval = null;
    }
  }
🧹 Nitpick comments (1)
src/client/Matchmaking.ts (1)

84-91: Avoid console logging in render.

renderVideoAd logs every render; this is noisy in prod. Prefer a debug flag or remove the log.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/client/ClientGameRunner.ts`:
- Around line 109-117: The code currently attaches
waitBeforeJoin.then(onPrestart) in both the "prestart" and "start" message
handlers which can call onPrestart twice and also swallows rejections; fix by
attaching the handler only once (e.g., add a boolean flag like prestartAttached
and set it when you first call waitBeforeJoin.then(...)) and change the
attachment to waitBeforeJoin.then(onPrestart).catch(err => {
console.error("waitBeforeJoin failed", err); onPrestart(); }) so any rejection
is logged and onPrestart still runs; apply these changes in the message handling
for message.type "prestart" and "start" where waitBeforeJoin and onPrestart are
used.

In `@src/client/Matchmaking.ts`:
- Around line 250-251: Fix the typo in the console log inside Matchmaking.ts:
update the console.log call that prints "Matchmaking server  closed connection"
(the on-close/connection handler) to use a single space so the message reads
"Matchmaking server closed connection"; locate the console.log in the connection
close handler and correct the string.
🧹 Nitpick comments (1)
src/client/Main.ts (1)

791-791: Unnecessary async keyword.

This callback is marked async but contains no await. Either remove async or add a comment if this is intentional for future use.

Comment on lines +109 to +117
waitBeforeJoin.then(onPrestart);
}
if (message.type === "start") {
// Trigger prestart for singleplayer games
onPrestart();
waitBeforeJoin.then(onPrestart);
console.log(
`lobby: game started: ${JSON.stringify(message, replacer, 2)}`,
);
await waitBeforeJoin;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential double onPrestart call and missing rejection handling.

When a prestart message arrives (line 109), you fire waitBeforeJoin.then(onPrestart). Then when start arrives (line 113), you fire it again. If both messages arrive in the same session, onPrestart runs twice.

Also, .then(onPrestart) with no .catch() silently swallows any rejection from waitBeforeJoin. If the ad flow fails, onPrestart never fires and the user sees a stuck loading screen.

Suggested fix
   if (message.type === "prestart") {
     console.log(
       `lobby: game prestarting: ${JSON.stringify(message, replacer)}`,
     );
     terrainLoad = loadTerrainMap(
       message.gameMap,
       message.gameMapSize,
       terrainMapFileLoader,
     );
-    waitBeforeJoin.then(onPrestart);
+    waitBeforeJoin.then(onPrestart).catch((e) => {
+      console.error("waitBeforeJoin failed during prestart:", e);
+      onPrestart(); // Proceed anyway so game doesn't hang
+    });
   }
   if (message.type === "start") {
     // Trigger prestart for singleplayer games
-    waitBeforeJoin.then(onPrestart);
+    // Only call onPrestart here if prestart message was never received
+    // (singleplayer case). Consider tracking whether onPrestart was already queued.
+    waitBeforeJoin.then(onPrestart).catch((e) => {
+      console.error("waitBeforeJoin failed during start:", e);
+      onPrestart();
+    });
🤖 Prompt for AI Agents
In `@src/client/ClientGameRunner.ts` around lines 109 - 117, The code currently
attaches waitBeforeJoin.then(onPrestart) in both the "prestart" and "start"
message handlers which can call onPrestart twice and also swallows rejections;
fix by attaching the handler only once (e.g., add a boolean flag like
prestartAttached and set it when you first call waitBeforeJoin.then(...)) and
change the attachment to waitBeforeJoin.then(onPrestart).catch(err => {
console.error("waitBeforeJoin failed", err); onPrestart(); }) so any rejection
is logged and onPrestart still runs; apply these changes in the message handling
for message.type "prestart" and "start" where waitBeforeJoin and onPrestart are
used.

Comment on lines 250 to 255
console.log("Matchmaking server closed connection");
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo: double space in log message.

Minor: "Matchmaking server closed" has two spaces.

-      console.log("Matchmaking server  closed connection");
+      console.log("Matchmaking server closed connection");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log("Matchmaking server closed connection");
};
console.log("Matchmaking server closed connection");
};
🤖 Prompt for AI Agents
In `@src/client/Matchmaking.ts` around lines 250 - 251, Fix the typo in the
console log inside Matchmaking.ts: update the console.log call that prints
"Matchmaking server  closed connection" (the on-close/connection handler) to use
a single space so the message reads "Matchmaking server closed connection";
locate the console.log in the connection close handler and correct the string.

@evanpelle evanpelle changed the title video ads Add video ads during ranked matchmaking Jan 28, 2026
@evanpelle evanpelle added this to the v29 milestone Jan 28, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/client/ClientGameRunner.ts`:
- Around line 117-118: The await of waitBeforeJoin inside the async onmessage
handler can reject and leave unhandled rejections; wrap the await waitBeforeJoin
in a try-catch (or append .catch()) inside the onmessage handler in
ClientGameRunner so that onJoin() is only called on success and the catch
handles the error (e.g., log via process/logger, call any existing error
handler, and/or close the transport/session) to prevent bubbling unhandled
rejections; update the block containing waitBeforeJoin and onJoin to perform
this protected await and error handling.

In `@src/client/components/VideoAd.ts`:
- Line 78: Currently the code assigns a new callback to
window.ramp.onPlayerReady, which clobbers any existing listener; change this to
preserve and call the previous callback by capturing the existing handler (e.g.,
const prev = window.ramp.onPlayerReady) and then assign a new function to
window.ramp.onPlayerReady that first invokes prev (if present) and then runs the
VideoAd-specific logic in this component (look for window.ramp.onPlayerReady in
VideoAd.ts to update the assignment); ensure the wrapper correctly binds context
and handles errors from the previous handler.
🧹 Nitpick comments (1)
src/client/components/VideoAd.ts (1)

4-4: Remove debug console.log statements before release.

Multiple console.log calls exist for debugging purposes. These should be removed or gated behind a debug flag for production.

Also applies to: 34-34, 169-169

Comment on lines +117 to 118
await waitBeforeJoin;
onJoin();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unhandled rejection if waitBeforeJoin fails at game start.

If waitBeforeJoin rejects at line 117, the error bubbles up from this async onmessage handler. The transport callback may not handle async rejections, leaving the user stuck without feedback.

Wrap in try-catch or use .catch():

Suggested fix
     if (message.type === "start") {
       // Trigger prestart for singleplayer games
       waitBeforeJoin.then(onPrestart);
       console.log(
         `lobby: game started: ${JSON.stringify(message, replacer, 2)}`,
       );
-      await waitBeforeJoin;
-      onJoin();
+      try {
+        await waitBeforeJoin;
+      } catch (e) {
+        console.error("waitBeforeJoin failed, proceeding anyway:", e);
+      }
+      onJoin();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await waitBeforeJoin;
onJoin();
try {
await waitBeforeJoin;
} catch (e) {
console.error("waitBeforeJoin failed, proceeding anyway:", e);
}
onJoin();
🤖 Prompt for AI Agents
In `@src/client/ClientGameRunner.ts` around lines 117 - 118, The await of
waitBeforeJoin inside the async onmessage handler can reject and leave unhandled
rejections; wrap the await waitBeforeJoin in a try-catch (or append .catch())
inside the onmessage handler in ClientGameRunner so that onJoin() is only called
on success and the catch handles the error (e.g., log via process/logger, call
any existing error handler, and/or close the transport/session) to prevent
bubbling unhandled rejections; update the block containing waitBeforeJoin and
onJoin to perform this protected await and error handling.

}, VideoAd.AD_LOAD_TIMEOUT_MS);

// Set up event listeners when player is ready
window.ramp.onPlayerReady = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Overwriting onPlayerReady may break other listeners.

Setting window.ramp.onPlayerReady = () => {...} replaces any existing callback. If other code in the app relies on this callback, it will be lost. Consider preserving the existing callback:

Suggested fix
-    window.ramp.onPlayerReady = () => {
+    const existingCallback = window.ramp.onPlayerReady;
+    window.ramp.onPlayerReady = () => {
+      if (existingCallback) existingCallback();
       if (window.Bolt) {
         // ... listener registrations
       }
     };
🤖 Prompt for AI Agents
In `@src/client/components/VideoAd.ts` at line 78, Currently the code assigns a
new callback to window.ramp.onPlayerReady, which clobbers any existing listener;
change this to preserve and call the previous callback by capturing the existing
handler (e.g., const prev = window.ramp.onPlayerReady) and then assign a new
function to window.ramp.onPlayerReady that first invokes prev (if present) and
then runs the VideoAd-specific logic in this component (look for
window.ramp.onPlayerReady in VideoAd.ts to update the assignment); ensure the
wrapper correctly binds context and handles errors from the previous handler.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/Matchmaking.ts (1)

218-237: ⚠️ Potential issue | 🔴 Critical

Fix indentation - this is causing the Prettier pipeline failure.

Lines 218-229 have broken indentation. The await this.waitForAdMidpoint() block and this.socket.send(...) appear to be at the wrong nesting level relative to the setTimeout callback.

Suggested fix
       this.connectTimeout = setTimeout(async () => {
         if (this.socket?.readyState !== WebSocket.OPEN) {
           console.warn("[Matchmaking] socket not ready");
           return;
         }
         // Set a delay so the user can see the "connecting" message,
         // otherwise the "searching" message will be shown immediately.
         // Also wait so people who back out immediately aren't added
         // to the matchmaking queue.
-  
-      // Wait for ad midpoint before sending join request
-      // This is so the ad doesn't get delay game start too long.
-      await this.waitForAdMidpoint();
-
-      // Early return if modal was closed while waiting for ad
-      if (!this.isModalOpen) {
-        this.socket?.close();
-        return;
-      }
-
-      this.socket.send(
+
+        // Wait for ad midpoint before sending join request
+        // This is so the ad doesn't delay game start too long.
+        await this.waitForAdMidpoint();
+
+        // Early return if modal was closed while waiting for ad
+        if (!this.isModalOpen) {
+          this.socket?.close();
+          return;
+        }
+
+        this.socket.send(
           JSON.stringify({
             type: "join",
             jwt: await getPlayToken(),
           }),
         );
         this.connected = true;
         this.requestUpdate();
       }, 2000);

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/Matchmaking.ts (1)

336-346: ⚠️ Potential issue | 🟡 Minor

Verify rejection handling for waitBeforeJoin in the message handler.

The waitBeforeJoin promise is correctly passed from the event handler in Main.ts (line 873) to joinLobby() in ClientGameRunner.ts. However, in ClientGameRunner.ts lines 109, 113, and 117, the promise is used with .then() and await without explicit rejection handling (no .catch() or try-catch). If the promise rejects, the rejection will propagate unhandled in the message handler. Ensure rejection is caught and handled appropriately.

🤖 Fix all issues with AI agents
In `@src/client/Matchmaking.ts`:
- Around line 62-80: The resolvers stored by waitForAdComplete and
waitForAdMidpoint (adCompleteResolve and adMidpointResolve) are never cleared if
the modal closes, causing callers like joinLobby to hang; update the modal's
onClose() to call these stored resolve functions (or reject them) if present and
then null out adCompleteResolve and adMidpointResolve so pending Promises are
settled and references cleaned up; locate the instance fields and methods by
name (waitForAdComplete, waitForAdMidpoint, adCompleteResolve,
adMidpointResolve, onClose) and ensure onClose always settles and clears both
resolvers.
- Around line 94-106: The adBlocked branch renders a message but no way to exit;
update the JSX in Matchmaking.ts where this.adBlocked is handled to include a
dismiss control (e.g., a "Close" or "Dismiss" button) next to the message that
invokes the component's existing modal close/dismiss logic (call the existing
close handler such as this.close() or this.closeModal(), or dispatch a 'close'
event if that's the pattern used elsewhere), ensure the button text uses
translateText("matchmaking_modal.close_button") (or add that key) and has
appropriate styling and accessibility attributes so users can exit the modal
when ads are blocked.

Comment on lines +62 to +80
private waitForAdComplete = (): Promise<void> => {
// If ad is already complete or ads are disabled, resolve immediately
if (this.videoAdComplete || !window.adsEnabled) {
return Promise.resolve();
}
return new Promise((resolve) => {
this.adCompleteResolve = resolve;
});
};

private waitForAdMidpoint = (): Promise<void> => {
// If midpoint already reached or ads are disabled, resolve immediately
if (this.adMidpointReached || !window.adsEnabled) {
return Promise.resolve();
}
return new Promise((resolve) => {
this.adMidpointResolve = resolve;
});
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Promise resolvers not cleaned up when modal closes.

waitForAdComplete() and waitForAdMidpoint() store resolve functions in instance fields. If the user closes the modal before the ad finishes, these promises never resolve. The caller (e.g., joinLobby in ClientGameRunner) will wait forever on await waitBeforeJoin.

Clean up the resolvers in onClose():

Suggested fix in onClose()
   protected onClose(): void {
     this.connected = false;
     this.socket?.close();
+    // Resolve any pending ad promises so callers don't hang
+    if (this.adCompleteResolve) {
+      this.adCompleteResolve();
+      this.adCompleteResolve = null;
+    }
+    if (this.adMidpointResolve) {
+      this.adMidpointResolve();
+      this.adMidpointResolve = null;
+    }
     if (this.connectTimeout) {
       clearTimeout(this.connectTimeout);
       this.connectTimeout = null;
     }
🤖 Prompt for AI Agents
In `@src/client/Matchmaking.ts` around lines 62 - 80, The resolvers stored by
waitForAdComplete and waitForAdMidpoint (adCompleteResolve and
adMidpointResolve) are never cleared if the modal closes, causing callers like
joinLobby to hang; update the modal's onClose() to call these stored resolve
functions (or reject them) if present and then null out adCompleteResolve and
adMidpointResolve so pending Promises are settled and references cleaned up;
locate the instance fields and methods by name (waitForAdComplete,
waitForAdMidpoint, adCompleteResolve, adMidpointResolve, onClose) and ensure
onClose always settles and clears both resolvers.

Comment on lines +94 to +106
if (this.adBlocked) {
return html`
<div
class="w-full flex flex-col items-center justify-center mb-4 px-6 py-8 bg-red-900/30 border border-red-500/50 rounded-lg"
>
<p class="text-red-400 text-lg font-semibold text-center mb-2">
${translateText("matchmaking_modal.ad_blocked_title")}
</p>
<p class="text-white/70 text-sm text-center">
${translateText("matchmaking_modal.ad_blocked_message")}
</p>
</div>
`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ad blocked state shows message but user has no way to proceed.

When adBlocked is true, the user sees "Ad blocked" text but there is no button to close the modal, dismiss the message, or navigate elsewhere. The user is stuck.

Add a close button or link so the user can exit:

Suggested fix
       if (this.adBlocked) {
         return html`
           <div
             class="w-full flex flex-col items-center justify-center mb-4 px-6 py-8 bg-red-900/30 border border-red-500/50 rounded-lg"
           >
             <p class="text-red-400 text-lg font-semibold text-center mb-2">
               ${translateText("matchmaking_modal.ad_blocked_title")}
             </p>
             <p class="text-white/70 text-sm text-center">
               ${translateText("matchmaking_modal.ad_blocked_message")}
             </p>
+            <button
+              `@click`="${this.close}"
+              class="mt-4 px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg transition"
+            >
+              ${translateText("common.back")}
+            </button>
           </div>
         `;
       }
🤖 Prompt for AI Agents
In `@src/client/Matchmaking.ts` around lines 94 - 106, The adBlocked branch
renders a message but no way to exit; update the JSX in Matchmaking.ts where
this.adBlocked is handled to include a dismiss control (e.g., a "Close" or
"Dismiss" button) next to the message that invokes the component's existing
modal close/dismiss logic (call the existing close handler such as this.close()
or this.closeModal(), or dispatch a 'close' event if that's the pattern used
elsewhere), ensure the button text uses
translateText("matchmaking_modal.close_button") (or add that key) and has
appropriate styling and accessibility attributes so users can exit the modal
when ads are blocked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

1 participant