mraid checker
This commit is contained in:
@@ -73,18 +73,19 @@ const offY = (viewH - 1920 * s) / 2
|
||||
## CTA — SDK Priority (triggerCTA fallback chain)
|
||||
```
|
||||
1. ExitApi.exit() → GoogleAds (gg)
|
||||
2. FbPlayableAd.onCTAClick() → Facebook (fb)
|
||||
2. FbPlayableAd.onCTAClick() → Facebook (fb) · Moloco (mo)
|
||||
3. Luna.Unity.Playable → Unity (un)
|
||||
4. playableSDK.openAppStore() → (runtime fallback if SDK present)
|
||||
5. window.install() → Mintegral (mtg)
|
||||
6. window.openAppStore() → (runtime fallback)
|
||||
7. window.clickTag → Moloco (mo)
|
||||
7. window.clickTag → Moloco (mo) fallback
|
||||
8. window.__VUNGLE__ → Vungle (vu) via parent.postMessage
|
||||
9. window.__TIKTOK__ → TikTok (tt) → openAppStore() or window.open fallback
|
||||
10. mraid.open(url) → Applovin (al) / Ironsource (is)
|
||||
10. mraid.open(url) → Applovin (al) / Ironsource (is) / Unity (un fallback after Luna)
|
||||
11. window.open(storeUrl) → Fallback (all others)
|
||||
```
|
||||
`notifyGameClose()` fires before every CTA redirect (all networks, no-op when SDK absent).
|
||||
MRAID CTA calls must be guarded: require `typeof mraid.open === 'function'`; treat missing `mraid.getState()` as `ready`; if state is still `loading`, fall through to `window.open(storeUrl)`.
|
||||
|
||||
## Analytics — SDK Priority
|
||||
```
|
||||
@@ -107,15 +108,63 @@ gameClose() → notifyGameClose() in cta.ts redirectToStore(), before triggerCT
|
||||
### Pause / Resume / Mute (bindLifecycle in networks.ts)
|
||||
| Network | Mechanism |
|
||||
|---|---|
|
||||
| Unity | `luna:mute` / `luna:unmute` / `luna:pause` / `luna:resume` events |
|
||||
| Unity | `luna:mute` / `luna:unmute` / `luna:pause` / `luna:resume` events; also shared MRAID exposure/viewability when `mraid.js` is present |
|
||||
| Mintegral | `window.message` → `onPause` / `onResume` |
|
||||
| Vungle | `ad-event-pause` / `ad-event-resume` events |
|
||||
| Applovin / Ironsource | MRAID viewable state (gated in initMraid) |
|
||||
| Applovin / Ironsource / Unity | MRAID 3.0 `exposureChange`, MRAID 2.0 fallback `viewableChange` / `isViewable`, and `audioVolumeChange` (gated in `initMraid`) |
|
||||
| GoogleAds / Facebook / Moloco | No SDK pause — browser handles visibility |
|
||||
|
||||
### MRAID 3.0 Requirements & Best Practices
|
||||
MRAID networks are **Applovin (`al`)**, **Ironsource (`is`)**, and **Unity (`un`)**. All three must request `<script src="mraid.js"></script>` early in the generated HTML. Unity still uses Luna first for CTA/lifecycle where available, but MRAID must exist as the standards fallback.
|
||||
|
||||
Creative requirements from the MRAID 3.0 spec and Best Practices Guide:
|
||||
- Request `mraid.js` as early as possible, exactly once, either with `<script src="mraid.js"></script>` or DOM insertion. Do not rely on the container to inject it automatically.
|
||||
- Wait for both DOM readiness and MRAID readiness before starting rich media behavior. Use `document.readyState` / `DOMContentLoaded` or `load` for the DOM side.
|
||||
- Use `mraid.getState()` together with `mraid.addEventListener('ready', ...)` so the creative still starts when the container fires `ready` before the listener attaches.
|
||||
- Guard access to `mraid` and MRAID methods with `typeof` checks. Treat missing optional methods as unsupported and fall back gracefully.
|
||||
- Use `mraid.open(url)` for MRAID clickthroughs. Avoid `<a href>`, `location.href` / `assign` / `replace`, and unguarded `window.open`.
|
||||
- Include `<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">` or an equivalent mobile viewport tag.
|
||||
- Do not assume nested iframes can access native MRAID. If nested iframe access is required, the outer frame must provide its own bridge.
|
||||
- Use `exposureChange` as the MRAID 3.0 viewability signal, then degrade to `viewableChange` / `isViewable()` for MRAID 2.0 compatibility.
|
||||
- Listen for `audioVolumeChange`; `null` is valid and must be ignored. Only apply volume math when the value is numeric.
|
||||
- Listen for `error`; if the ad cannot run or assets fail, gracefully degrade or call `mraid.unload()` instead of showing a broken/blank ad.
|
||||
- Call `setResizeProperties()` before `resize()`. Do not use `resize()` for full-screen takeovers; use `expand()` for that case.
|
||||
- Avoid resizing or expanding interstitials. Check placement with `getPlacementType()` when one creative can serve inline and interstitial placements.
|
||||
- Avoid `useCustomClose()` and custom close indicators in MRAID 3.0. The host provides the close control.
|
||||
- Avoid two-part expandable ads (`expand(url)`); use self-contained one-part expandables.
|
||||
- Call `supports()` before optional/native features such as `storePicture`, `createCalendarEvent`, `getLocation`, VPAID, SMS/tel, or inline video behavior.
|
||||
- Use `playVideo()` for native video playback cases instead of `mraid.open(videoUrl)`. Use HTML5 `<video>` for inline playback when supported.
|
||||
|
||||
Implementation requirements for `networks.ts` / `networks.js`:
|
||||
- Export `initMraid(timeoutMs = 2000, detectTimeoutMs = 500)` and call it at module load.
|
||||
- If `window.mraid` is missing at module load, poll briefly (`detectTimeoutMs`, default 500 ms) for late container injection before resolving as no-MRAID.
|
||||
- If MRAID exists and `mraid.getState() === 'loading'`, wait for `mraid.addEventListener('ready', ...)`; self-resolve after `timeoutMs` (default 2000 ms) so startup never hangs.
|
||||
- `main` / boot must `await initMraid()` before constructing `new Phaser.Game(...)`.
|
||||
- On setup, seed cached visibility from `mraid.isViewable()` when available.
|
||||
- Register MRAID callbacks once, before any Phaser scene is ready:
|
||||
- `mraid.addEventListener('error', (message, action) => ...)` — log or gracefully handle failed/unsupported MRAID calls.
|
||||
- `mraid.addEventListener('stateChange', state => ...)` — required when using state-changing MRAID behavior such as `close`, `expand`, or `resize`.
|
||||
- `mraid.addEventListener('exposureChange', exposedPercentage => ...)` — MRAID 3.0 primary viewability. Pause when `exposedPercentage <= 0`; resume when positive.
|
||||
- `mraid.addEventListener('viewableChange', viewable => ...)` — MRAID 2.0 compatibility fallback.
|
||||
- `mraid.addEventListener('audioVolumeChange', pct => ...)` — only apply `pct / 100` when `typeof pct === 'number'`.
|
||||
- In `bindLifecycle(scene)`, apply cached hidden/non-exposed MRAID state immediately once a scene exists.
|
||||
- If the creative renders its own close control, wire that control to a guarded `mraid.close()` helper and call `notifyGameClose()` before closing.
|
||||
|
||||
## Build
|
||||
Vite outputs IIFE format (`format: 'iife'`, `modulePreload: false`).
|
||||
Build script strips `type="module"` and `crossorigin` — ad networks reject ES modules.
|
||||
MRAID networks (`al`, `is`, `un`) inject exactly one `<script src="mraid.js"></script>` request.
|
||||
If built output contains `console.error` from Phaser or bundled node modules, prefer dropping it at bundle time instead of editing library source. Add this option to the shared Vite config, usually `vite.shared.js`:
|
||||
|
||||
```js
|
||||
export default {
|
||||
esbuild: {
|
||||
pure: ['console.error'],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Keep project-authored error handling meaningful: use visible fallback UI, `console.warn`, or guarded debug logging where needed.
|
||||
|
||||
| Network | Tag | Injected | Zipped | Included |
|
||||
|---|---|---|---|---|
|
||||
@@ -124,7 +173,7 @@ Build script strips `type="module"` and `crossorigin` — ad networks reject ES
|
||||
| Ironsource | is | `mraid.js` | — | ✓ |
|
||||
| Mintegral | mtg | `onload="gameReady()"` | ✓ | ✓ |
|
||||
| Facebook | fb | — | — | ✓ |
|
||||
| Unity | un | — | — | ✓ |
|
||||
| Unity | un | `mraid.js` | — | ✓ |
|
||||
| Vungle | vu | `window.__VUNGLE__=true` | ✓ | ✓ |
|
||||
| Moloco | mo | — | — | ✓ |
|
||||
| TikTok | tt | `window.__TIKTOK__=true` | — | — |
|
||||
@@ -146,7 +195,6 @@ dist/
|
||||
ws_mip_grhpl_wbinspiredvar1_01_real_na_noseason_en_2words_na_al.html
|
||||
Google/
|
||||
ws_mip_grhpl_wbinspiredvar1_01_real_na_noseason_en_2words_na_gg.zip
|
||||
index.zip
|
||||
Ironsource/
|
||||
ws_mip_grhpl_wbinspiredvar1_01_real_na_noseason_en_2words_na_is.html
|
||||
…one HTML or ZIP per network…
|
||||
@@ -213,6 +261,7 @@ Define depths as named constants in [constants.ts](src/constants.ts) — never u
|
||||
| MRAID crash | `typeof mraid !== 'undefined'` guard |
|
||||
| Assets not inlined | `assetsInlineLimit: 100_000_000` |
|
||||
| Bundle > 5 MB | WebP q75, shorter audio |
|
||||
| Built output contains library `console.error` | If `console.error` comes from Phaser or bundled node modules, add `esbuild: { pure: ['console.error'] }` in `vite.shared.js` so it is dropped at bundle time. Do not edit library source. |
|
||||
| Audio blocked | Gate on `pointerdown` |
|
||||
| Letterbox | Avoid `Scale.FIT/EXPAND/CENTER_BOTH` |
|
||||
| CORS / module error | `format: 'iife'` + strip `type="module"` and `crossorigin` |
|
||||
@@ -227,6 +276,46 @@ Define depths as named constants in [constants.ts](src/constants.ts) — never u
|
||||
| Previewer black screen with `Cannot read properties of null (reading 'appendChild')` in Phaser boot | Two compounding causes: (1) `vite-plugin-singlefile` hoists the bundled script into `<head>`, and the build strips `type="module"`, so the script runs **synchronously before `<body>` is parsed** — `document.body` is null. Wrap main.ts in a `DOMContentLoaded` gate (`if (document.readyState === 'loading') addEventListener('DOMContentLoaded', boot) else boot()`). (2) `parent: 'game'` (string selector) fails when the previewer sandbox strips custom DOM; create `<div id="game">` at runtime inside `boot()` and pass the **element reference** (not the string) to `Phaser.Game({ parent })`. The cascading `ScaleManager.resize → Cannot set properties of undefined (setting 'width')` errors all trace back to `game.canvas` never being created when either of these fail. |
|
||||
| NineSlice won't shrink in landscape | Phaser's `NineSlice` silently clamps its minimum dimensions to `(leftWidth + rightWidth) × (topHeight + bottomHeight)` **in source pixels**. Symptom: one UI chrome element stays huge in landscape while every other `sd()`-driven element rescales correctly — because the requested `sd(H)` is below the inset total. Shrinking the insets fixes the scale clamp but stretches the art's rounded corners. The right fix: **operate the NineSlice in source-pixel space and scale it uniformly with `setScale(sd(1))`**. Keep `setSize(wSrc, hSrc)` in source px so the center stretches horizontally with text growth while corners stay intact; the `sd(1)` scale is what makes the whole object shrink in landscape, bypassing the min-dim clamp entirely. When reading `this.preview.width` (which is in scaled px) to decide `wSrc`, divide by `sd(1)` to convert back to source space. Tween callers that reset `setScale(1)` also need updating — the bg's base scale is now `sd(1)`, not 1. See [SelectionLine.ts](src/game/SelectionLine.ts) for the pattern. |
|
||||
| Unity (un) submission: "not allowed to use window.top" | Luna's static scan flags the literal `window.top`, which Phaser 3 ships internally. In `build-all.mjs`, rewrite `window.top` → `window.self` for the `un` HTML only (other networks keep the original). Safe inside the playable's own frame because the iframe-vs-top check collapses to always-top. |
|
||||
| MRAID network missing `mraid.js` | MRAID 3.0 creatives must request `mraid.js` early. Inject `<script src="mraid.js"></script>` exactly once for Applovin (`al`), Ironsource (`is`), and Unity (`un`), even if the container may also expose runtime MRAID objects. |
|
||||
| MRAID listener wired too late | Previewers can probe MRAID before any Phaser scene is ready — if listeners are wired only inside `bindLifecycle` / scene `create()`, the creative can miss `ready`, `exposureChange`, or `viewableChange`. Fix: call `initMraid()` at `networks.ts` module-load time, guarded by `getState() === 'loading'` → `addEventListener('ready', setupMraid)` vs immediate call. Cache `_mraidViewable` / `_mraidExposed`; in `bindLifecycle` apply cached hidden/non-exposed state immediately. |
|
||||
| MRAID 3.0 `exposureChange` not handled | MRAID 3.0 containers dispatch `exposureChange` with `exposedPercentage` directly instead of — or in addition to — `viewableChange`. If only listening to `viewableChange`, the ad may never receive pause signals on newer AL/IS/Unity builds. Register both: `mraid.addEventListener('exposureChange', exposedPercentage => onExposure(exposedPercentage > 0))` and `mraid.addEventListener('viewableChange', onViewable)` inside `initMraid()`. Seed initial state from `mraid.isViewable()` on ready. |
|
||||
| MRAID 3.0 `audioVolumeChange` not handled | MRAID 3.0 containers fire `audioVolumeChange` with a volume level (0–100) or `null` when unavailable. Without a listener, the ad ignores host-level mute/unmute signals; if `null` is divided by 100, the ad can mute accidentally. Register `mraid.addEventListener('audioVolumeChange', v => { if (typeof v === 'number') scene.sound.setVolume(v / 100) })` in `initMraid()`. |
|
||||
| MRAID `error` listener missing | Validators expect failed or unsupported MRAID calls to be observable. Register `mraid.addEventListener('error', (message, action) => console.warn('[MRAID error]', { message, action }))` during early MRAID setup. |
|
||||
| MRAID `stateChange` listener missing | If the creative uses state-changing container behavior such as `mraid.close()`, `expand`, or `resize`, validators expect a `stateChange` listener. Register `mraid.addEventListener('stateChange', state => console.log('[MRAID stateChange]', state))` with the other early MRAID callbacks. |
|
||||
| Custom close control not wired to MRAID | If the creative renders its own close button, the button must call a guarded close helper: `notifyGameClose()` then `mraid.close()` when `typeof mraid.close === 'function'` and `mraid.getState() !== 'loading'`. Do not use CTA/install buttons as close controls. |
|
||||
| MRAID init not awaited before Phaser boot | If `new Phaser.Game()` is created before `initMraid()` resolves, the scene may fire `create()` before MRAID state is known — `bindLifecycle` then cannot apply a cached hidden/paused state. Make `boot()` async and `await initMraid()` before constructing the Phaser game. `initMraid()` must self-resolve after a timeout (~2 s) so a missing or non-firing MRAID container never blocks startup. |
|
||||
| Late MRAID injection not handled | Some containers inject `window.mraid` after the script executes — a synchronous `typeof window.mraid !== 'undefined'` check at module load returns false even though MRAID will be present. After the immediate check fails, poll briefly (~500 ms) before concluding no-MRAID, then proceed. |
|
||||
| Unsafe optional MRAID API calls | `useCustomClose()` was removed in MRAID 3.0, and APIs such as `expand`, `resize`, `storePicture`, and `createCalendarEvent` are optional/capability-dependent. Do not call them unless the project intentionally supports the feature and guards every call with `typeof mraid.method === 'function'` / `mraid.supports(...)` where relevant. |
|
||||
|
||||
## Ironsource — Runtime Analysis
|
||||
Ironsource requires every `_is.html` build to pass **LevelPlay → Creative Management → Playable Workshop** (Runtime Analysis / Playable Validator) before launch. Skipping this can get the ad disabled. The validator runs in an iOS-like WKWebView sandbox and exposes console errors — use it as a free diagnostic channel for iPhone-only failures when a Mac + Safari remote-debug isn't available.
|
||||
|
||||
## Moloco — Upload Checklist & CTA Implementation
|
||||
Creative specs: HTML5 (`.html` / `.htm`), < 5 MB, portrait + landscape.
|
||||
|
||||
Before uploading to Moloco Ads, confirm all three:
|
||||
- [ ] No `XMLHttpRequest` anywhere in the ad (Moloco rejects it outright)
|
||||
- [ ] CTA calls `FbPlayableAd.onCTAClick()` with no parameters — Moloco uses this for store redirect (not `window.clickTag`)
|
||||
- [ ] No JavaScript redirects in ad file assets
|
||||
|
||||
### CTA implementation options
|
||||
|
||||
**Option 1 — event listener**
|
||||
```html
|
||||
<button id="ctaButton">Install</button>
|
||||
<script>
|
||||
document.getElementById('ctaButton').addEventListener('click', function() {
|
||||
FbPlayableAd.onCTAClick();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
**Option 2 — inline onClick**
|
||||
```html
|
||||
<button onclick="FbPlayableAd.onCTAClick()">Install</button>
|
||||
```
|
||||
|
||||
Moloco's validator requires `FbPlayableAd.onCTAClick()` — this is why it shares priority #2 in the CTA chain with Facebook. `window.clickTag` is kept as a fallback at priority #7.
|
||||
|
||||
***EOF***
|
||||
[05/28/2026]
|
||||
|
||||
BIN
aiAssets/MRAID-3.0-Best-Practices-Guide.pdf
Normal file
BIN
aiAssets/MRAID-3.0-Best-Practices-Guide.pdf
Normal file
Binary file not shown.
BIN
aiAssets/MRAID_3.0_FINAL_June_2018.pdf
Normal file
BIN
aiAssets/MRAID_3.0_FINAL_June_2018.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user