feat(wallet): invoice-first lightning send — amount locks from the pasted invoice
A pasted fixed-amount invoice auto-fills and locks the amount ('set by
invoice' — nothing to type, just review and send); zero-amount invoices get
an explicit 'enter how many sats' hint. Plus a clipboard Paste button on the
destination field (hidden where clipboard read can't work).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f72d4b92ac
commit
80875a2ed4
@ -67,6 +67,7 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="flex items-center justify-between mb-1">
|
||||||
<label class="text-white/60 text-sm">{{ t('sendBitcoin.amountSats') }}</label>
|
<label class="text-white/60 text-sm">{{ t('sendBitcoin.amountSats') }}</label>
|
||||||
|
<span v-if="pastedInvoiceAmount !== null" class="text-[11px] px-2 py-0.5 rounded-full bg-white/10 text-white/50">set by invoice</span>
|
||||||
<button
|
<button
|
||||||
v-if="sendMethod === 'onchain'"
|
v-if="sendMethod === 'onchain'"
|
||||||
@click="toggleSendAll"
|
@click="toggleSendAll"
|
||||||
@ -82,19 +83,34 @@
|
|||||||
v-model.number="amount"
|
v-model.number="amount"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
:placeholder="sendAll ? '' : '1000'"
|
:placeholder="sendAll ? '' : pastedInvoiceAmount !== null ? '' : '1000'"
|
||||||
:disabled="sendAll"
|
:disabled="sendAll || pastedInvoiceAmount !== null"
|
||||||
class="w-full input-glass disabled:opacity-50"
|
class="w-full input-glass disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
<p v-if="sendAll" class="text-xs text-white/50 mt-1">
|
<p v-if="sendAll" class="text-xs text-white/50 mt-1">
|
||||||
Sweeps your entire on-chain balance{{ onchainBalance !== null ? ` (~${onchainBalance.toLocaleString()} sats)` : '' }} minus network fees.
|
Sweeps your entire on-chain balance{{ onchainBalance !== null ? ` (~${onchainBalance.toLocaleString()} sats)` : '' }} minus network fees.
|
||||||
</p>
|
</p>
|
||||||
|
<p v-else-if="pastedInvoiceAmount !== null" class="text-xs text-white/50 mt-1">
|
||||||
|
This invoice fixes the amount — nothing to type, just review and send.
|
||||||
|
</p>
|
||||||
|
<p v-else-if="effectiveMethod === 'lightning' && dest.trim()" class="text-xs text-white/50 mt-1">
|
||||||
|
Zero-amount invoice — enter how many sats to pay.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="effectiveMethod !== 'ecash'" class="mb-3">
|
<div v-if="effectiveMethod !== 'ecash'" class="mb-3">
|
||||||
<label class="text-white/60 text-sm block mb-1">
|
<div class="flex items-center justify-between mb-1">
|
||||||
{{ effectiveMethod === 'lightning' ? t('sendBitcoin.lightningInvoice') : effectiveMethod === 'ark' ? 'Ark address, invoice or lightning address' : t('sendBitcoin.bitcoinAddress') }}
|
<label class="text-white/60 text-sm">
|
||||||
</label>
|
{{ effectiveMethod === 'lightning' ? t('sendBitcoin.lightningInvoice') : effectiveMethod === 'ark' ? 'Ark address, invoice or lightning address' : t('sendBitcoin.bitcoinAddress') }}
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
v-if="canReadClipboard"
|
||||||
|
@click="pasteFromClipboard"
|
||||||
|
class="text-xs px-2 py-0.5 rounded border bg-white/5 border-white/15 text-white/60 hover:text-white/90 transition-colors"
|
||||||
|
>
|
||||||
|
{{ effectiveMethod === 'lightning' ? 'Paste invoice' : 'Paste' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<textarea v-model="dest" rows="2" :placeholder="effectiveMethod === 'lightning' ? 'lnbc...' : effectiveMethod === 'ark' ? 'tark1… / lnbc… / user@lnaddress' : 'bc1...'" class="w-full input-glass font-mono"></textarea>
|
<textarea v-model="dest" rows="2" :placeholder="effectiveMethod === 'lightning' ? 'lnbc...' : effectiveMethod === 'ark' ? 'tark1… / lnbc… / user@lnaddress' : 'bc1...'" class="w-full input-glass font-mono"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -177,6 +193,35 @@ function toggleSendAll() {
|
|||||||
// Leaving the on-chain tab disarms the sweep so it can never apply elsewhere
|
// Leaving the on-chain tab disarms the sweep so it can never apply elsewhere
|
||||||
watch(sendMethod, (m) => { if (m !== 'onchain') sendAll.value = false })
|
watch(sendMethod, (m) => { if (m !== 'onchain') sendAll.value = false })
|
||||||
|
|
||||||
|
// Invoice-first lightning UX: a pasted invoice that fixes its amount locks
|
||||||
|
// the amount field (auto-filled, "set by invoice"); zero-amount invoices
|
||||||
|
// leave it editable. Clearing/leaving lightning unlocks again.
|
||||||
|
const pastedInvoiceAmount = computed<number | null>(() => {
|
||||||
|
if (effectiveMethod.value !== 'lightning') return null
|
||||||
|
const d = dest.value.trim()
|
||||||
|
if (!d) return null
|
||||||
|
return parseBolt11AmountSats(d.toLowerCase().startsWith('lightning:') ? d.slice(10) : d)
|
||||||
|
})
|
||||||
|
watch(pastedInvoiceAmount, (fixed, prev) => {
|
||||||
|
if (fixed !== null) amount.value = fixed
|
||||||
|
// Swapping a fixed-amount invoice for a zero-amount one: don't silently
|
||||||
|
// keep the previous invoice's sats — make the user type the new amount.
|
||||||
|
else if (prev !== null) amount.value = 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clipboard read needs a secure context (or the companion bridge); hide the
|
||||||
|
// button where it can't work — the textarea still accepts a manual paste.
|
||||||
|
const canReadClipboard = typeof navigator !== 'undefined' && !!navigator.clipboard?.readText
|
||||||
|
|
||||||
|
async function pasteFromClipboard() {
|
||||||
|
try {
|
||||||
|
const text = (await navigator.clipboard.readText()).trim()
|
||||||
|
if (text) dest.value = text
|
||||||
|
} catch {
|
||||||
|
// Permission denied — user can long-press/Ctrl+V into the field instead.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const effectiveMethod = computed(() => {
|
const effectiveMethod = computed(() => {
|
||||||
if (sendMethod.value !== 'auto') return sendMethod.value
|
if (sendMethod.value !== 'auto') return sendMethod.value
|
||||||
const amt = amount.value || 0
|
const amt = amount.value || 0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user