From ebac2fcb3221dfef0e8dcdf26a5580b10872202d Mon Sep 17 00:00:00 2001 From: martbost Date: Sat, 12 Sep 2026 09:31:43 -0500 Subject: [PATCH] Member area: in-page dialogs replace every browser prompt/confirm Mobile Safari shows a red 'Suppress dialogs' option on the second native pop-up in a row and, once tapped, swallows every later prompt on the site until reload (Hugh, 2026-09-12, campaign top-up). IAP.ask / IAP.confirmBox in common.js render a modal-card dialog (Enter/Escape, focus, backdrop cancel); all nine call sites in my.js use them (top-up credits, link insert x2 with selection restore, pay it forward, adopt note, release, unlink, add position, Trust Wallet check). Version tags bumped on every page that loads common.js. Co-Authored-By: Claude Fable 5.1 --- public/admin.html | 2 +- public/assets/common.js | 29 ++++++++++++++++++++++++++++- public/assets/my.js | 27 ++++++++++++++++----------- public/contract.html | 2 +- public/disclaimer.html | 2 +- public/index.html | 2 +- public/join.html | 2 +- public/launch.html | 2 +- public/ledger.html | 2 +- public/my.html | 4 ++-- public/plays.html | 2 +- public/privacy.html | 2 +- public/terms.html | 2 +- public/tx.html | 2 +- public/wall.html | 2 +- public/wallets.html | 2 +- 16 files changed, 59 insertions(+), 27 deletions(-) diff --git a/public/admin.html b/public/admin.html index 0a13be8..66d8174 100644 --- a/public/admin.html +++ b/public/admin.html @@ -329,7 +329,7 @@ - + diff --git a/public/assets/common.js b/public/assets/common.js index 4a9309a..705469f 100644 --- a/public/assets/common.js +++ b/public/assets/common.js @@ -205,5 +205,32 @@ window.IAP = (function () { note: 'Joined through you so far: ' + refs + '.' } ]; } - return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, reportAd, requestCode, launchChecks, launchToggle, $ }; + // In-page dialogs instead of window.prompt / confirm. Mobile Safari shows a red "Suppress dialogs" + // option on the second native pop-up in a row and, once tapped, swallows every later prompt on the + // site until reload. ask() resolves the typed value (null on cancel); confirmBox() resolves true/false. + function dialog(o) { + return new Promise(resolve => { + const esc = t => String(t == null ? '' : t).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); + const back = document.createElement('div'); back.className = 'modal-back'; back.style.zIndex = '200'; + const field = o.type === 'none' ? '' : o.type === 'textarea' + ? '' + : ''; + back.innerHTML = ''; + document.body.appendChild(back); + const inp = back.querySelector('#dlgInput'); + const done = v => { document.removeEventListener('keydown', onKey); back.remove(); resolve(v); }; + const okv = () => done(o.type === 'none' ? true : (inp ? inp.value : '')); + const onKey = e => { if (e.key === 'Escape') { e.preventDefault(); done(o.type === 'none' ? false : null); } else if (e.key === 'Enter' && o.type !== 'textarea') { e.preventDefault(); okv(); } }; + document.addEventListener('keydown', onKey); + back.querySelector('#dlgOk').addEventListener('click', okv); + back.querySelector('#dlgCancel').addEventListener('click', () => done(o.type === 'none' ? false : null)); + back.addEventListener('click', e => { if (e.target === back) done(o.type === 'none' ? false : null); }); + setTimeout(() => { if (inp) { inp.focus(); if (inp.select && o.type !== 'textarea') inp.select(); } else back.querySelector('#dlgOk').focus(); }, 30); + }); + } + function ask(o) { return dialog(Object.assign({ type: 'text', value: '', placeholder: '' }, o || {})); } + function confirmBox(text, o) { return dialog(Object.assign({ type: 'none', text, ok: 'Yes', cancel: 'No' }, o || {})); } + return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, reportAd, requestCode, launchChecks, launchToggle, ask, confirmBox, $ }; })(); diff --git a/public/assets/my.js b/public/assets/my.js index 1f5e05a..266db9a 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -651,7 +651,7 @@ catch (e) { IAP.status(e.message, 'bad'); } })); el.querySelectorAll('button[data-topup]').forEach(b => b.addEventListener('click', async () => { - const n = prompt('How many credits to add to this campaign? (buys more views)'); + const n = await IAP.ask({ title: 'Add credits', text: 'How many credits to add to this campaign? More credits buy more views.', type: 'number', placeholder: 'e.g. 100', ok: 'Add credits' }); if (!n) return; try { const r = await api('/api/my/campaigns/' + b.dataset.topup + '/topup', { credits: Number(n) }); IAP.status('Added ' + r.added + ' credits' + (r.reactivated ? ' — campaign is live again.' : '.'), 'ok'); @@ -676,10 +676,11 @@ btn.addEventListener('click', () => { $('cSoloEd').focus(); document.execCommand(btn.dataset.cmd, false, null); })); document.querySelectorAll('.ed-bar [data-block]').forEach(btn => btn.addEventListener('click', () => { $('cSoloEd').focus(); document.execCommand('formatBlock', false, btn.dataset.block); })); - $('edLinkBtn').addEventListener('click', () => { - const url = prompt('Link URL (https://…)'); + $('edLinkBtn').addEventListener('click', async () => { + const sel = window.getSelection(); const range = sel && sel.rangeCount ? sel.getRangeAt(0).cloneRange() : null; // the dialog steals the selection + const url = await IAP.ask({ title: 'Insert link', text: 'Link URL (https://…)', placeholder: 'https://', ok: 'Insert' }); if (!url) return; - $('cSoloEd').focus(); + $('cSoloEd').focus(); if (range) { sel.removeAllRanges(); sel.addRange(range); } document.execCommand('createLink', false, url); }); // inline media: upload, then drop the element at the cursor (BV-style) @@ -1031,7 +1032,7 @@ async function pif(email, name, address) { let suggest = 25; try { const { products } = await (await fetch('/api/catalog')).json(); const p20 = (products || []).find(p => p.priceCents === 2000); if (p20 && p20.costWei) suggest = Math.ceil(Number(p20.costWei) / 1e18) + 3; } catch (e) {} - const amt = prompt('Send POL from your wallet to ' + name + ' (' + address.slice(0, 6) + '…' + address.slice(-4) + ') for their first package.\nSuggested: the $20 package plus fees. Amount in POL:', String(suggest)); + const amt = await IAP.ask({ title: 'Pay it forward', text: 'Send POL from your wallet to ' + name + ' (' + address.slice(0, 6) + '…' + address.slice(-4) + ') for their first package.\nSuggested: the $20 package plus fees. Amount in POL:', type: 'number', value: String(suggest), ok: 'Send POL' }); if (amt === null) return; const pol = Number(amt); if (!(pol > 0)) { IAP.status('Enter an amount in POL.', 'bad'); return; } try { @@ -1066,7 +1067,7 @@ + 'last sign-in: ' + ago(w.lastSeen) + '' + (r.eligible ? '' : '') + '').join(''); el.querySelectorAll('[data-adopt]').forEach(b => b.addEventListener('click', async () => { - const note = prompt('Your first message to ' + b.dataset.aname + ' (sent as a chat and an email):', 'Hi, I picked you up from the InstantAdPay holding tank so you have a sponsor who will actually help. Reply here and I will walk you through the first three steps.'); + const note = await IAP.ask({ title: 'Adopt ' + b.dataset.aname, text: 'Your first message to ' + b.dataset.aname + ' (sent as a chat and an email):', type: 'textarea', ok: 'Adopt and send', value: 'Hi, I picked you up from the InstantAdPay holding tank so you have a sponsor who will actually help. Reply here and I will walk you through the first three steps.' }); if (note === null) return; try { const rr = await api('/api/my/tank/adopt', { who: b.dataset.adopt, note }); IAP.status('You are now the sponsor for ' + rr.name + '. Chat and email sent.', 'ok'); loadTank(); loadCoach(); } catch (e) { IAP.status(e.message, 'bad'); } @@ -1100,7 +1101,7 @@ const inp = $('chatInput'); if (inp) { inp.value = b.dataset.say.replace(/\{\{name\}\}/g, b.dataset.nname.replace(/^@/, '')); inp.focus(); } })); el.querySelectorAll('[data-release]').forEach(b => b.addEventListener('click', async () => { - if (!confirm('Release ' + b.dataset.rname + ' to the holding tank? You stop being their sponsor and another member can adopt them.')) return; + if (!(await IAP.confirmBox('Release ' + b.dataset.rname + ' to the holding tank? You stop being their sponsor and another member can adopt them.', { title: 'Release to the tank', ok: 'Release' }))) return; try { await api('/api/my/tank/release', { email: b.dataset.release }); IAP.status(b.dataset.rname + ' is in the holding tank.', 'ok'); loadCoach(); } catch (e) { IAP.status(e.message, 'bad'); } })); @@ -1247,7 +1248,11 @@ b.addEventListener('click', () => { $('bcEd').focus(); document.execCommand(b.dataset.bc, false, null); })); document.querySelectorAll('[data-bcblock]').forEach(b => b.addEventListener('click', () => { $('bcEd').focus(); document.execCommand('formatBlock', false, b.dataset.bcblock); })); - if ($('bcLinkBtn')) $('bcLinkBtn').addEventListener('click', () => { const u = prompt('Link URL (https://…)'); if (u) { $('bcEd').focus(); document.execCommand('createLink', false, u); } }); + if ($('bcLinkBtn')) $('bcLinkBtn').addEventListener('click', async () => { + const sel = window.getSelection(); const range = sel && sel.rangeCount ? sel.getRangeAt(0).cloneRange() : null; + const u = await IAP.ask({ title: 'Insert link', text: 'Link URL (https://…)', placeholder: 'https://', ok: 'Insert' }); + if (u) { $('bcEd').focus(); if (range) { sel.removeAllRanges(); sel.addRange(range); } document.execCommand('createLink', false, u); } + }); if ($('bcSendBtn')) $('bcSendBtn').addEventListener('click', busy2($('bcSendBtn'), async () => { const r = await api('/api/my/broadcast', { scope: $('bcScope').value, subject: $('bcSubject').value, body: $('bcEd').innerHTML }); IAP.status('Broadcast sent to ' + r.sent + ' member' + (r.sent === 1 ? '' : 's') + '.', 'ok'); @@ -1537,7 +1542,7 @@ $('buyFromWrap').hidden = !list.length; } document.querySelectorAll('[data-unlink]').forEach(b => b.addEventListener('click', async () => { - if (!confirm('Unlink ' + short(b.dataset.unlink) + ' from your account?')) return; + if (!(await IAP.confirmBox('Unlink ' + short(b.dataset.unlink) + ' from your account?', { title: 'Unlink position', ok: 'Unlink' }))) return; try { await api('/api/my/positions/remove', { address: b.dataset.unlink }); IAP.status('Position unlinked.', 'ok'); loadPositions(); } catch (e) { IAP.status(e.message, 'bad'); } })); @@ -1547,7 +1552,7 @@ const me = await (await fetch('/api/me')).json(); if (!me.address) throw new Error('Link your main wallet first (Wallet tab), then add positions under it.'); if (!me.memberId) throw new Error('Switch on payouts for your main wallet first (Wallet tab). Positions register under your member number.'); - if (!confirm('Your wallet will ask which account to connect. Tick ONLY the new account (not ' + short(me.address) + '), then sign once.\n\nIf you have not created the extra account yet: MetaMask, account menu, Add account. Trust or SafePal: switch wallet.\n\nReady?')) return; + if (!(await IAP.confirmBox('Your wallet will ask which account to connect. Tick ONLY the new account (not ' + short(me.address) + '), then sign once.\n\nIf you have not created the extra account yet: MetaMask, account menu, Add account. Trust or SafePal: switch wallet.\n\nReady?', { title: 'Add a position', ok: 'Ready' }))) return; IAP.status('Pick the new account in your wallet, then sign once…'); const r = await IAPWallet.signIn({ asPosition: true, pick: true }); $('qsHint').textContent = 'Added ' + short(r.address) + '. Now choose it under "Buy from" and buy a $20 or larger package.'; @@ -1617,7 +1622,7 @@ } const pct = Number(need * 100n / bal); const isTrust = /trust/i.test(IAPWallet.walletName() || ''); - if (isTrust && pct > 55 && !confirm('Heads up for Trust Wallet users: this purchase uses about ' + pct + '% of the POL in your wallet, and Trust Wallet refuses transactions that spend most of the balance (it shows a "drain your wallet" warning with only Stop and go back).' + '\n\n' + 'Options: pick a smaller package first, add some POL, or connect a different wallet (MetaMask, Phantom, SafePal). Extra POL always stays yours.' + '\n\n' + 'Try it anyway?')) { + if (isTrust && pct > 55 && !(await IAP.confirmBox('Heads up for Trust Wallet users: this purchase uses about ' + pct + '% of the POL in your wallet, and Trust Wallet refuses transactions that spend most of the balance (it shows a "drain your wallet" warning with only Stop and go back).' + '\n\n' + 'Options: pick a smaller package first, add some POL, or connect a different wallet (MetaMask, Phantom, SafePal). Extra POL always stays yours.' + '\n\n' + 'Try it anyway?', { title: 'Trust Wallet check', ok: 'Buy anyway', cancel: 'Pause' }))) { IAP.status('Purchase paused. Pick a smaller package, add POL, or connect another wallet, then try again.', 'ok'); return; } diff --git a/public/contract.html b/public/contract.html index d24a718..c30e086 100644 --- a/public/contract.html +++ b/public/contract.html @@ -141,7 +141,7 @@
Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.
- + diff --git a/public/disclaimer.html b/public/disclaimer.html index e88fb20..de70353 100644 --- a/public/disclaimer.html +++ b/public/disclaimer.html @@ -26,7 +26,7 @@

