v1.6.8 sells tickets on-chain through SatsPay charges — a per-purchase watchonly address, a hosted charge page, and a webhook back into the extension. We are not adopting that: on-chain goes through the fork's native LndRestWallet support once core can mint purpose-bound receiving addresses (aiolabs/lnbits#53). This is the parity note on #41, applied as its own commit so the merge it follows stays a faithful diff of what upstream shipped. Removed: - services: the five SatsPay/watchonly HTTP clients (and with them the only uses of httpx and typing.Any in that module) - views_api: _get_watchonly_status, GET /events/onchain/status, the SatsPay charge branch of api_ticket_create, POST /tickets/{id}/satspay-webhook, PUT /tickets/{hash}/onchain-confirm - models: EventExtra.onchain_{enabled,wallet_id,zeroconf,fasttrack}, TicketExtra.satspay_charge_id, TicketPaymentRequest.satspay_charge_url - frontend: the organiser's "Onchain payments" panel and its watchonly wallet picker, the per-ticket confirm-onchain button, the SatsPay charge-page redirect, and the wallet-status fetch behind them `onchain` is no longer an accepted payment_method — there is no rail to fulfil it until #41 lands, so accepting it could only fail later. Kept as vocabulary for #41, in upstream's field names so it needs no migration: TicketExtra.onchain / onchain_address and TicketPaymentRequest.onchain_amount_sat. 35 routes register with no duplicates; ruff, black, prettier clean; 97 tests pass; mypy error set still identical to HEAD's baseline.
299 lines
9.3 KiB
JavaScript
299 lines
9.3 KiB
JavaScript
window.PageEventsDisplay = {
|
|
template: '#page-events-display',
|
|
data() {
|
|
return {
|
|
eventErrorLabel: '',
|
|
event: null,
|
|
paymentReq: null,
|
|
redirectUrl: null,
|
|
formDialog: {
|
|
show: false,
|
|
data: {
|
|
name: '',
|
|
email: '',
|
|
refund: '',
|
|
nostr_identifier: '',
|
|
ticket_wave_id: null,
|
|
payment_method: 'lightning'
|
|
}
|
|
},
|
|
ticketLink: {
|
|
show: false,
|
|
data: {
|
|
link: ''
|
|
}
|
|
},
|
|
receive: {
|
|
show: false,
|
|
status: 'pending',
|
|
paymentReq: null,
|
|
isFiat: false
|
|
},
|
|
paymentDismissMsg: null,
|
|
paymentWebsocket: null
|
|
}
|
|
},
|
|
async created() {
|
|
this.eventId = this.$route.params.id
|
|
this.event = await this.getEvent()
|
|
// Default to the first rail the organizer accepts (a card-only event
|
|
// must not submit "lightning").
|
|
this.formDialog.data.payment_method = this.paymentMethods[0] || 'lightning'
|
|
},
|
|
computed: {
|
|
formatDescription() {
|
|
return LNbits.utils.convertMarkdown(this.event?.info || '')
|
|
},
|
|
paymentMethods() {
|
|
// Mirrors `effective_payment_methods` on the backend: an explicit
|
|
// extra.payment_methods list wins, else Lightning + fiat-if-allow_fiat.
|
|
// Since v1.6.8 `allow_fiat` is a per-wave flag and `event.allow_fiat`
|
|
// is only the PRIMARY wave's, so prefer the active wave when there is
|
|
// one — otherwise a later fiat-enabled wave would not offer fiat.
|
|
const explicit = this.event?.extra?.payment_methods || []
|
|
if (explicit.length) return explicit
|
|
const allowFiat = this.selectedTicketWave
|
|
? this.selectedTicketWave.allow_fiat
|
|
: this.event?.allow_fiat
|
|
return ['lightning', ...(allowFiat ? ['fiat'] : [])]
|
|
},
|
|
activeTicketWaves() {
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
return (this.event?.extra?.ticket_waves || []).filter(
|
|
wave =>
|
|
wave.amount_tickets > 0 &&
|
|
wave.opening_date <= today &&
|
|
wave.closing_date >= today
|
|
)
|
|
},
|
|
selectedTicketWave() {
|
|
return (
|
|
this.activeTicketWaves.find(
|
|
wave => wave.id === this.formDialog.data.ticket_wave_id
|
|
) ||
|
|
this.activeTicketWaves[0] ||
|
|
null
|
|
)
|
|
},
|
|
showTicketWaveSelector() {
|
|
return this.activeTicketWaves.length > 1
|
|
},
|
|
allowFiatCheckout() {
|
|
return this.paymentMethods.includes('fiat')
|
|
},
|
|
paymentMethodOptions() {
|
|
// Options come from `paymentMethods` — the same list the backend
|
|
// validates against in `effective_payment_methods` — rather than
|
|
// being re-derived from per-method booleans, so a rail the server
|
|
// would reject can never be offered. The `|| method` fallback covers
|
|
// a method with no label yet (on-chain, once #41 lands).
|
|
const labels = {
|
|
lightning: 'Lightning',
|
|
fiat: this.fiatCheckoutLabel
|
|
}
|
|
return this.paymentMethods.map(method => ({
|
|
value: method,
|
|
label: labels[method] || method
|
|
}))
|
|
},
|
|
fiatCheckoutLabel() {
|
|
if (!this.allowFiatCheckout) return 'Fiat'
|
|
const unit = ['sat', 'sats'].includes(
|
|
(this.selectedTicketWave?.currency || '').toLowerCase()
|
|
)
|
|
? this.selectedTicketWave?.fiat_currency
|
|
: this.selectedTicketWave?.currency
|
|
return `Fiat (${(unit || 'GBP').toUpperCase()})`
|
|
},
|
|
allowEmailNotifications() {
|
|
return Boolean(this.event?.extra?.email_notifications)
|
|
},
|
|
allowNostrNotifications() {
|
|
return Boolean(this.event?.extra?.nostr_notifications)
|
|
}
|
|
},
|
|
methods: {
|
|
async getEvent() {
|
|
try {
|
|
const {data} = await LNbits.api.request(
|
|
'GET',
|
|
`/events/api/v1/events/${this.eventId}`
|
|
)
|
|
const activeWaves = (data.extra?.ticket_waves || []).filter(wave => {
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
return (
|
|
wave.amount_tickets > 0 &&
|
|
wave.opening_date <= today &&
|
|
wave.closing_date >= today
|
|
)
|
|
})
|
|
this.formDialog.data.ticket_wave_id =
|
|
activeWaves.length === 1 ? activeWaves[0].id : null
|
|
return data
|
|
} catch (error) {
|
|
this.eventErrorLabel = 'Event unavailable.'
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
resetForm(e) {
|
|
e.preventDefault()
|
|
this.formDialog.data.name = ''
|
|
this.formDialog.data.email = ''
|
|
this.formDialog.data.refund = ''
|
|
this.formDialog.data.nostr_identifier = ''
|
|
this.formDialog.data.ticket_wave_id =
|
|
this.activeTicketWaves.length === 1
|
|
? this.activeTicketWaves[0].id
|
|
: null
|
|
this.formDialog.data.promo_code = ''
|
|
this.formDialog.data.payment_method =
|
|
this.paymentMethods[0] || 'lightning'
|
|
},
|
|
|
|
closeReceiveDialog() {
|
|
if (this.paymentDismissMsg) {
|
|
this.paymentDismissMsg()
|
|
this.paymentDismissMsg = null
|
|
}
|
|
if (this.paymentWebsocket) {
|
|
this.paymentWebsocket.close()
|
|
this.paymentWebsocket = null
|
|
}
|
|
this.paymentReq = null
|
|
this.receive = {
|
|
show: false,
|
|
status: 'pending',
|
|
paymentReq: null,
|
|
isFiat: false
|
|
}
|
|
},
|
|
nameValidation(val) {
|
|
const regex = /[`!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]/g
|
|
return (
|
|
!regex.test(val) ||
|
|
'Please enter valid name. No special character allowed.'
|
|
)
|
|
},
|
|
emailValidation(val) {
|
|
const regex = /^[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}$/
|
|
return regex.test(val) || 'Please enter valid email.'
|
|
},
|
|
paymentSuccess(paymentHash) {
|
|
if (this.paymentDismissMsg) {
|
|
this.paymentDismissMsg()
|
|
this.paymentDismissMsg = null
|
|
}
|
|
this.paymentReq = null
|
|
this.formDialog.data.name = ''
|
|
this.formDialog.data.email = ''
|
|
this.formDialog.data.refund = ''
|
|
this.formDialog.data.nostr_identifier = ''
|
|
this.formDialog.data.ticket_wave_id =
|
|
this.activeTicketWaves.length === 1
|
|
? this.activeTicketWaves[0].id
|
|
: null
|
|
this.formDialog.data.payment_method =
|
|
this.paymentMethods[0] || 'lightning'
|
|
Quasar.Notify.create({
|
|
type: 'positive',
|
|
message: 'Sent, thank you!',
|
|
icon: null
|
|
})
|
|
this.receive = {
|
|
show: false,
|
|
status: 'complete',
|
|
paymentReq: null,
|
|
isFiat: false
|
|
}
|
|
this.ticketLink = {
|
|
show: true,
|
|
data: {
|
|
link: `/events/ticket/${paymentHash}`
|
|
}
|
|
}
|
|
window.open(`/events/ticket/${paymentHash}`, '_blank', 'noopener')
|
|
},
|
|
async createInvoice() {
|
|
try {
|
|
const {data} = await LNbits.api.request(
|
|
'POST',
|
|
`/events/api/v1/tickets/${this.eventId}`,
|
|
null,
|
|
{
|
|
name: this.formDialog.data.name,
|
|
email: this.formDialog.data.email,
|
|
ticket_wave_id: this.formDialog.data.ticket_wave_id || null,
|
|
promo_code: this.formDialog.data.promo_code || null,
|
|
refund_address: this.formDialog.data.refund || null,
|
|
nostr_identifier: this.formDialog.data.nostr_identifier || null,
|
|
// Gate matches the template's (`paymentMethods.length > 1`).
|
|
// v1.6.8 gated on `showPaymentMethodSelector`
|
|
// (`allowFiatCheckout || allowOnchain`), a different condition:
|
|
// where the two disagreed the buyer's visible choice was
|
|
// silently replaced with 'lightning'.
|
|
payment_method:
|
|
this.paymentMethods.length > 1
|
|
? this.formDialog.data.payment_method
|
|
: this.paymentMethods[0] || 'lightning'
|
|
}
|
|
)
|
|
const isFiat = Boolean(data.is_fiat)
|
|
this.paymentReq = isFiat
|
|
? data.fiat_payment_request || null
|
|
: data.payment_request
|
|
this.paymentHash = data.payment_hash
|
|
|
|
this.paymentDismissMsg = Quasar.Notify.create({
|
|
timeout: 0,
|
|
message: 'Waiting for payment...'
|
|
})
|
|
this.receive = {
|
|
show: true,
|
|
status: 'pending',
|
|
paymentReq: this.paymentReq,
|
|
isFiat
|
|
}
|
|
if (isFiat && this.paymentReq) {
|
|
window.open(this.paymentReq, '_blank', 'noopener')
|
|
}
|
|
this.paymentWatcher(this.paymentHash)
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
paymentWatcher(paymentHash) {
|
|
if (this.paymentWebsocket) {
|
|
this.paymentWebsocket.close()
|
|
}
|
|
|
|
const url = new URL(window.location)
|
|
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
url.pathname = `/events/api/v1/tickets/ws/${paymentHash}`
|
|
url.search = ''
|
|
url.hash = ''
|
|
|
|
const ws = new WebSocket(url.toString())
|
|
this.paymentWebsocket = ws
|
|
|
|
ws.onmessage = event => {
|
|
const data = JSON.parse(event.data)
|
|
if (data.paid === true) {
|
|
this.paymentSuccess(paymentHash)
|
|
ws.close()
|
|
}
|
|
}
|
|
ws.onerror = error => {
|
|
console.error('WebSocket error:', error)
|
|
}
|
|
ws.onclose = () => {
|
|
if (this.paymentWebsocket !== ws) return
|
|
this.paymentWebsocket = null
|
|
if (this.receive.show) {
|
|
setTimeout(() => {
|
|
if (this.receive.show) this.paymentWatcher(paymentHash)
|
|
}, 3000)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|