From 332087208ed6022b1b87e6f1b3b4e60061211574 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 27 Jan 2026 09:27:49 +0000 Subject: [PATCH 01/11] uv update --- config.json | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.json b/config.json index 11312b6..fb33ebe 100644 --- a/config.json +++ b/config.json @@ -6,7 +6,7 @@ "short_description": "Sell and register event tickets", "description": "", "tile": "/events/static/image/events.png", - "min_lnbits_version": "1.3.0", + "min_lnbits_version": "1.4.0", "contributors": [ { "name": "talvasconcelos", diff --git a/pyproject.toml b/pyproject.toml index 4508802..bec6870 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,8 @@ dependencies = [ "lnbits>1" ] [tool.poetry] package-mode = false -[tool.uv] -dev-dependencies = [ +[dependency-groups] +dev = [ "black", "pytest-asyncio", "pytest", From 95f6689b66acbe836483374d9389fac7bc186e9c Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 28 Jan 2026 08:26:00 +0000 Subject: [PATCH 02/11] index --- static/js/index.js | 7 +- static/js/index.vue | 478 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 481 insertions(+), 4 deletions(-) create mode 100644 static/js/index.vue diff --git a/static/js/index.js b/static/js/index.js index d26133c..aeb01d6 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -5,9 +5,8 @@ const mapEvents = function (obj) { return obj } -window.app = Vue.createApp({ - el: '#vue', - mixins: [windowMixin], +window.PageEvents = { + template: '#page-events', data() { return { events: [], @@ -293,4 +292,4 @@ window.app = Vue.createApp({ this.currencies = await LNbits.api.getCurrencies() } } -}) +} diff --git a/static/js/index.vue b/static/js/index.vue new file mode 100644 index 0000000..41b3767 --- /dev/null +++ b/static/js/index.vue @@ -0,0 +1,478 @@ + From 177665c80ae0ddc1bccce1a0d7044dbbb188f02e Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 28 Jan 2026 08:32:19 +0000 Subject: [PATCH 03/11] display --- static/js/display.js | 7 ++- static/js/display.vue | 112 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 static/js/display.vue diff --git a/static/js/display.js b/static/js/display.js index 6098e5a..44e1520 100644 --- a/static/js/display.js +++ b/static/js/display.js @@ -1,6 +1,5 @@ -window.app = Vue.createApp({ - el: '#vue', - mixins: [windowMixin], +window.PageEventsDisplay = { + template: '#page-events-display', data() { return { paymentReq: null, @@ -127,4 +126,4 @@ window.app = Vue.createApp({ .catch(LNbits.utils.notifyApiError) } } -}) +} diff --git a/static/js/display.vue b/static/js/display.vue new file mode 100644 index 0000000..7143e1f --- /dev/null +++ b/static/js/display.vue @@ -0,0 +1,112 @@ + From efea14e2e1da8854416d9836800fc9746e30b842 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 28 Jan 2026 08:32:25 +0000 Subject: [PATCH 04/11] register --- static/js/register.js | 9 ++--- static/js/register.vue | 78 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 static/js/register.vue diff --git a/static/js/register.js b/static/js/register.js index a7ab92f..bc1e269 100644 --- a/static/js/register.js +++ b/static/js/register.js @@ -1,3 +1,5 @@ +const {template} = require('underscore') + const mapEvents = function (obj) { obj.date = Quasar.date.formatDate( new Date(obj.time * 1000), @@ -8,9 +10,8 @@ const mapEvents = function (obj) { return obj } -window.app = Vue.createApp({ - el: '#vue', - mixins: [windowMixin], +window.PageEventsRegister = { + template: '#page-events-register', data() { return { tickets: [], @@ -75,4 +76,4 @@ window.app = Vue.createApp({ created() { this.getEventTickets() } -}) +} diff --git a/static/js/register.vue b/static/js/register.vue new file mode 100644 index 0000000..bcaacc8 --- /dev/null +++ b/static/js/register.vue @@ -0,0 +1,78 @@ + From 7f27efa70a22e6df1803183cc0118f2c8d54d836 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 28 Jan 2026 08:40:34 +0000 Subject: [PATCH 05/11] tickets --- static/js/ticket.js | 8 ++++++++ static/js/ticket.vue | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 static/js/ticket.js create mode 100644 static/js/ticket.vue diff --git a/static/js/ticket.js b/static/js/ticket.js new file mode 100644 index 0000000..3fc4121 --- /dev/null +++ b/static/js/ticket.js @@ -0,0 +1,8 @@ +window.PageEventsTicket = { + template: '#page-events-ticket', + methods: { + printWindow() { + window.print() + } + } +} diff --git a/static/js/ticket.vue b/static/js/ticket.vue new file mode 100644 index 0000000..0a58da9 --- /dev/null +++ b/static/js/ticket.vue @@ -0,0 +1,27 @@ + From b80dbcc0257a78020ec2492613287bc1cefcc6a8 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 28 Jan 2026 08:48:37 +0000 Subject: [PATCH 06/11] add routes --- static/routes.json | 26 +++++ views.py | 285 ++++++++++++++++++++++++--------------------- 2 files changed, 181 insertions(+), 130 deletions(-) create mode 100644 static/routes.json diff --git a/static/routes.json b/static/routes.json new file mode 100644 index 0000000..4fccdec --- /dev/null +++ b/static/routes.json @@ -0,0 +1,26 @@ +[ + { + "path": "/events/", + "name": "PageEvents", + "template": "/events/static/js/index.vue", + "component": "/events/static/js/index.js" + }, + { + "path": "/events/:id", + "name": "PageEventsPublic", + "template": "/events/static/js/display.vue", + "component": "/events/static/js/display.js" + }, + { + "path": "/events/ticket/:id", + "name": "PageEventsTicket", + "template": "/events/static/js/ticket.vue", + "component": "/events/static/js/ticket.js" + }, + { + "path": "/events/register/:id", + "name": "PageEventsRegister", + "template": "/events/static/js/register.vue", + "component": "/events/static/js/register.js" + } +] \ No newline at end of file diff --git a/views.py b/views.py index 0680dcc..dba2e1b 100644 --- a/views.py +++ b/views.py @@ -1,139 +1,164 @@ -from datetime import date, datetime -from http import HTTPStatus - from fastapi import APIRouter, Depends, Request -from lnbits.core.models import User -from lnbits.decorators import check_user_exists -from lnbits.helpers import template_renderer from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse +from lnbits.core.models import User +from lnbits.core.views.generic import index, index_public +from lnbits.decorators import check_account_id_exists, check_user_exists +from lnbits.helpers import template_renderer + from .crud import get_event, get_ticket, purge_unpaid_tickets, update_event from .services import refund_tickets events_generic_router = APIRouter() - -def events_renderer(): - return template_renderer(["events/templates"]) - - -@events_generic_router.get("/", response_class=HTMLResponse) -async def index(request: Request, user: User = Depends(check_user_exists)): - return events_renderer().TemplateResponse( - "events/index.html", {"request": request, "user": user.json()} - ) - - -@events_generic_router.get("/{event_id}", response_class=HTMLResponse) -async def display(request: Request, event_id): - event = await get_event(event_id) - if not event: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." - ) - - await purge_unpaid_tickets(event_id) - - is_window_open = ( - date.today() < datetime.strptime(event.closing_date, "%Y-%m-%d").date() - ) - is_min_tickets_met = ( - event.sold >= event.extra.min_tickets if event.extra.conditional else True - ) - - if event.amount_tickets < 1: - return events_renderer().TemplateResponse( - "events/error.html", - { - "request": request, - "event_name": event.name, - "event_error": "Sorry, tickets are sold out :(", - }, - ) - if event.extra.conditional and not is_min_tickets_met and not is_window_open: - event.canceled = True - await update_event(event) - await refund_tickets(event_id) - - return events_renderer().TemplateResponse( - "events/error.html", - { - "request": request, - "event_name": event.name, - "event_error": "Sorry, event was cancelled.", - }, - ) - if not is_window_open: - return events_renderer().TemplateResponse( - "events/error.html", - { - "request": request, - "event_name": event.name, - "event_error": "Sorry, ticket closing date has passed :(", - }, - ) - - if len(event.extra.promo_codes) > 0: - has_promo_codes = True - else: - has_promo_codes = False - - event.extra.promo_codes = [] - return events_renderer().TemplateResponse( - "events/display.html", - { - "request": request, - "event_id": event_id, - "event_name": event.name, - "event_info": event.info, - "event_price": event.price_per_ticket, - "event_banner": event.banner, - "event_extra": event.extra.json(), - "has_promo_codes": has_promo_codes, - }, - ) - - -@events_generic_router.get("/ticket/{ticket_id}", response_class=HTMLResponse) -async def ticket(request: Request, ticket_id): - ticket = await get_ticket(ticket_id) - if not ticket: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist." - ) - - event = await get_event(ticket.event) - if not event: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." - ) - - return events_renderer().TemplateResponse( - "events/ticket.html", - { - "request": request, - "ticket_id": ticket_id, - "ticket_name": event.name, - "ticket_info": event.info, - }, - ) - - -@events_generic_router.get("/register/{event_id}", response_class=HTMLResponse) -async def register(request: Request, event_id): - event = await get_event(event_id) - if not event: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." - ) - - return events_renderer().TemplateResponse( - "events/register.html", - { - "request": request, - "event_id": event_id, - "event_name": event.name, - "wallet_id": event.wallet, - }, - ) +""" +bitcoinswitch_generic_router.add_api_route( + "/", methods=["GET"], endpoint=index, dependencies=[Depends(check_user_exists)] +) + +""" + +events_generic_router.add_api_route( + "/", + methods=["GET"], + endpoint=index, + dependencies=[Depends(check_account_id_exists)], +) + +events_generic_router.add_api_route( + "/{event_id}", methods=["GET"], endpoint=index_public +) + +events_generic_router.add_api_route( + "/ticket/{ticket_id}", methods=["GET"], endpoint=index_public +) + +events_generic_router.add_api_route( + "/register/{event_id}", methods=["GET"], endpoint=index_public +) + + +# def events_renderer(): +# return template_renderer(["events/templates"]) + + +# @events_generic_router.get("/", response_class=HTMLResponse) +# async def index(request: Request, user: User = Depends(check_user_exists)): +# return events_renderer().TemplateResponse( +# "events/index.html", {"request": request, "user": user.json()} +# ) + + +# @events_generic_router.get("/{event_id}", response_class=HTMLResponse) +# async def display(request: Request, event_id): +# event = await get_event(event_id) +# if not event: +# raise HTTPException( +# status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." +# ) + +# await purge_unpaid_tickets(event_id) + +# is_window_open = ( +# date.today() < datetime.strptime(event.closing_date, "%Y-%m-%d").date() +# ) +# is_min_tickets_met = ( +# event.sold >= event.extra.min_tickets if event.extra.conditional else True +# ) + +# if event.amount_tickets < 1: +# return events_renderer().TemplateResponse( +# "events/error.html", +# { +# "request": request, +# "event_name": event.name, +# "event_error": "Sorry, tickets are sold out :(", +# }, +# ) +# if event.extra.conditional and not is_min_tickets_met and not is_window_open: +# event.canceled = True +# await update_event(event) +# await refund_tickets(event_id) + +# return events_renderer().TemplateResponse( +# "events/error.html", +# { +# "request": request, +# "event_name": event.name, +# "event_error": "Sorry, event was cancelled.", +# }, +# ) +# if not is_window_open: +# return events_renderer().TemplateResponse( +# "events/error.html", +# { +# "request": request, +# "event_name": event.name, +# "event_error": "Sorry, ticket closing date has passed :(", +# }, +# ) + +# if len(event.extra.promo_codes) > 0: +# has_promo_codes = True +# else: +# has_promo_codes = False + +# event.extra.promo_codes = [] +# return events_renderer().TemplateResponse( +# "events/display.html", +# { +# "request": request, +# "event_id": event_id, +# "event_name": event.name, +# "event_info": event.info, +# "event_price": event.price_per_ticket, +# "event_banner": event.banner, +# "event_extra": event.extra.json(), +# "has_promo_codes": has_promo_codes, +# }, +# ) + + +# @events_generic_router.get("/ticket/{ticket_id}", response_class=HTMLResponse) +# async def ticket(request: Request, ticket_id): +# ticket = await get_ticket(ticket_id) +# if not ticket: +# raise HTTPException( +# status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist." +# ) + +# event = await get_event(ticket.event) +# if not event: +# raise HTTPException( +# status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." +# ) + +# return events_renderer().TemplateResponse( +# "events/ticket.html", +# { +# "request": request, +# "ticket_id": ticket_id, +# "ticket_name": event.name, +# "ticket_info": event.info, +# }, +# ) + + +# @events_generic_router.get("/register/{event_id}", response_class=HTMLResponse) +# async def register(request: Request, event_id): +# event = await get_event(event_id) +# if not event: +# raise HTTPException( +# status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." +# ) + +# return events_renderer().TemplateResponse( +# "events/register.html", +# { +# "request": request, +# "event_id": event_id, +# "event_name": event.name, +# "wallet_id": event.wallet, +# }, +# ) From 919875ea3c9b5446c6edd72021740de49e4e0308 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 28 Jan 2026 11:28:00 +0000 Subject: [PATCH 07/11] final conversion --- static/js/display.js | 23 +- static/js/display.vue | 2 +- static/js/index.vue | 35 ++- static/js/ticket.js | 18 ++ static/js/ticket.vue | 4 +- static/routes.json | 2 +- templates/events/_api_docs.html | 25 -- templates/events/display.html | 116 -------- templates/events/error.html | 31 --- templates/events/index.html | 464 -------------------------------- templates/events/register.html | 84 ------ templates/events/ticket.html | 39 --- views.py | 7 - views_api.py | 63 ++++- 14 files changed, 136 insertions(+), 777 deletions(-) delete mode 100644 templates/events/_api_docs.html delete mode 100644 templates/events/display.html delete mode 100644 templates/events/error.html delete mode 100644 templates/events/index.html delete mode 100644 templates/events/register.html delete mode 100644 templates/events/ticket.html diff --git a/static/js/display.js b/static/js/display.js index 44e1520..fce2a4b 100644 --- a/static/js/display.js +++ b/static/js/display.js @@ -2,6 +2,7 @@ window.PageEventsDisplay = { template: '#page-events-display', data() { return { + eventName: '', paymentReq: null, redirectUrl: null, formDialog: { @@ -26,11 +27,8 @@ window.PageEventsDisplay = { } }, async created() { - this.info = event_info - this.info = this.info.substring(1, this.info.length - 1) - this.banner = event_banner - this.extra = event_extra - this.hasPromoCodes = has_promoCodes + this.eventId = this.$route.params.id + await this.getEvent() }, computed: { formatDescription() { @@ -38,6 +36,21 @@ window.PageEventsDisplay = { } }, methods: { + async getEvent() { + try { + const {data} = await LNbits.api.request( + 'GET', + `/events/api/v1/events/${this.eventId}` + ) + this.eventName = data.event_name + this.info = data.event_info + this.banner = data.event_banner + this.extra = data.event_extra + this.hasPromoCodes = data.has_promo_codes + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, resetForm(e) { e.preventDefault() this.formDialog.data.name = '' diff --git a/static/js/display.vue b/static/js/display.vue index 7143e1f..c69bb93 100644 --- a/static/js/display.vue +++ b/static/js/display.vue @@ -4,7 +4,7 @@ -

{{ event_name }}

+

{{ eventName }}



diff --git a/static/js/index.vue b/static/js/index.vue index 41b3767..c766bcf 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -222,7 +222,40 @@
- {% include "events/_api_docs.html" %} + + + + +
+ Events: Sell and register ticket waves for an event +
+

+ Events allows you to make a wave of tickets for an event, + each ticket is in the form of a unique QRcode, which the + user presents at registration. Events comes with a shareable + ticket scanner, which can be used to register attendees.
+ + Created by, + Ben Arc + +

+
+
+ +
+
diff --git a/static/js/ticket.js b/static/js/ticket.js index 3fc4121..3085f47 100644 --- a/static/js/ticket.js +++ b/static/js/ticket.js @@ -1,8 +1,26 @@ window.PageEventsTicket = { template: '#page-events-ticket', + data() { + return { + ticketId: null, + ticketName: null + } + }, methods: { printWindow() { window.print() } + }, + async created() { + this.ticketId = this.$route.params.id + try { + const {data} = await LNbits.api.request( + 'GET', + `/events/api/v1/tickets/${this.ticketId}` + ) + this.ticketName = data.ticket_name + } catch (error) { + LNbits.utils.notifyApiError(error) + } } } diff --git a/static/js/ticket.vue b/static/js/ticket.vue index 0a58da9..f4fe029 100644 --- a/static/js/ticket.vue +++ b/static/js/ticket.vue @@ -4,7 +4,7 @@
-

{{ ticket_name }} Ticket

+

{{ ticketName }} Ticket


Bookmark, print or screenshot this page,
@@ -12,7 +12,7 @@


diff --git a/static/routes.json b/static/routes.json index 4fccdec..7d04e9d 100644 --- a/static/routes.json +++ b/static/routes.json @@ -7,7 +7,7 @@ }, { "path": "/events/:id", - "name": "PageEventsPublic", + "name": "PageEventsDisplay", "template": "/events/static/js/display.vue", "component": "/events/static/js/display.js" }, diff --git a/templates/events/_api_docs.html b/templates/events/_api_docs.html deleted file mode 100644 index dbf0131..0000000 --- a/templates/events/_api_docs.html +++ /dev/null @@ -1,25 +0,0 @@ - - - -
- Events: Sell and register ticket waves for an event -
-

- Events allows you to make a wave of tickets for an event, each ticket is - in the form of a unique QRcode, which the user presents at registration. - Events comes with a shareable ticket scanner, which can be used to - register attendees.
- - Created by, - Ben Arc - -

-
-
- -
diff --git a/templates/events/display.html b/templates/events/display.html deleted file mode 100644 index 73d279d..0000000 --- a/templates/events/display.html +++ /dev/null @@ -1,116 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - - -

{{ event_name }}

-
-
-
-
-
- - -
Buy Ticket
- - - - - -
- Submit - Cancel -
-
-
-
- - -
- Link to your ticket! -

-

You'll be redirected in a few moments...

-
-
-
- - - - - -
- -
-
- Copy invoice - Close -
-
-
-
- -{% endblock %} {% block scripts %} - - -{% endblock %} diff --git a/templates/events/error.html b/templates/events/error.html deleted file mode 100644 index 3993db5..0000000 --- a/templates/events/error.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -
-

{{ event_name }} error

-
- - -
{{ event_error }}
-
-
-
-
-
-
-{% endblock %} {% block scripts %} - - - -{% endblock %} diff --git a/templates/events/index.html b/templates/events/index.html deleted file mode 100644 index 62752d1..0000000 --- a/templates/events/index.html +++ /dev/null @@ -1,464 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - New Event - - - - - -
-
-
Events
-
-
- Export to CSV -
-
- - - - -
-
- - - -
-
-
Tickets
-
-
- Export to CSV -
-
- - - - -
-
-
-
- - -
- {{SITE_TITLE}} Events extension -
-
- - - {% include "events/_api_docs.html" %} - -
-
- - - - -
-
- -
-
- - -
-
- - - -
-
Ticket closing date
-
- -
-
-
-
Event begins
-
- -
-
- -
-
Event ends
-
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
Conditional Events
-
- Make this event conditional if - minimum tickets are sold. User will be asked to - provide a Lightning Address or LNURL pay for refunds. -
-
- -
-
- -
-
- -
Promo Codes
-
- Allow users to enter a promo code for discounts. -
- -
- - - - - - -
-
- Add Promo Code -
-
- -
- Update Event - Create Event - Cancel -
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - - -{% endblock %} diff --git a/templates/events/register.html b/templates/events/register.html deleted file mode 100644 index 92589a3..0000000 --- a/templates/events/register.html +++ /dev/null @@ -1,84 +0,0 @@ -{% extends "public.html" %} {% block page %} - -
-
- - -
-

{{ event_name }} Registration

-
- -
- - Scan ticket -
-
-
- - - - - - - - - -
- - - -
- -
-
- Cancel -
-
-
-
-{% endblock %} {% block scripts %} - - -{% endblock %} diff --git a/templates/events/ticket.html b/templates/events/ticket.html deleted file mode 100644 index bcf7e82..0000000 --- a/templates/events/ticket.html +++ /dev/null @@ -1,39 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -
-

{{ ticket_name }} Ticket

-
-
- Bookmark, print or screenshot this page,
- and present it for registration! -
-
- -
- - Print -
-
-
-
-
-{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/views.py b/views.py index dba2e1b..4659cdc 100644 --- a/views.py +++ b/views.py @@ -12,13 +12,6 @@ events_generic_router = APIRouter() -""" -bitcoinswitch_generic_router.add_api_route( - "/", methods=["GET"], endpoint=index, dependencies=[Depends(check_user_exists)] -) - -""" - events_generic_router.add_api_route( "/", methods=["GET"], diff --git a/views_api.py b/views_api.py index 58c9bca..7166ee8 100644 --- a/views_api.py +++ b/views_api.py @@ -2,6 +2,8 @@ from http import HTTPStatus from fastapi import APIRouter, Depends, Query +from starlette.exceptions import HTTPException + from lnbits.core.crud import get_standalone_payment, get_user from lnbits.core.models import WalletTypeInfo from lnbits.core.services import create_invoice @@ -13,7 +15,6 @@ fiat_amount_as_satoshis, get_fiat_rate_satoshis, ) -from starlette.exceptions import HTTPException from .crud import ( create_event, @@ -26,6 +27,7 @@ get_events, get_ticket, get_tickets, + purge_unpaid_tickets, update_event, update_ticket, ) @@ -49,6 +51,46 @@ async def api_events( return [event.dict() for event in await get_events(wallet_ids)] +@events_api_router.get("/api/v1/events/{event_id}") +async def api_get_event(event_id: str) -> dict: + event = await get_event(event_id) + if not event: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." + ) + await purge_unpaid_tickets(event_id) + + is_window_open = datetime.now(timezone.utc) < datetime.strptime( + event.closing_date, "%Y-%m-%d" + ).replace(tzinfo=timezone.utc) + is_min_tickets_met = ( + event.sold >= event.extra.min_tickets if event.extra.conditional else True + ) + if event.amount_tickets < 1: + raise HTTPException(status_code=HTTPStatus.GONE, detail="Event is sold out.") + if event.extra.conditional and not is_min_tickets_met and not is_window_open: + event.canceled = True + await update_event(event) + await refund_tickets(event_id) + + raise HTTPException(status_code=HTTPStatus.GONE, detail="Event canceled.") + + if not is_window_open: + raise HTTPException( + status_code=HTTPStatus.GONE, detail="Ticket closing date has passed." + ) + + return { + "event_id": event_id, + "event_name": event.name, + "event_info": event.info, + "event_price": event.price_per_ticket, + "event_banner": event.banner, + "event_extra": event.extra.json(), + "has_promo_codes": len(event.extra.promo_codes) > 0, + } + + @events_api_router.post("/api/v1/events") @events_api_router.put("/api/v1/events/{event_id}") async def api_event_create( @@ -131,6 +173,25 @@ async def api_tickets( return await get_tickets(wallet_ids) +@events_api_router.get("/api/v1/tickets/{ticket_id}") +async def api_get_ticket(ticket_id: str) -> dict: + ticket = await get_ticket(ticket_id) + if not ticket: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist." + ) + event = await get_event(ticket.event) + if not event: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." + ) + return { + "ticket_id": ticket.id, + "ticket_name": event.name, + "ticket_info": event.info, + } + + @events_api_router.post("/api/v1/tickets/{event_id}") async def api_ticket_create(event_id: str, data: CreateTicket): name = data.name From d474d9274e96688b4e365f627cbd9e10d8a1355f Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 28 Jan 2026 11:36:38 +0000 Subject: [PATCH 08/11] chore: cleanup and lint --- static/routes.json | 50 ++++++++-------- views.py | 139 +-------------------------------------------- views_api.py | 3 +- 3 files changed, 29 insertions(+), 163 deletions(-) diff --git a/static/routes.json b/static/routes.json index 7d04e9d..ae46a9a 100644 --- a/static/routes.json +++ b/static/routes.json @@ -1,26 +1,26 @@ [ - { - "path": "/events/", - "name": "PageEvents", - "template": "/events/static/js/index.vue", - "component": "/events/static/js/index.js" - }, - { - "path": "/events/:id", - "name": "PageEventsDisplay", - "template": "/events/static/js/display.vue", - "component": "/events/static/js/display.js" - }, - { - "path": "/events/ticket/:id", - "name": "PageEventsTicket", - "template": "/events/static/js/ticket.vue", - "component": "/events/static/js/ticket.js" - }, - { - "path": "/events/register/:id", - "name": "PageEventsRegister", - "template": "/events/static/js/register.vue", - "component": "/events/static/js/register.js" - } -] \ No newline at end of file + { + "path": "/events/", + "name": "PageEvents", + "template": "/events/static/js/index.vue", + "component": "/events/static/js/index.js" + }, + { + "path": "/events/:id", + "name": "PageEventsDisplay", + "template": "/events/static/js/display.vue", + "component": "/events/static/js/display.js" + }, + { + "path": "/events/ticket/:id", + "name": "PageEventsTicket", + "template": "/events/static/js/ticket.vue", + "component": "/events/static/js/ticket.js" + }, + { + "path": "/events/register/:id", + "name": "PageEventsRegister", + "template": "/events/static/js/register.vue", + "component": "/events/static/js/register.js" + } +] diff --git a/views.py b/views.py index 4659cdc..39ac273 100644 --- a/views.py +++ b/views.py @@ -1,14 +1,6 @@ -from fastapi import APIRouter, Depends, Request -from starlette.exceptions import HTTPException -from starlette.responses import HTMLResponse - -from lnbits.core.models import User -from lnbits.core.views.generic import index, index_public -from lnbits.decorators import check_account_id_exists, check_user_exists -from lnbits.helpers import template_renderer - -from .crud import get_event, get_ticket, purge_unpaid_tickets, update_event -from .services import refund_tickets +from fastapi import APIRouter, Depends +from lnbits.core.views.generic import index, index_public # type: ignore +from lnbits.decorators import check_account_id_exists events_generic_router = APIRouter() @@ -30,128 +22,3 @@ events_generic_router.add_api_route( "/register/{event_id}", methods=["GET"], endpoint=index_public ) - - -# def events_renderer(): -# return template_renderer(["events/templates"]) - - -# @events_generic_router.get("/", response_class=HTMLResponse) -# async def index(request: Request, user: User = Depends(check_user_exists)): -# return events_renderer().TemplateResponse( -# "events/index.html", {"request": request, "user": user.json()} -# ) - - -# @events_generic_router.get("/{event_id}", response_class=HTMLResponse) -# async def display(request: Request, event_id): -# event = await get_event(event_id) -# if not event: -# raise HTTPException( -# status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." -# ) - -# await purge_unpaid_tickets(event_id) - -# is_window_open = ( -# date.today() < datetime.strptime(event.closing_date, "%Y-%m-%d").date() -# ) -# is_min_tickets_met = ( -# event.sold >= event.extra.min_tickets if event.extra.conditional else True -# ) - -# if event.amount_tickets < 1: -# return events_renderer().TemplateResponse( -# "events/error.html", -# { -# "request": request, -# "event_name": event.name, -# "event_error": "Sorry, tickets are sold out :(", -# }, -# ) -# if event.extra.conditional and not is_min_tickets_met and not is_window_open: -# event.canceled = True -# await update_event(event) -# await refund_tickets(event_id) - -# return events_renderer().TemplateResponse( -# "events/error.html", -# { -# "request": request, -# "event_name": event.name, -# "event_error": "Sorry, event was cancelled.", -# }, -# ) -# if not is_window_open: -# return events_renderer().TemplateResponse( -# "events/error.html", -# { -# "request": request, -# "event_name": event.name, -# "event_error": "Sorry, ticket closing date has passed :(", -# }, -# ) - -# if len(event.extra.promo_codes) > 0: -# has_promo_codes = True -# else: -# has_promo_codes = False - -# event.extra.promo_codes = [] -# return events_renderer().TemplateResponse( -# "events/display.html", -# { -# "request": request, -# "event_id": event_id, -# "event_name": event.name, -# "event_info": event.info, -# "event_price": event.price_per_ticket, -# "event_banner": event.banner, -# "event_extra": event.extra.json(), -# "has_promo_codes": has_promo_codes, -# }, -# ) - - -# @events_generic_router.get("/ticket/{ticket_id}", response_class=HTMLResponse) -# async def ticket(request: Request, ticket_id): -# ticket = await get_ticket(ticket_id) -# if not ticket: -# raise HTTPException( -# status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist." -# ) - -# event = await get_event(ticket.event) -# if not event: -# raise HTTPException( -# status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." -# ) - -# return events_renderer().TemplateResponse( -# "events/ticket.html", -# { -# "request": request, -# "ticket_id": ticket_id, -# "ticket_name": event.name, -# "ticket_info": event.info, -# }, -# ) - - -# @events_generic_router.get("/register/{event_id}", response_class=HTMLResponse) -# async def register(request: Request, event_id): -# event = await get_event(event_id) -# if not event: -# raise HTTPException( -# status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." -# ) - -# return events_renderer().TemplateResponse( -# "events/register.html", -# { -# "request": request, -# "event_id": event_id, -# "event_name": event.name, -# "wallet_id": event.wallet, -# }, -# ) diff --git a/views_api.py b/views_api.py index 7166ee8..03f03fa 100644 --- a/views_api.py +++ b/views_api.py @@ -2,8 +2,6 @@ from http import HTTPStatus from fastapi import APIRouter, Depends, Query -from starlette.exceptions import HTTPException - from lnbits.core.crud import get_standalone_payment, get_user from lnbits.core.models import WalletTypeInfo from lnbits.core.services import create_invoice @@ -15,6 +13,7 @@ fiat_amount_as_satoshis, get_fiat_rate_satoshis, ) +from starlette.exceptions import HTTPException from .crud import ( create_event, From fd293150c6e7e783f37bccf5a8cf0546e2b15f5e Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Wed, 28 Jan 2026 17:03:06 +0000 Subject: [PATCH 09/11] final refactor --- static/js/display.js | 120 ++++++++++++++++++++++------------------- static/js/display.vue | 9 ++-- static/js/index.js | 2 +- static/js/index.vue | 5 +- static/js/register.js | 2 - static/js/register.vue | 2 +- static/js/ticket.vue | 2 +- 7 files changed, 77 insertions(+), 65 deletions(-) diff --git a/static/js/display.js b/static/js/display.js index fce2a4b..6b3bea2 100644 --- a/static/js/display.js +++ b/static/js/display.js @@ -75,68 +75,78 @@ window.PageEventsDisplay = { const regex = /^[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}$/ return regex.test(val) || 'Please enter valid email.' }, - Invoice() { - axios - .post(`/events/api/v1/tickets/${event_id}`, { - name: this.formDialog.data.name, - email: this.formDialog.data.email, - promo_code: this.formDialog.data.promo_code || null - }) - .then(response => { - this.paymentReq = response.data.payment_request - this.paymentCheck = response.data.payment_hash + 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, + refund: this.formDialog.data.refund || null + } + ) + this.paymentReq = data.payment_request + this.paymentCheck = data.payment_hash - dismissMsg = Quasar.Notify.create({ - timeout: 0, - message: 'Waiting for payment...' - }) + dismissMsg = Quasar.Notify.create({ + timeout: 0, + message: 'Waiting for payment...' + }) - this.receive = { - show: true, - status: 'pending', - paymentReq: this.paymentReq - } - paymentChecker = setInterval(() => { - axios - .post(`/events/api/v1/tickets/${event_id}/${this.paymentCheck}`, { - event: event_id, - event_name: event_name, + this.receive = { + show: true, + status: 'pending', + paymentReq: this.paymentReq + } + //TODO: use websockets + paymentChecker = setInterval(async () => { + try { + const res = await LNbits.api.request( + 'POST', + `/events/api/v1/tickets/${this.eventId}/${this.paymentCheck}`, + { + event: this.eventId, + event_name: this.eventName, name: this.formDialog.data.name, email: this.formDialog.data.email - }) - .then(res => { - if (res.data.paid) { - clearInterval(paymentChecker) - dismissMsg() - this.formDialog.data.name = '' - this.formDialog.data.email = '' + } + ) + if (res.data.paid) { + clearInterval(paymentChecker) + dismissMsg() + this.formDialog.data.name = '' + this.formDialog.data.email = '' - Quasar.Notify.create({ - type: 'positive', - message: 'Sent, thank you!', - icon: null - }) - this.receive = { - show: false, - status: 'complete', - paymentReq: null - } + Quasar.Notify.create({ + type: 'positive', + message: 'Sent, thank you!', + icon: null + }) + this.receive = { + show: false, + status: 'complete', + paymentReq: null + } - this.ticketLink = { - show: true, - data: { - link: `/events/ticket/${res.data.ticket_id}` - } - } - setTimeout(() => { - window.location.href = `/events/ticket/${res.data.ticket_id}` - }, 5000) + this.ticketLink = { + show: true, + data: { + link: `/events/ticket/${res.data.ticket_id}` } - }) - .catch(LNbits.utils.notifyApiError) - }, 2000) - }) - .catch(LNbits.utils.notifyApiError) + } + setTimeout(() => { + window.location.href = `/events/ticket/${res.data.ticket_id}` + }, 5000) + } + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, 2000) + } catch (error) { + LNbits.utils.notifyApiError(error) + } } } } diff --git a/static/js/display.vue b/static/js/display.vue index c69bb93..253fefe 100644 --- a/static/js/display.vue +++ b/static/js/display.vue @@ -4,7 +4,7 @@ -

{{ eventName }}

+



@@ -13,7 +13,7 @@
Buy Ticket
- +
- Copy invoice Close diff --git a/static/js/index.js b/static/js/index.js index aeb01d6..1136fba 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -289,7 +289,7 @@ window.PageEvents = { if (this.g.user.wallets.length) { this.getTickets() this.getEvents() - this.currencies = await LNbits.api.getCurrencies() + this.currencies = ['sats', ...g.allowedCurrencies] } } } diff --git a/static/js/index.vue b/static/js/index.vue index c766bcf..5e8cc4c 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -216,8 +216,9 @@
-
- {{ SITE_TITLE }} Events extension +
+ + Events extension
diff --git a/static/js/register.js b/static/js/register.js index bc1e269..32863ab 100644 --- a/static/js/register.js +++ b/static/js/register.js @@ -1,5 +1,3 @@ -const {template} = require('underscore') - const mapEvents = function (obj) { obj.date = Quasar.date.formatDate( new Date(obj.time * 1000), diff --git a/static/js/register.vue b/static/js/register.vue index bcaacc8..34fa9a8 100644 --- a/static/js/register.vue +++ b/static/js/register.vue @@ -4,7 +4,7 @@
-

{{ event_name }} Registration

+

Registration



diff --git a/static/js/ticket.vue b/static/js/ticket.vue index f4fe029..7fdecce 100644 --- a/static/js/ticket.vue +++ b/static/js/ticket.vue @@ -4,7 +4,7 @@
-

{{ ticketName }} Ticket

+

Ticket


Bookmark, print or screenshot this page,
From 1502fdf57248afa9d71757b6299ef6064317c1e8 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 29 Jan 2026 09:07:49 +0000 Subject: [PATCH 10/11] bump min version --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index fb33ebe..acfb7d7 100644 --- a/config.json +++ b/config.json @@ -6,7 +6,7 @@ "short_description": "Sell and register event tickets", "description": "", "tile": "/events/static/image/events.png", - "min_lnbits_version": "1.4.0", + "min_lnbits_version": "1.4.1", "contributors": [ { "name": "talvasconcelos", From 193deaf680b9fdab88fbae768e2b60fb16066d38 Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Thu, 29 Jan 2026 09:11:10 +0000 Subject: [PATCH 11/11] fallback to currencies --- static/js/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/js/index.js b/static/js/index.js index 1136fba..ccfa2ba 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -289,7 +289,11 @@ window.PageEvents = { if (this.g.user.wallets.length) { this.getTickets() this.getEvents() - this.currencies = ['sats', ...g.allowedCurrencies] + if (g.allowedCurrencies && g.allowedCurrencies.length) { + this.currencies = ['sats', ...g.allowedCurrencies] + } else { + this.currencies = ['sats', ...g.currencies] + } } } }