diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 23 | ||||
| -rw-r--r-- | src/features/names.js | 8 | ||||
| -rw-r--r-- | src/features/zaps.js | 6 |
3 files changed, 36 insertions, 1 deletions
@@ -5059,6 +5059,13 @@ function destReady(a) { // to candidates with avatars; picking one runs the same path as pasting. const sendSearch = { rows: null, sync: null, pick: null, el: null }; let sendRevealTimer = null; +// A lightning address typed and then left alone: once typing has paused, +// ask whether it's real (the names and zaps features each know a way), and +// go on to the amount if anything answers. A half-typed domain answers +// nothing, so it just waits, with no error. +let sendPauseTimer = null; +const LN_ADDRESS_SHAPE = /^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i; +const SEND_PAUSE_MS = 900; // With the phone keyboard up, the space under the recipient field is scarce — // park the field at the top of the view so the candidate list gets what's // left. Runs on focus and again whenever fresh candidates land (the list @@ -5132,6 +5139,8 @@ function recipientRow(s, r, i) { autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', value: shown, // one line of text: Enter doesn't break it onKeydown: (e) => { if (e.key === 'Enter') e.preventDefault(); }, + // pasted text is complete: it moves on at once, like the Paste button + onPaste: () => { r._pasted = true; }, // the delay lets the keyboard start opening (and any render() swap of // this input) before parkSendField measures the live activeElement. // warmSearch pre-opens the search transports so the first keystroke's @@ -5147,7 +5156,19 @@ function recipientRow(s, r, i) { const hadError = !!ui.sendError; ui.sendError = ''; // editing the destination starts over — drop stale errors if (i === 0) sendSearcher.update(v); // candidates render when results land - if (tryFeature(v, true)) return; // a bolt11 advances to its own confirmation + const pasted = r._pasted; r._pasted = false; + clearTimeout(sendPauseTimer); + if (tryFeature(v, !pasted)) return; // a bolt11 (or anything pasted) advances to its own confirmation + if (single && LN_ADDRESS_SHAPE.test(v.trim())) { + sendPauseTimer = setTimeout(async () => { + if (r.address !== v) return; + const answers = featureAll('probeLnAddress', v.trim()); + if (!answers.length) return; + const real = await Promise.any(answers.map((p) => Promise.resolve(p).then((x) => x || Promise.reject()))).then(() => true, () => false); + // still the same text, still on this form + if (real && r.address === v && ui.tab === 'send' && ui.send === s) tryFeature(v, false); + }, SEND_PAUSE_MS); + } // reveal/hide amount + controls as validity flips, an annotation // (zap button) appears/disappears, or an error clears — but only after // typing pauses: an immediate render swaps this input for a fresh node, diff --git a/src/features/names.js b/src/features/names.js index 8f37b36..7e51c8c 100644 --- a/src/features/names.js +++ b/src/features/names.js @@ -923,6 +923,14 @@ export function namesFeature(ctx) { render: (seg) => namePane(seg), }]; }, + // Is a typed name real yet? (the send form asks once typing pauses, and + // only moves on when something answers — never on a half-typed domain) + async probeLnAddress(text) { + const parsed = parsePaymentName(text); + if (!parsed) return false; + if (OUR_DOMAINS.includes(parsed.domain)) return !!(await registrarLookup(parsed.name, parsed.domain).catch(() => null)); + return !!(await resolveBip353(parsed.name, parsed.domain).catch(() => null)); + }, // BEFORE zaps in the registry: a pasted user@domain tries DNS first. matchSendText(text, typed) { if (typed) return false; // don't yank the form away mid-keystroke diff --git a/src/features/zaps.js b/src/features/zaps.js index a99de94..f0b655a 100644 --- a/src/features/zaps.js +++ b/src/features/zaps.js @@ -414,6 +414,12 @@ export function zapsFeature(ctx) { begin(target, zapDisplay(target, text), tap); return true; }, + // A lightning address is real when its server answers as a pay endpoint. + async probeLnAddress(text) { + const target = parseZapTarget(text); + if (!target || target.kind !== 'lnaddr') return false; + return fetchPayParams(target.url).then(() => true, () => false); + }, // The typed-lightning-address affordance on the send form. sendFormNote(a) { if (!canPay() || !ui.send || ui.send.recipients.length !== 1) return null; |
