diff options
Diffstat (limited to 'src/app.js')
| -rw-r--r-- | src/app.js | 55 |
1 files changed, 50 insertions, 5 deletions
@@ -585,6 +585,15 @@ let _publicChatMode = false; // welcome screen is the one screen that must never be guessed at. The boot // shell holds the frame for those few ms instead; see the dispatch below. let _bootDeciding = true; +// Boot phases for the crash-loop guard (build.js BOOT_GUARD counts page +// starts): the app notes the phase it reaches, and clears the count once it +// has lived eight seconds or leaves cleanly. A page iOS kills while opening +// does neither, so the third start in a row opens in safe mode. +function bootPhase(p) { try { localStorage.setItem('coinos-boot-phase', p); } catch {} } +function bootSettled() { try { localStorage.removeItem('coinos-boot'); } catch {} } +const safeBoot = () => !!(typeof window !== 'undefined' && window.__safeBoot); +document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') bootSettled(); }); +addEventListener('pagehide', bootSettled); // Belt and braces: a boot path that returns early (or throws) must not leave // the welcome screen held back forever. Nothing legitimate is still deciding // four seconds in — the store wait alone caps at 800ms. @@ -1200,31 +1209,44 @@ let unit = (() => { // been fetched — a third position showing blanks is worse than two that work. function toggleUnit() { const ring = haveRate() ? ['sats', 'btc', 'fiat'] : ['sats', 'btc']; + const was = unit; unit = ring[(ring.indexOf(unit) + 1) % ring.length]; try { localStorage.setItem(UNIT_KEY, unit); } catch {} + // An amount already typed into the send form is the same money in the new + // unit, not the same digits: 1000 sats does not become 1000 BTC. Whatever + // doesn't parse is left as typed. + for (const r of (ui.send && ui.send.recipients) || []) { + if (!r.amount || !String(r.amount).trim()) continue; + const sat = parseField(r.amount, was); + if (sat) r.amount = amountField(sat); + } render(); } // A sats figure as the amount field types it in the current unit — what a // Max button or a fixed-amount invoice fills in. Fiat is floored to the // cent so a filled-in maximum never parses back to more than was offered. -// The figure it filled is remembered with its exact sats: a fiat maximum +// The figures it filled are remembered with their exact sats: a fiat maximum // rounded to the cent would otherwise parse back a few sats short and leave -// them behind. Typing anything else parses as typed. -let lastFill = null; +// them behind. Typing anything else parses as typed. Several are kept so a +// form with more than one amount round-trips each of them. +const fills = new Map(); function amountField(sat) { let s; const r = unit === 'fiat' && rateNow(); if (unit === 'sats') s = String(sat); else if (r) s = (Math.floor((sat / 1e8) * r * 100) / 100).toFixed(2); else s = (sat / 1e8).toFixed(8); - lastFill = { s, unit, sat }; + fills.delete(unit + '|' + s); + fills.set(unit + '|' + s, sat); + while (fills.size > 16) fills.delete(fills.keys().next().value); return s; } function parseField(v, u = unit) { - if (lastFill && lastFill.unit === u && String(v).trim() === lastFill.s) return lastFill.sat; + const k = u + '|' + String(v).trim(); + if (fills.has(k)) return fills.get(k); return parseAmount(v, u, rateNow()); } @@ -4478,6 +4500,7 @@ function walletScreen() { brandHeader(true), h('div', { class: 'mt16' }, balanceCard()), ui.offlineFallback && wallet.offline ? offlineBanner() : null, + safeBootNotice(), backupNotice(), ...featureAll('walletNotices'), tabsBar(), @@ -4536,6 +4559,24 @@ function backupNotice() { } }, t('backupReminderGo'))); } +// Safe mode (see BOOT_GUARD in build.js): the app crashed on open twice in a +// row, so this boot made no automatic connections. Say so, name the phase +// the last boot reached, and offer the way back. +function safeBootNotice() { + if (!safeBoot()) return null; + const phase = (typeof window !== 'undefined' && window.__bootPhase) || '?'; + const report = () => ['Coinos safe mode', 'build ' + (typeof window._v === 'string' ? window._v : '?'), + 'starts without a clean boot: ' + window.__safeBoot, 'last phase: ' + phase, navigator.userAgent].join('\n'); + return h('div', { class: 'notice info col', style: 'margin:12px 0 0;gap:8px' }, + h('b', {}, t('safeBootTitle')), + h('span', { class: 'small' }, t('safeBootBody', { n: window.__safeBoot - 1, phase })), + h('div', { class: 'row', style: 'gap:8px' }, + h('button', { class: 'btn-sm', onClick: () => { bootSettled(); location.reload(); } }, t('safeBootOpen')), + h('button', { class: 'btn-sm', onClick: (e) => { + navigator.clipboard?.writeText(report()).then(() => { e.target.textContent = t('safeBootCopied'); }).catch(() => {}); + } }, t('safeBootCopy')))); +} + function offlineBanner() { return h( 'div', @@ -6824,6 +6865,7 @@ window.addEventListener('load', () => { }, 1000); }); loadLocale(getLang()).finally(async () => { + bootPhase('boot'); applyBootAutoLogout(); // clear an overdue session before we read it for claim targets // Public read-only community chat: no wallet, no login — the join material // ships in the bundle, so the page decrypts client-side (src/public-chat.js). @@ -6849,6 +6891,7 @@ loadLocale(getLang()).finally(async () => { // and a wallet opening in fiat should open with a number in it. refreshRates().then(() => { if (unit === 'fiat') render(); }).catch(() => {}); setInterval(() => { refreshRates().then(() => { if (unit === 'fiat') render(); }).catch(() => {}); }, 10 * 60_000); + bootPhase('store'); const storeReady = featureHook('arkStoreReady'); // the IndexedDB caches (profiles, reports, …) open alongside: a wallet's // caches are read as soon as it opens @@ -6859,6 +6902,8 @@ loadLocale(getLang()).finally(async () => { _bootDeciding = false; // from here on the screen is a decision, not a guess const opened = restoreAccountsState(); if (!opened) render(); + bootPhase('ui'); + setTimeout(bootSettled, 8000); if (WAKE_PAYLOAD != null) { // Nothing opened — a locked vault, or no wallet on this device. There is // nothing to answer with, so say so now. |
