From 93912fa621ffc45b72f12ef0f0a74bf2e178cb55 Mon Sep 17 00:00:00 2001 From: martbost Date: Tue, 8 Sep 2026 07:24:48 -0500 Subject: [PATCH] Ad viewing: full-screen in-page overlay instead of a new tab Mobile and in-app wallet (dApp) browsers handle tabs badly, so opening each ad in a new tab made the earn loop hard to navigate and impossible to close. Ads now open in a full-screen in-page overlay that reuses the /view dwell + human-check + credit logic in an iframe: an always-working close control, no window.close, no tab-switching. The framed /view relaxes its focus-pause to visibility only and messages the dashboard when it credits or the user is done. Co-Authored-By: Claude Opus 4.8 --- public/assets/my.js | 36 ++++++++++++++++++++++++++++++++---- public/assets/view.js | 15 +++++++++++++-- public/my.html | 2 +- public/view.html | 2 +- 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/public/assets/my.js b/public/assets/my.js index b54562d..5cf1b21 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -1280,12 +1280,40 @@ : 'No member ads are live in rotation right now. Views resume the moment a campaign is running.') + ''; return; } - const w = window.open(r.viewUrl, '_blank'); - box.innerHTML = 'Ad open in its own tab. Watch the countdown, pass the quick ' - + 'check, and the view credits itself — this page updates the moment it does.' - + (w ? '' : '
Pop-up blocked — open the ad here.'); + openAdOverlay(r.viewUrl); + box.innerHTML = 'Watch the countdown and pass the quick check. Your view credits itself and this page updates right away.'; $('earnStartBtn').textContent = 'View next ad'; } + // In-page ad overlay: no new tab (mobile and in-app wallet browsers handle tabs + // badly), no window.close needed. The /view page runs the dwell + check inside, + // messages back when it credits or when the user is done. + function openAdOverlay(url) { + closeAdOverlay(); + const back = document.createElement('div'); + back.id = 'adOverlay'; + back.style.cssText = 'position:fixed;inset:0;z-index:100000;background:#050b09;display:flex;flex-direction:column'; + const x = document.createElement('button'); + x.type = 'button'; x.setAttribute('aria-label', 'Close ad'); + x.textContent = '✕'; + x.style.cssText = 'position:absolute;top:10px;right:12px;z-index:2;width:40px;height:40px;border-radius:50%;border:1px solid rgba(255,255,255,.25);background:rgba(6,10,16,.85);color:#fff;font-size:20px;font-weight:700;cursor:pointer'; + x.addEventListener('click', closeAdOverlay); + const ifr = document.createElement('iframe'); + ifr.src = url; + ifr.style.cssText = 'flex:1;width:100%;border:0;background:#050b09'; + back.appendChild(ifr); back.appendChild(x); + document.body.appendChild(back); + document.body.style.overflow = 'hidden'; + } + function closeAdOverlay() { + const m = $('adOverlay'); if (m) m.remove(); + document.body.style.overflow = ''; + earnRefresh(); loadDashboard(); + } + window.addEventListener('message', e => { + if (e.origin !== location.origin || !e.data) return; + if (e.data.t === 'iap-view-close') closeAdOverlay(); + else if (e.data.t === 'iap-view-done') { earnRefresh(); loadDashboard(); } + }); $('earnStartBtn').addEventListener('click', () => earnShowAd()); // the viewer tab pings localStorage when a view credits; refresh instantly window.addEventListener('storage', e => { diff --git a/public/assets/view.js b/public/assets/view.js index 89d6046..ceaa90b 100644 --- a/public/assets/view.js +++ b/public/assets/view.js @@ -5,6 +5,7 @@ (() => { const $ = id => document.getElementById(id); const token = location.pathname.split('/').pop(); + const framed = window.self !== window.top; // shown inside the dashboard's in-page ad overlay let left = 5, timer = null, credited = false, asking = false; const setMsg = t => { $('vMsg').textContent = t; }; async function j(url, body) { @@ -15,10 +16,12 @@ } function tick() { if (credited || asking) return; - if (document.visibilityState !== 'visible' || !document.hasFocus()) { + // In the in-page overlay the iframe often doesn't hold focus even though it's + // fully on screen, so gate on tab visibility only when framed. + if (document.visibilityState !== 'visible' || (!framed && !document.hasFocus())) { $('vTimer').classList.add('paused'); $('vTimer').textContent = 'paused'; - setMsg('Come back to this tab to keep the countdown moving.'); + setMsg(framed ? 'Keep this ad on screen to finish the countdown.' : 'Come back to this tab to keep the countdown moving.'); return; } $('vTimer').classList.remove('paused'); @@ -71,6 +74,7 @@ + (st.views >= st.target && !st.claimed ? ' Head back and claim your credits.' : '')); $('vDone').classList.add('on'); try { localStorage.setItem('iap-view-done', String(Date.now())); } catch (e) {} + try { if (framed) window.parent.postMessage({ t: 'iap-view-done' }, location.origin); } catch (e) {} } function fail(msg) { credited = true; // stop the loop; this view is over either way @@ -89,6 +93,13 @@ if (!window.closed) { setMsg('Taking you back to your dashboard…'); location.href = '/my#earn'; } }, 400); }); + // When shown inside the dashboard's in-page overlay there is no tab to close: + // hide "Close tab" and turn "Back to dashboard" into a close-the-overlay signal. + if (framed) { + const cb = $('vClose'); if (cb) cb.style.display = 'none'; + const back = document.querySelector('#vDone a[href="/my#earn"]'); + if (back) back.addEventListener('click', e => { e.preventDefault(); try { window.parent.postMessage({ t: 'iap-view-close' }, location.origin); } catch (x) {} }); + } (async () => { const info = await j('/api/my/viewinfo?token=' + token); if (info.error) { diff --git a/public/my.html b/public/my.html index da7eff1..cf73582 100644 --- a/public/my.html +++ b/public/my.html @@ -691,7 +691,7 @@ - + diff --git a/public/view.html b/public/view.html index f1daa14..63bc052 100644 --- a/public/view.html +++ b/public/view.html @@ -44,6 +44,6 @@ - +