You decide whether, and how much, to spend. Never spend more than you can afford to lose.

- + diff --git a/public/index.html b/public/index.html index 4695fd1..89309da 100644 --- a/public/index.html +++ b/public/index.html @@ -461,7 +461,7 @@ - + diff --git a/public/join.html b/public/join.html index 309c840..9941536 100644 --- a/public/join.html +++ b/public/join.html @@ -155,7 +155,7 @@ InstantAdPay · Contract · Terms · Privacy · Disclaimer - + diff --git a/public/launch.html b/public/launch.html index c219f6d..4c0fb22 100644 --- a/public/launch.html +++ b/public/launch.html @@ -85,7 +85,7 @@
Advertising services with a performance referral program. Not an investment product; no income guarantees.
- + diff --git a/public/ledger.html b/public/ledger.html index 1a439af..f994fff 100644 --- a/public/ledger.html +++ b/public/ledger.html @@ -37,7 +37,7 @@
InstantAdPay · how it works · contract source ↗
- + diff --git a/public/my.html b/public/my.html index 5c786ef..1e7fede 100644 --- a/public/my.html +++ b/public/my.html @@ -908,10 +908,10 @@ - + - + diff --git a/public/plays.html b/public/plays.html index 9d99c4c..849d31f 100644 --- a/public/plays.html +++ b/public/plays.html @@ -192,7 +192,7 @@
Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.
- + diff --git a/public/privacy.html b/public/privacy.html index 50d658f..6b57bec 100644 --- a/public/privacy.html +++ b/public/privacy.html @@ -28,7 +28,7 @@

We use reasonable safeguards, but no system is perfectly secure. Protect your email and your wallet.

- + diff --git a/public/terms.html b/public/terms.html index 364ce12..8c81c62 100644 --- a/public/terms.html +++ b/public/terms.html @@ -36,7 +36,7 @@

See also the Disclaimer and Privacy Policy.

- + diff --git a/public/tx.html b/public/tx.html index 95315fd..b9a9b04 100644 --- a/public/tx.html +++ b/public/tx.html @@ -34,7 +34,7 @@

← Back to the live ledger · Read the contract review

- + diff --git a/public/wall.html b/public/wall.html index 4f9cd0f..29e9c07 100644 --- a/public/wall.html +++ b/public/wall.html @@ -47,7 +47,7 @@ - + diff --git a/public/wallets.html b/public/wallets.html index a1dcba0..8355d30 100644 --- a/public/wallets.html +++ b/public/wallets.html @@ -121,7 +121,7 @@
Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never share your recovery phrase.
- +