diff options
| author | Adam Soltys <[email protected]> | 2026-06-29 08:16:36 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-29 08:16:36 -0700 |
| commit | ce084def3e71620a0ce7722c6f0412f4f13b75bd (patch) | |
| tree | e2726555f8992077fa77291e2c1bee2e5beb758c /src | |
| parent | 729690e58a2601ec9c96f5b4431944c1d8f999c7 (diff) | |
| download | coinosv3-ce084def3e71620a0ce7722c6f0412f4f13b75bd.tar.gz coinosv3-ce084def3e71620a0ce7722c6f0412f4f13b75bd.zip | |
Receive dropdown, drop the Swap tab, auto-detect pasted invoices
- Receive: replace the 3-button segmented control (too wide on mobile) with a
compact 'Receive over' dropdown that scales to more address types.
- Remove the standalone Swap tab. Paying an invoice now lives in Send; the
in-flight/recent swap list folds into the top of History. Deleted the dead
swapTab + doSubmarine.
- Send: as soon as a valid bolt11 is pasted/typed/scanned into the recipient
field, jump straight to the Lightning confirmation (submarine-swap quote with
itemized cost) — the invoice's baked-in amount is used, so the amount field is
skipped entirely. Wired into the recipient onInput, the paste button, and QR scan.
Verified in-browser: Swap tab gone, dropdown renders, and a pasted 60k invoice
auto-progresses to 'Pay over Lightning' with the Boltz fee shown, no Review tap.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 67 | ||||
| -rw-r--r-- | src/i18n.js | 1 |
2 files changed, 29 insertions, 39 deletions
@@ -2545,15 +2545,6 @@ async function doReverse() { ui.swapBusy = false; render(); } -async function doSubmarine() { - const inv = (ui.swapInvoice || '').trim().toLowerCase().replace(/^lightning:/, ''); - if (!inv.startsWith('ln')) { ui.swapError = 'Paste a bolt11 invoice'; return render(); } - ui.swapError = ''; ui.swapBusy = true; render(); - try { await swaps.startSubmarine(inv); ui.swapInvoice = ''; } - catch (e) { ui.swapError = e.message; } - ui.swapBusy = false; render(); -} - async function doRefund(id) { ui.swapError = ''; ui.swapBusy = true; render(); try { await swaps.refundSubmarine(id); } @@ -2594,30 +2585,11 @@ function swapListView() { ); } -function swapTab() { - // Receiving over Lightning now lives under Receive → Lightning; this tab keeps - // paying an invoice (a submarine swap) + the in-flight swap list for now. - return h('div', { class: 'col', style: 'gap:12px' }, - wallet.watchOnly - ? h('div', { class: 'card col' }, - h('h3', {}, '⚡ Pay over Lightning'), - h('p', { class: 'small muted', style: 'margin:0' }, 'Re-enter your recovery phrase to pay over Lightning — swaps need your keys to claim on-chain.')) - : h('div', { class: 'card col' }, - h('h3', {}, '⚡ Pay a Lightning invoice'), - h('p', { class: 'small muted', style: 'margin:0' }, 'Pay a bolt11 from your on-chain balance: funds a swap that Boltz pays over Lightning.'), - h('textarea', { rows: 3, class: 'mono-input', placeholder: 'lnbcrt1…', value: ui.swapInvoice || '', onInput: (e) => { ui.swapInvoice = e.target.value; } }), - h('button', { disabled: !!ui.swapBusy, onClick: doSubmarine }, ui.swapBusy ? '…' : 'Pay invoice')), - ui.swapError ? h('div', { class: 'notice err' }, ui.swapError) : null, - swapListView() - ); -} - function tabsBar() { const tabs = [ ['receive', t('tabReceive')], // Watch-only wallets show Send too — it prompts to load the seed to spend. ['send', t('tabSend')], - ['swap', '⚡ Swap'], ['history', t('tabHistory')], ['settings', t('tabSettings')], ]; @@ -2643,7 +2615,6 @@ function tabContent() { switch (ui.tab) { case 'receive': return receiveTab(); case 'send': return sendTab(); - case 'swap': return swapTab(); case 'history': return historyTab(); case 'settings': return settingsTab(); } @@ -2696,12 +2667,16 @@ function receiveTab() { let mode = ui.receiveType || 'address'; if (mode === 'sp' && !spAddr) mode = 'address'; if (mode === 'ln' && !canLn) mode = 'address'; - const seg = (spAddr || canLn) - ? h('div', { class: 'seg', style: 'display:flex;width:100%' }, - h('button', { type: 'button', class: (mode === 'address' ? 'active ' : '') + 'grow', onClick: () => { ui.receiveType = 'address'; render(); } }, t('receiveAddressTab')), - canLn ? h('button', { type: 'button', class: (mode === 'ln' ? 'active ' : '') + 'grow', onClick: () => { ui.receiveType = 'ln'; render(); } }, t('receiveLnTab')) : null, - spAddr ? h('button', { type: 'button', class: (mode === 'sp' ? 'active ' : '') + 'grow', onClick: () => { ui.receiveType = 'sp'; render(); } }, t('receiveSpTab')) : null - ) + // A compact dropdown (not a 3-button segmented control) so it fits on mobile and + // scales if more address types are added. + const opts = [['address', t('receiveAddressTab')]]; + if (canLn) opts.push(['ln', t('receiveLnTab')]); + if (spAddr) opts.push(['sp', t('receiveSpTab')]); + const seg = opts.length > 1 + ? h('label', { class: 'field', style: 'width:100%' }, + h('span', { class: 'lab' }, t('receiveOver')), + h('select', { onChange: (e) => { ui.receiveType = e.target.value; render(); } }, + opts.map(([id, label]) => h('option', { value: id, selected: mode === id }, label)))) : null; if (mode === 'ln') { return h('div', { class: 'card col', style: 'align-items:center;gap:14px' }, seg, ...lnReceiveContent()); @@ -2875,6 +2850,10 @@ function handleScanned(raw) { const s = ui.send; ui.sendError = ''; + // A scanned bolt11 (optionally lightning:-prefixed) → pay it over Lightning. + const lnInv = text.replace(/^lightning:/i, ''); + if (isLnInvoice(lnInv)) { startLnSend(lnInv); return; } + if (/^bitcoin:/i.test(text)) { const { address, amount } = parseBip21(text); if (!address) { @@ -3101,17 +3080,24 @@ function recipientRow(s, r, i) { check.style.display = a ? '' : 'none'; }; + // A pasted/typed/scanned bolt11 → jump straight to the Lightning confirmation + // (a submarine swap), so its baked-in amount is used and the amount field skipped. + const tryLn = (v) => { + const inv = (v || '').trim().replace(/^lightning:/i, ''); + if (s.recipients.length === 1 && !ui.lnSendBusy && !ui.lnSend && isLnInvoice(inv)) { startLnSend(inv); return true; } + return false; + }; const addrInput = h('input', { type: 'text', class: 'mono-input grow', placeholder: 'bc1q…', autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: r.address, - onInput: (e) => { r.address = e.target.value; syncCheck(); }, + onInput: (e) => { r.address = e.target.value; syncCheck(); tryLn(e.target.value); }, }); const row = h( 'div', { class: 'col gap6' }, h('div', { class: 'input-group' }, addrInput, - pasteBtn((text) => { addrInput.value = text; r.address = text; syncCheck(); }), + pasteBtn((text) => { addrInput.value = text; r.address = text; syncCheck(); tryLn(text); }), i === 0 && canScan() && h('button', { type: 'button', class: 'btn-sm', title: t('scanQr'), onClick: scanIntoSend, html: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8V5a1 1 0 0 1 1-1h3M16 4h3a1 1 0 0 1 1 1v3M20 16v3a1 1 0 0 1-1 1h-3M8 20H5a1 1 0 0 1-1-1v-3"/></svg>', @@ -3601,8 +3587,10 @@ function historyTab() { ? [...wallet.outstandingGifts(), ...wallet.claimedGifts().map((c) => ({ ...c, claimed: true }))] : []; if (ui.giftsAll && gifts.length) return giftsAllView(gifts); + // In-flight / recent Lightning swaps (formerly the Swap tab) sit above history. + const swapsCard = swapListView(); if (!txs.length && !gifts.length) - return h('div', { class: 'card' }, h('p', { class: 'muted center', style: 'margin:0' }, t('noTxYet'))); + return swapsCard || h('div', { class: 'card' }, h('p', { class: 'muted center', style: 'margin:0' }, t('noTxYet'))); // Show at most 3 gifts inline; the rest live behind "View all". Transactions // paginate 10 at a time. @@ -3610,7 +3598,7 @@ function historyTab() { const txPages = Math.ceil(txs.length / PAGE_SIZE); const txPage = Math.min(ui.txPage, Math.max(0, txPages - 1)); const txSlice = txs.slice(txPage * PAGE_SIZE, txPage * PAGE_SIZE + PAGE_SIZE); - return h( + const historyCard = h( 'div', { class: 'card' }, h('div', { class: 'list' }, @@ -3630,6 +3618,7 @@ function historyTab() { ) : null ); + return swapsCard ? h('div', { class: 'col', style: 'gap:12px' }, swapsCard, historyCard) : historyCard; } // Open a tx's detail view, lazily fetching its fee/details if we don't have them diff --git a/src/i18n.js b/src/i18n.js index 72d2013..9403dcc 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -347,6 +347,7 @@ const STR = { spReceiveTitle: 'Silent payment address', spReceiveDesc: "A reusable address that can't be tracked.", receiveAddressTab: 'Address', + receiveOver: 'Receive over', receiveLnTab: 'Lightning', receiveSpTab: 'Silent payment', lnReceiveIntro: 'Receive over Lightning — paid into this wallet on-chain, non-custodial.', |
