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 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-12 09:31:43 -05:00
parent 261f90d843
commit ebac2fcb32
16 changed files with 59 additions and 27 deletions
+28 -1
View File
@@ -205,5 +205,32 @@ window.IAP = (function () {
note: 'Joined through you so far: <b>' + refs + '</b>.' }
];
}
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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const back = document.createElement('div'); back.className = 'modal-back'; back.style.zIndex = '200';
const field = o.type === 'none' ? '' : o.type === 'textarea'
? '<textarea id="dlgInput" rows="5" style="width:100%;margin-top:12px">' + esc(o.value) + '</textarea>'
: '<input id="dlgInput" type="' + (o.type === 'number' ? 'number' : 'text') + '" ' + (o.type === 'number' ? 'inputmode="decimal" min="0" step="any" ' : '') + 'value="' + esc(o.value) + '" placeholder="' + esc(o.placeholder) + '" style="width:100%;margin-top:12px" autocomplete="off">';
back.innerHTML = '<div class="modal-card" role="dialog" aria-modal="true">' + (o.title ? '<h3 style="margin:0 0 8px">' + esc(o.title) + '</h3>' : '')
+ (o.text ? '<p class="muted small" style="margin:0;white-space:pre-line">' + esc(o.text) + '</p>' : '') + field
+ '<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:16px;flex-wrap:wrap"><button type="button" class="btn ghost" id="dlgCancel">' + esc(o.cancel || 'Cancel') + '</button><button type="button" class="btn" id="dlgOk">' + esc(o.ok || 'OK') + '</button></div></div>';
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, $ };
})();