fix: WebLN QR fallback for anon users (#1858)

* fix: WebLN QR fallback for anon users

* wip: clear zap color on payment fail

* reverse clearItemMeAnonSats

* webln-specific retry bypass

* cleanup

* send WebLN payment when user is Anon AND on QR

* skip wallet checking on anon

* Use WalletError for all errors in webln.sendPayment

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
soxa 2025-03-01 23:56:18 +01:00 committed by GitHub
parent 5e7fd693f1
commit f72af08882
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 29 additions and 5 deletions

View File

@ -1,8 +1,9 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import Invoice from '@/components/invoice' import Invoice from '@/components/invoice'
import { InvoiceCanceledError, InvoiceExpiredError } from '@/wallets/errors' import { InvoiceCanceledError, InvoiceExpiredError, AnonWalletError } from '@/wallets/errors'
import { useShowModal } from '@/components/modal' import { useShowModal } from '@/components/modal'
import useInvoice from '@/components/use-invoice' import useInvoice from '@/components/use-invoice'
import { sendPayment } from '@/wallets/webln/client'
export default function useQrPayment () { export default function useQrPayment () {
const invoice = useInvoice() const invoice = useInvoice()
@ -16,6 +17,10 @@ export default function useQrPayment () {
waitFor = inv => inv?.satsReceived > 0 waitFor = inv => inv?.satsReceived > 0
} = {} } = {}
) => { ) => {
// if anon user and webln is available, try to pay with webln
if (typeof window.webln !== 'undefined' && (walletError instanceof AnonWalletError)) {
sendPayment(inv.bolt11).catch(e => { console.error('WebLN payment failed:', e) })
}
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
let paid let paid
const cancelAndReject = async (onClose) => { const cancelAndReject = async (onClose) => {

View File

@ -62,6 +62,13 @@ export class WalletsNotAvailableError extends WalletConfigurationError {
} }
} }
export class AnonWalletError extends WalletConfigurationError {
constructor () {
super('anon cannot pay with wallets')
this.name = 'AnonWalletError'
}
}
export class WalletAggregateError extends WalletError { export class WalletAggregateError extends WalletError {
constructor (errors, invoice) { constructor (errors, invoice) {
super('WalletAggregateError') super('WalletAggregateError')

View File

@ -4,23 +4,30 @@ import { formatSats } from '@/lib/format'
import useInvoice from '@/components/use-invoice' import useInvoice from '@/components/use-invoice'
import { FAST_POLL_INTERVAL, WALLET_SEND_PAYMENT_TIMEOUT_MS } from '@/lib/constants' import { FAST_POLL_INTERVAL, WALLET_SEND_PAYMENT_TIMEOUT_MS } from '@/lib/constants'
import { import {
WalletsNotAvailableError, WalletSenderError, WalletAggregateError, WalletPaymentAggregateError, AnonWalletError, WalletsNotAvailableError, WalletSenderError, WalletAggregateError, WalletPaymentAggregateError,
WalletNotEnabledError, WalletSendNotConfiguredError, WalletPaymentError, WalletError, WalletReceiverError WalletNotEnabledError, WalletSendNotConfiguredError, WalletPaymentError, WalletError, WalletReceiverError
} from '@/wallets/errors' } from '@/wallets/errors'
import { canSend } from './common' import { canSend } from './common'
import { useWalletLoggerFactory } from './logger' import { useWalletLoggerFactory } from './logger'
import { timeoutSignal, withTimeout } from '@/lib/time' import { timeoutSignal, withTimeout } from '@/lib/time'
import { useMe } from '@/components/me'
export function useWalletPayment () { export function useWalletPayment () {
const wallets = useSendWallets() const wallets = useSendWallets()
const sendPayment = useSendPayment() const sendPayment = useSendPayment()
const loggerFactory = useWalletLoggerFactory() const loggerFactory = useWalletLoggerFactory()
const invoiceHelper = useInvoice() const invoiceHelper = useInvoice()
const { me } = useMe()
return useCallback(async (invoice, { waitFor, updateOnFallback } = {}) => { return useCallback(async (invoice, { waitFor, updateOnFallback } = {}) => {
let aggregateError = new WalletAggregateError([]) let aggregateError = new WalletAggregateError([])
let latestInvoice = invoice let latestInvoice = invoice
// anon user cannot pay with wallets
if (!me) {
throw new AnonWalletError()
}
// throw a special error that caller can handle separately if no payment was attempted // throw a special error that caller can handle separately if no payment was attempted
if (wallets.length === 0) { if (wallets.length === 0) {
throw new WalletsNotAvailableError() throw new WalletsNotAvailableError()

View File

@ -1,14 +1,19 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { SSR } from '@/lib/constants' import { SSR } from '@/lib/constants'
import { WalletError } from '../errors'
export * from '@/wallets/webln' export * from '@/wallets/webln'
export const sendPayment = async (bolt11) => { export const sendPayment = async (bolt11) => {
if (typeof window.webln === 'undefined') { if (typeof window.webln === 'undefined') {
throw new Error('WebLN provider not found') throw new WalletError('WebLN provider not found')
} }
// this will prompt the user to unlock the wallet if it's locked // this will prompt the user to unlock the wallet if it's locked
try {
await window.webln.enable() await window.webln.enable()
} catch (err) {
throw new WalletError(err.message)
}
// this will prompt for payment if no budget is set // this will prompt for payment if no budget is set
const response = await window.webln.sendPayment(bolt11) const response = await window.webln.sendPayment(bolt11)
@ -16,7 +21,7 @@ export const sendPayment = async (bolt11) => {
// sendPayment returns nothing if WebLN was enabled // sendPayment returns nothing if WebLN was enabled
// but browser extension that provides WebLN was then disabled // but browser extension that provides WebLN was then disabled
// without reloading the page // without reloading the page
throw new Error('sendPayment returned no response') throw new WalletError('sendPayment returned no response')
} }
return response.preimage return response.preimage