Lightning tips: pay in-app via NWC when a wallet is connected

Adam pointed out the tip flow still showed the bolt11 / QR view even
when the user had a NIP-47 wallet set up. That re-introduces the
exact wallet-handoff friction the NWC integration is meant to solve.

Changes to the existing LNURL tip flow:

  - openTipModal sets the action button to "⚡ Send tip" when
    nwc.isConnected(), and to the original "Get invoice" otherwise.
  - getTipInvoiceFlow, after fetching the bolt11, tries
    nwc.payInvoice() FIRST when a wallet is connected. On success:
    toast "Sent N sats ⚡" + close modal. On failure (preimage
    missing, wallet error, budget exhausted, etc.): falls through
    to the existing showTipInvoiceView so the user can still pay
    the bolt11 externally with the visible error inline.
  - Button label restoration uses the same dynamic source so post-
    error retry shows the correct (state-appropriate) label.

Pre-flight gates (amount validation, LNURL meta fetch, zap path
construction) are unchanged — NIP-57 zap tagging still wraps the
LNURL request so the recipient still sees a tracked kind-9735
receipt; the only difference is who pays the bolt11.

Verified via CDP: button label flips between "Get invoice" and
"⚡ Send tip" with nwc.isConnected() state.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.7 committed 2026-05-28 15:06:06 -04:00
1 parent c6955cd2ad
commit 4e7ec76817
1 file changed
+30 -3
+30 -3
View File
@@ -18806,7 +18806,12 @@
$('tipCustomInput').style.display = 'none';
$('tipCustomInput').value = '';
const getBtn = $('tipGetInvoiceBtn');
if (getBtn) { getBtn.disabled = true; getBtn.textContent = 'Get invoice'; }
if (getBtn) {
getBtn.disabled = true;
// When a wallet is connected, the button pays in-app directly. When
// not, it falls back to the original LNURL → bolt11 QR flow.
getBtn.textContent = nwc.isConnected() ? '⚡ Send tip' : 'Get invoice';
}
// Clear any active selection
$('tipAmounts').querySelectorAll('button').forEach(b => b.classList.remove('active'));
@@ -18857,6 +18862,7 @@
if (!_tipTarget) return;
let sats = _tipSelectedSats;
if (sats <= 0) { if (errEl) errEl.textContent = 'Pick an amount first.'; return; }
const wasOriginalLabel = nwc.isConnected() ? '⚡ Send tip' : 'Get invoice';
try {
if (getBtn) { getBtn.disabled = true; getBtn.textContent = 'Fetching…'; }
@@ -18866,7 +18872,7 @@
const maxSats = Math.floor((meta.maxSendable || 100000000) / 1000);
if (sats < minSats || sats > maxSats) {
if (errEl) errEl.textContent = `Enter between ${minSats} and ${maxSats.toLocaleString()} sats.`;
if (getBtn) { getBtn.disabled = false; getBtn.textContent = 'Get invoice'; }
if (getBtn) { getBtn.disabled = false; getBtn.textContent = wasOriginalLabel; }
return;
}
const msats = sats * 1000;
@@ -18875,6 +18881,27 @@
const issuedAt = Math.floor(Date.now() / 1000);
const bolt11 = await fetchLnurlInvoice(meta.callback, msats, zap ? { nostr: zap.encoded } : {});
// NWC-first: if a wallet is connected, try to pay the invoice in-app.
// Falls back to the bolt11/QR view on any failure so the user still has
// a way to settle the tip externally.
if (nwc.isConnected()) {
if (getBtn) getBtn.textContent = 'Paying…';
try {
const result = await nwc.payInvoice(bolt11);
if (result?.preimage) {
const tracked = !!zap;
toastSuccess(`Sent ${sats.toLocaleString()} sats${tracked ? ' ⚡' : ''}`);
closeTipModal();
return;
}
// No preimage returned — treat as failure and surface the QR.
} catch (err) {
// Show the failure inline + still display the bolt11 so the user
// can pay externally.
if (errEl) errEl.textContent = `Wallet error: ${err?.message || err}. Falling back to invoice.`;
}
}
// If we took the zap path, watch for the kind-9735 receipt and auto-close on success.
let watch = null;
if (zap && _tipTarget?.targetEvent?.authorPubkey) {
@@ -18891,7 +18918,7 @@
showTipInvoiceView(bolt11, sats, !!zap, watch);
} catch (err) {
if (errEl) errEl.textContent = String(err?.message || err);
if (getBtn) { getBtn.disabled = false; getBtn.textContent = 'Get invoice'; }
if (getBtn) { getBtn.disabled = false; getBtn.textContent = wasOriginalLabel; }
}
}