93912fa621
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 <noreply@anthropic.com>
123 lines
5.1 KiB
JavaScript
123 lines
5.1 KiB
JavaScript
// Full-screen ad viewer: the advertiser URL fills the tab, a countdown runs in
|
|
// the top bar (paused whenever this tab loses focus), and once the dwell is
|
|
// done the server hands out a human check. Solve it and the view credits.
|
|
// The dwell floor is enforced on the SERVER clock; this UI cannot cheat it.
|
|
(() => {
|
|
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) {
|
|
const r = await fetch(url, body
|
|
? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
|
|
: undefined);
|
|
return r.json();
|
|
}
|
|
function tick() {
|
|
if (credited || asking) return;
|
|
// 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(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');
|
|
left = Math.max(0, left - 0.25);
|
|
$('vTimer').textContent = Math.ceil(left) + 's left';
|
|
if (left <= 0) { clearInterval(timer); askChallenge(); }
|
|
}
|
|
async function askChallenge() {
|
|
asking = true;
|
|
$('vTimer').textContent = 'check';
|
|
$('vTimer').classList.add('done');
|
|
setMsg('One quick check to count the view:');
|
|
let c = await j('/api/my/viewchallenge?token=' + token);
|
|
if (c.early) { // server clock says not quite yet: wait it out and re-ask
|
|
await new Promise(r => setTimeout(r, (c.wait || 1) * 1000 + 300));
|
|
c = await j('/api/my/viewchallenge?token=' + token);
|
|
}
|
|
if (c.error) return fail(c.error);
|
|
renderChallenge(c);
|
|
}
|
|
function renderChallenge(c) {
|
|
$('vPrompt').textContent = 'Click the ' + c.prompt + ':';
|
|
const w = $('vOpts');
|
|
w.innerHTML = '';
|
|
c.options.forEach((em, i) => {
|
|
const b = document.createElement('button');
|
|
b.type = 'button';
|
|
b.textContent = em;
|
|
b.addEventListener('click', () => answer(i));
|
|
w.appendChild(b);
|
|
});
|
|
$('vCheck').classList.add('on');
|
|
}
|
|
async function answer(i) {
|
|
const r = await j('/api/my/adview', { token, answer: i });
|
|
if (r.error) {
|
|
if (r.retry) {
|
|
setMsg('Not that one — try again.');
|
|
const c = await j('/api/my/viewchallenge?token=' + token);
|
|
if (!c.error) return renderChallenge(c);
|
|
return fail(c.error);
|
|
}
|
|
return fail(r.error);
|
|
}
|
|
credited = true;
|
|
$('vCheck').classList.remove('on');
|
|
$('vTimer').textContent = '✓ credited';
|
|
const st = r.status || r;
|
|
setMsg('View ' + st.views + ' of ' + st.target + ' counted for today.'
|
|
+ (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
|
|
if (timer) clearInterval(timer);
|
|
$('vCheck').classList.remove('on');
|
|
$('vTimer').textContent = '—';
|
|
setMsg(msg);
|
|
$('vDone').classList.add('on');
|
|
}
|
|
$('vClose').addEventListener('click', () => {
|
|
window.close();
|
|
// window.close() is blocked in mobile and in-app (wallet dApp) browsers. If
|
|
// the tab is still here a moment later, take them back to the dashboard so
|
|
// they are never stuck on a tab they can't close.
|
|
setTimeout(() => {
|
|
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) {
|
|
$('vFrame').remove();
|
|
const d = document.createElement('div');
|
|
d.className = 'vfail';
|
|
d.textContent = info.error;
|
|
document.querySelector('.vw').appendChild(d);
|
|
$('vDone').classList.add('on');
|
|
$('vTimer').textContent = '—';
|
|
setMsg('');
|
|
return;
|
|
}
|
|
left = info.dwell || 5;
|
|
$('vFrame').src = info.targetUrl;
|
|
setMsg('Watching: ' + (info.adName || 'member ad') + ' — stay on this tab.');
|
|
$('vTimer').textContent = left + 's left';
|
|
timer = setInterval(tick, 250);
|
|
})();
|
|
})();
|