feat: guest checkout, Stripe return flow, emailed QR tickets, per-event payment methods (v1.6.1-aio.8) #36
5 changed files with 94 additions and 38 deletions
feat: per-event payment methods (extra.payment_methods)
Organizers pick which rails an event accepts — Lightning, card (fiat) or both — instead of a bare "allow fiat" toggle. `extra.payment_methods` uses the field name upstream v2 (lnbits/events#64) introduces so the eventual rebase merges cleanly; an empty list keeps the legacy rule (Lightning always, fiat when allow_fiat), and allow_fiat stays the fiat-currency carrier, kept in lockstep on save. The effective list is published as the NIP-52 tag `tickets_payment_methods` so clients render exactly the buttons the purchase endpoint will accept, and the buyer page defaults to the first accepted rail (a card-only event never submits "lightning"). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
commit
c2d9a96239
|
|
@ -17,7 +17,7 @@ from datetime import datetime, timezone
|
||||||
from lnbits.core.signers import NostrSigner
|
from lnbits.core.signers import NostrSigner
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from .models import Event
|
from .models import Event, effective_payment_methods
|
||||||
from .nostr.event import NostrEvent
|
from .nostr.event import NostrEvent
|
||||||
from .nostr_timestamp import monotonic_created_at
|
from .nostr_timestamp import monotonic_created_at
|
||||||
|
|
||||||
|
|
@ -110,6 +110,11 @@ def build_nip52_event(event: Event, pubkey: str) -> NostrEvent:
|
||||||
tags.append(["tickets_allow_fiat", "true"])
|
tags.append(["tickets_allow_fiat", "true"])
|
||||||
if event.fiat_currency:
|
if event.fiat_currency:
|
||||||
tags.append(["tickets_fiat_currency", event.fiat_currency])
|
tags.append(["tickets_fiat_currency", event.fiat_currency])
|
||||||
|
# Rails the organizer accepts, resolved through the same helper the
|
||||||
|
# ticket endpoint enforces with, so a client can render exactly the
|
||||||
|
# buttons that will be accepted (e.g. a card-only event) without a REST
|
||||||
|
# round-trip. Comma-separated, lowercase.
|
||||||
|
tags.append(["tickets_payment_methods", ",".join(effective_payment_methods(event))])
|
||||||
|
|
||||||
# NIP-52 calendar events are replaceable: this d-tag is republished
|
# NIP-52 calendar events are replaceable: this d-tag is republished
|
||||||
# whenever inventory changes (a ticket sells). Use a strictly-monotonic
|
# whenever inventory changes (a ticket sells). Use a strictly-monotonic
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,29 @@ window.PageEventsDisplay = {
|
||||||
async created() {
|
async created() {
|
||||||
this.eventId = this.$route.params.id
|
this.eventId = this.$route.params.id
|
||||||
this.event = await this.getEvent()
|
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: {
|
computed: {
|
||||||
formatDescription() {
|
formatDescription() {
|
||||||
return LNbits.utils.convertMarkdown(this.event?.info || '')
|
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.
|
||||||
|
const explicit = this.event?.extra?.payment_methods || []
|
||||||
|
if (explicit.length) return explicit
|
||||||
|
return ['lightning', ...(this.event?.allow_fiat ? ['fiat'] : [])]
|
||||||
|
},
|
||||||
allowFiatCheckout() {
|
allowFiatCheckout() {
|
||||||
return Boolean(this.event?.allow_fiat)
|
return this.paymentMethods.includes('fiat')
|
||||||
|
},
|
||||||
|
paymentMethodOptions() {
|
||||||
|
return this.paymentMethods.map(method => ({
|
||||||
|
value: method,
|
||||||
|
label: method === 'fiat' ? this.fiatCheckoutLabel : 'Lightning'
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
fiatCheckoutLabel() {
|
fiatCheckoutLabel() {
|
||||||
if (!this.allowFiatCheckout) return 'Fiat'
|
if (!this.allowFiatCheckout) return 'Fiat'
|
||||||
|
|
@ -78,7 +94,8 @@ window.PageEventsDisplay = {
|
||||||
this.formDialog.data.email = ''
|
this.formDialog.data.email = ''
|
||||||
this.formDialog.data.refund = ''
|
this.formDialog.data.refund = ''
|
||||||
this.formDialog.data.nostr_identifier = ''
|
this.formDialog.data.nostr_identifier = ''
|
||||||
this.formDialog.data.payment_method = 'lightning'
|
this.formDialog.data.payment_method =
|
||||||
|
this.paymentMethods[0] || 'lightning'
|
||||||
},
|
},
|
||||||
|
|
||||||
closeReceiveDialog() {
|
closeReceiveDialog() {
|
||||||
|
|
@ -112,7 +129,8 @@ window.PageEventsDisplay = {
|
||||||
this.formDialog.data.email = ''
|
this.formDialog.data.email = ''
|
||||||
this.formDialog.data.refund = ''
|
this.formDialog.data.refund = ''
|
||||||
this.formDialog.data.nostr_identifier = ''
|
this.formDialog.data.nostr_identifier = ''
|
||||||
this.formDialog.data.payment_method = 'lightning'
|
this.formDialog.data.payment_method =
|
||||||
|
this.paymentMethods[0] || 'lightning'
|
||||||
Quasar.Notify.create({
|
Quasar.Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Sent, thank you!',
|
message: 'Sent, thank you!',
|
||||||
|
|
|
||||||
|
|
@ -90,20 +90,18 @@
|
||||||
:hint="`If minimum tickets (${event.extra?.min_tickets}) are not met, refund will be sent.`"
|
:hint="`If minimum tickets (${event.extra?.min_tickets}) are not met, refund will be sent.`"
|
||||||
></q-input>
|
></q-input>
|
||||||
<div class="row q-col-gutter-md q-pt-lg items-center">
|
<div class="row q-col-gutter-md q-pt-lg items-center">
|
||||||
<div v-if="allowFiatCheckout" class="col-auto">
|
<div v-if="paymentMethods.length > 1" class="col-auto">
|
||||||
<q-option-group
|
<q-option-group
|
||||||
v-model="formDialog.data.payment_method"
|
v-model="formDialog.data.payment_method"
|
||||||
inline
|
inline
|
||||||
:options="[
|
:options="paymentMethodOptions"
|
||||||
{label: 'Lightning', value: 'lightning'},
|
|
||||||
{
|
|
||||||
label: fiatCheckoutLabel,
|
|
||||||
value: 'fiat'
|
|
||||||
}
|
|
||||||
]"
|
|
||||||
></q-option-group>
|
></q-option-group>
|
||||||
</div>
|
</div>
|
||||||
<div :class="allowFiatCheckout ? 'col-12 col-md-3' : 'col-12'">
|
<div
|
||||||
|
:class="
|
||||||
|
paymentMethods.length > 1 ? 'col-12 col-md-3' : 'col-12'
|
||||||
|
"
|
||||||
|
>
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,7 @@ window.PageEvents = {
|
||||||
allow_fiat: false,
|
allow_fiat: false,
|
||||||
fiat_currency: 'GBP',
|
fiat_currency: 'GBP',
|
||||||
extra: {
|
extra: {
|
||||||
|
payment_methods: ['lightning'],
|
||||||
promo_codes: [],
|
promo_codes: [],
|
||||||
notification_subject: '',
|
notification_subject: '',
|
||||||
notification_body: ''
|
notification_body: ''
|
||||||
|
|
@ -271,12 +272,7 @@ window.PageEvents = {
|
||||||
},
|
},
|
||||||
saveSettings() {
|
saveSettings() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request('PUT', '/events/api/v1/events/settings', null, this.settings)
|
||||||
'PUT',
|
|
||||||
'/events/api/v1/events/settings',
|
|
||||||
null,
|
|
||||||
this.settings
|
|
||||||
)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
Quasar.Notify.create({type: 'positive', message: 'Settings saved'})
|
Quasar.Notify.create({type: 'positive', message: 'Settings saved'})
|
||||||
})
|
})
|
||||||
|
|
@ -326,7 +322,7 @@ window.PageEvents = {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog(
|
.confirmDialog(
|
||||||
'Re-emit every approved event to Nostr relays? This is safe ' +
|
'Re-emit every approved event to Nostr relays? This is safe ' +
|
||||||
'to run multiple times but generates one event per approved row.'
|
'to run multiple times but generates one event per approved row.'
|
||||||
)
|
)
|
||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
this.republishing = true
|
this.republishing = true
|
||||||
|
|
@ -351,9 +347,7 @@ window.PageEvents = {
|
||||||
},
|
},
|
||||||
republishMyEvents() {
|
republishMyEvents() {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog(
|
.confirmDialog('Re-emit your approved events to Nostr relays?')
|
||||||
'Re-emit your approved events to Nostr relays?'
|
|
||||||
)
|
|
||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
this.republishingMine = true
|
this.republishingMine = true
|
||||||
LNbits.api
|
LNbits.api
|
||||||
|
|
@ -420,6 +414,17 @@ window.PageEvents = {
|
||||||
code: code.code.trim().toUpperCase()
|
code: code.code.trim().toUpperCase()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
const methods = data.extra?.payment_methods || []
|
||||||
|
if (methods.length === 0) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Select at least one payment method.'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// allow_fiat stays the fiat-currency carrier the backend and the
|
||||||
|
// NIP-52 tags read; keep it in lockstep with the checkbox list.
|
||||||
|
data.allow_fiat = methods.includes('fiat')
|
||||||
if (!this.isFiatCurrency(data.currency)) {
|
if (!this.isFiatCurrency(data.currency)) {
|
||||||
if (!data.allow_fiat) {
|
if (!data.allow_fiat) {
|
||||||
data.fiat_currency = 'GBP'
|
data.fiat_currency = 'GBP'
|
||||||
|
|
@ -439,6 +444,15 @@ window.PageEvents = {
|
||||||
const end = this.splitDateTime(data.event_end_date)
|
const end = this.splitDateTime(data.event_end_date)
|
||||||
this.formDialog.data = {
|
this.formDialog.data = {
|
||||||
...data,
|
...data,
|
||||||
|
extra: {
|
||||||
|
...(data.extra || {}),
|
||||||
|
// Events created before extra.payment_methods existed carry an
|
||||||
|
// empty list; show the rails the backend actually accepts for
|
||||||
|
// them (Lightning always, fiat when allow_fiat).
|
||||||
|
payment_methods: data.extra?.payment_methods?.length
|
||||||
|
? data.extra.payment_methods
|
||||||
|
: ['lightning', ...(data.allow_fiat ? ['fiat'] : [])]
|
||||||
|
},
|
||||||
event_start_day: start.day,
|
event_start_day: start.day,
|
||||||
event_start_time: start.time,
|
event_start_time: start.time,
|
||||||
event_end_day: end.day,
|
event_end_day: end.day,
|
||||||
|
|
@ -454,6 +468,7 @@ window.PageEvents = {
|
||||||
event_end_day: '',
|
event_end_day: '',
|
||||||
event_end_time: '',
|
event_end_time: '',
|
||||||
extra: {
|
extra: {
|
||||||
|
payment_methods: ['lightning'],
|
||||||
conditional: false,
|
conditional: false,
|
||||||
min_tickets: 1,
|
min_tickets: 1,
|
||||||
email_notifications: false,
|
email_notifications: false,
|
||||||
|
|
@ -473,6 +488,7 @@ window.PageEvents = {
|
||||||
allow_fiat: false,
|
allow_fiat: false,
|
||||||
fiat_currency: 'GBP',
|
fiat_currency: 'GBP',
|
||||||
extra: {
|
extra: {
|
||||||
|
payment_methods: ['lightning'],
|
||||||
email_notifications: false,
|
email_notifications: false,
|
||||||
nostr_notifications: false,
|
nostr_notifications: false,
|
||||||
promo_codes: [],
|
promo_codes: [],
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<span class="text-subtitle2">Republish to Nostr</span>
|
<span class="text-subtitle2">Republish to Nostr</span>
|
||||||
<div class="text-caption text-grey-7" style="color: #aaa">
|
<div class="text-caption text-grey-7" style="color: #aaa">
|
||||||
Re-emit every approved event so connected clients pick
|
Re-emit every approved event so connected clients pick up the
|
||||||
up the latest tag set. Useful after the extension
|
latest tag set. Useful after the extension publisher changes
|
||||||
publisher changes (e.g. new tickets_* tags) so existing
|
(e.g. new tickets_* tags) so existing events don't need a
|
||||||
events don't need a per-event edit.
|
per-event edit.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
|
|
@ -56,8 +56,8 @@
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-caption q-mt-sm" style="color: #aaa">
|
<div class="text-caption q-mt-sm" style="color: #aaa">
|
||||||
Re-emit your approved events to Nostr relays. Useful after
|
Re-emit your approved events to Nostr relays. Useful after a
|
||||||
a publisher upgrade or if a relay dropped your events.
|
publisher upgrade or if a relay dropped your events.
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
@ -228,7 +228,13 @@
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="col.name === 'status'"
|
v-if="col.name === 'status'"
|
||||||
:color="col.value === 'approved' ? 'green' : col.value === 'proposed' ? 'orange' : 'red'"
|
:color="
|
||||||
|
col.value === 'approved'
|
||||||
|
? 'green'
|
||||||
|
: col.value === 'proposed'
|
||||||
|
? 'orange'
|
||||||
|
: 'red'
|
||||||
|
"
|
||||||
:label="col.value"
|
:label="col.value"
|
||||||
></q-badge>
|
></q-badge>
|
||||||
<span v-else v-text="col.value"></span>
|
<span v-else v-text="col.value"></span>
|
||||||
|
|
@ -399,7 +405,13 @@
|
||||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||||
<q-badge
|
<q-badge
|
||||||
v-if="col.name === 'status'"
|
v-if="col.name === 'status'"
|
||||||
:color="col.value === 'approved' ? 'green' : col.value === 'proposed' ? 'orange' : 'red'"
|
:color="
|
||||||
|
col.value === 'approved'
|
||||||
|
? 'green'
|
||||||
|
: col.value === 'proposed'
|
||||||
|
? 'orange'
|
||||||
|
: 'red'
|
||||||
|
"
|
||||||
:label="col.value"
|
:label="col.value"
|
||||||
></q-badge>
|
></q-badge>
|
||||||
<span v-else v-text="col.value"></span>
|
<span v-else v-text="col.value"></span>
|
||||||
|
|
@ -587,15 +599,22 @@
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-toggle
|
<div class="q-mt-sm">
|
||||||
v-model="formDialog.data.allow_fiat"
|
<div class="text-caption text-grey-7">Payment methods *</div>
|
||||||
label="Allow fiat checkout"
|
<q-option-group
|
||||||
left-label
|
v-model="formDialog.data.extra.payment_methods"
|
||||||
hint="Lets attendees pay through a configured fiat provider using the event currency."
|
type="checkbox"
|
||||||
></q-toggle>
|
inline
|
||||||
|
:options="paymentMethodOptions"
|
||||||
|
></q-option-group>
|
||||||
|
<div class="text-caption text-grey-7">
|
||||||
|
Card / fiat checkout goes through the fiat provider configured on
|
||||||
|
this LNbits instance. Untick Lightning for a card-only event.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<q-select
|
<q-select
|
||||||
v-if="
|
v-if="
|
||||||
formDialog.data.allow_fiat &&
|
acceptsFiat &&
|
||||||
['sat', 'sats'].includes(
|
['sat', 'sats'].includes(
|
||||||
(formDialog.data.currency || '').toLowerCase()
|
(formDialog.data.currency || '').toLowerCase()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue