From 817b58dd0df3929da498ea379e5b0134dd9e17df Mon Sep 17 00:00:00 2001 From: Patrick Mulligan <43773168+PatMulligan@users.noreply.github.com> Date: Tue, 5 May 2026 20:08:15 +0200 Subject: [PATCH 1/5] chore: make m006 idempotent Earlier downstream forks added some of these columns under different migration names. A duplicate-column-tolerant ALTER ADD COLUMN keeps the migration log monotonic for both fresh installs and forks that upgrade in-place. Co-Authored-By: Claude Opus 4.7 (1M context) --- migrations.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/migrations.py b/migrations.py index 6f8e838..c055617 100644 --- a/migrations.py +++ b/migrations.py @@ -162,16 +162,30 @@ async def m005_add_image_banner(db): await db.execute("ALTER TABLE events.events ADD COLUMN banner TEXT;") +async def _alter_add_column_safe(db, sql: str) -> None: + """ALTER TABLE ADD COLUMN that swallows duplicate-column errors. + + Earlier aiolabs/events forks added some of these columns under different + migration names (e.g. our former m007). Skipping the error keeps the + migration log monotonic for both fresh installs and pre-rebase upgrades. + """ + try: + await db.execute(sql) + except Exception as exc: + msg = str(exc).lower() + if "duplicate column" in msg or "already exists" in msg: + return + raise + + async def m006_add_extra_fields(db): """ Add a canceled and 'extra' column to events and ticket tables to support promo codes and ticket metadata. """ - # Add canceled and 'extra' columns to events table - await db.execute( - "ALTER TABLE events.events ADD COLUMN canceled BOOLEAN NOT NULL DEFAULT FALSE;" + await _alter_add_column_safe( + db, + "ALTER TABLE events.events ADD COLUMN canceled BOOLEAN NOT NULL DEFAULT FALSE", ) - await db.execute("ALTER TABLE events.events ADD COLUMN extra TEXT;") - - # Add 'extra' column to ticket table - await db.execute("ALTER TABLE events.ticket ADD COLUMN extra TEXT;") + await _alter_add_column_safe(db, "ALTER TABLE events.events ADD COLUMN extra TEXT") + await _alter_add_column_safe(db, "ALTER TABLE events.ticket ADD COLUMN extra TEXT") From 36f0521b882cc3671c6c651bc6a3db4ceef88507 Mon Sep 17 00:00:00 2001 From: Patrick Mulligan <43773168+PatMulligan@users.noreply.github.com> Date: Tue, 5 May 2026 20:08:15 +0200 Subject: [PATCH 2/5] feat: support optional user_id ticket identifier Add an alternative ticket identifier scheme: instead of (name, email), external integrations can issue tickets bound to an LNbits user_id. - m007 adds the user_id column on events.ticket - CreateTicket validator enforces exactly one identifier scheme per ticket - Ticket / PublicTicket: name, email, user_id all Optional - _parse_ticket_row reverses the empty-string sentinel used to keep the NOT NULL name/email columns satisfied when user_id is the identifier - POST /tickets/{event_id} dispatches to _create_user_id_ticket vs _create_named_ticket based on the supplied identifier - New GET /tickets/user/{user_id} returns tickets for a given user Co-Authored-By: Claude Opus 4.7 (1M context) --- crud.py | 93 ++++++++++++++++++++++++++++++++++++++++++++------- migrations.py | 11 ++++++ models.py | 27 +++++++++++---- views_api.py | 57 ++++++++++++++++++++++++++++--- 4 files changed, 165 insertions(+), 23 deletions(-) diff --git a/crud.py b/crud.py index 3046f0e..450c091 100644 --- a/crud.py +++ b/crud.py @@ -1,3 +1,4 @@ +import json from datetime import datetime, timedelta, timezone from lnbits.db import Database @@ -8,47 +9,115 @@ db = Database("ext_events") +def _parse_ticket_row(row) -> dict: + """Normalize a ticket row before constructing a Ticket model. + + - Empty-string sentinels in name/email (used because the DB columns are + NOT NULL but the Pydantic field is Optional when user_id is set) are + converted back to None. + - The `extra` JSON column may come back as a string when the row is + fetched without a model= argument; parse it so Pydantic can build + TicketExtra from a dict. + """ + ticket_data = dict(row) + + if ticket_data.get("name") == "": + ticket_data["name"] = None + if ticket_data.get("email") == "": + ticket_data["email"] = None + + extra = ticket_data.get("extra") + if isinstance(extra, str): + ticket_data["extra"] = json.loads(extra) if extra else {} + + return ticket_data + + async def create_ticket( - payment_hash: str, wallet: str, event: str, name: str, email: str, extra: dict + payment_hash: str, + wallet: str, + event: str, + name: str | None = None, + email: str | None = None, + user_id: str | None = None, + extra: dict | None = None, ) -> Ticket: now = datetime.now(timezone.utc) - ticket = Ticket( + + # name/email columns are NOT NULL in the schema, so we store "" when only + # user_id is supplied. _parse_ticket_row reverses this on read. + if user_id: + db_name = "" + db_email = "" + else: + db_name = name or "" + db_email = email or "" + + db_ticket = Ticket( + id=payment_hash, + wallet=wallet, + event=event, + name=db_name, + email=db_email, + user_id=user_id, + registered=False, + paid=False, + reg_timestamp=now, + time=now, + extra=TicketExtra(**extra) if extra else TicketExtra(), + ) + await db.insert("events.ticket", db_ticket) + + return Ticket( id=payment_hash, wallet=wallet, event=event, name=name, email=email, + user_id=user_id, registered=False, paid=False, reg_timestamp=now, time=now, extra=TicketExtra(**extra) if extra else TicketExtra(), ) - await db.insert("events.ticket", ticket) - return ticket async def update_ticket(ticket: Ticket) -> Ticket: - await db.update("events.ticket", ticket) + ticket_dict = ticket.dict() + if ticket_dict.get("name") is None: + ticket_dict["name"] = "" + if ticket_dict.get("email") is None: + ticket_dict["email"] = "" + await db.update("events.ticket", Ticket(**ticket_dict)) return ticket async def get_ticket(payment_hash: str) -> Ticket | None: - return await db.fetchone( + row = await db.fetchone( "SELECT * FROM events.ticket WHERE id = :id", {"id": payment_hash}, - Ticket, ) + if not row: + return None + return Ticket(**_parse_ticket_row(row)) async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]: if isinstance(wallet_ids, str): wallet_ids = [wallet_ids] q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids]) - return await db.fetchall( - f"SELECT * FROM events.ticket WHERE wallet IN ({q})", - model=Ticket, + rows = await db.fetchall(f"SELECT * FROM events.ticket WHERE wallet IN ({q})") + return [Ticket(**_parse_ticket_row(row)) for row in rows] + + +async def get_tickets_by_user_id(user_id: str) -> list[Ticket]: + """All tickets owned by the given LNbits user_id.""" + rows = await db.fetchall( + "SELECT * FROM events.ticket WHERE user_id = :user_id ORDER BY time DESC", + {"user_id": user_id}, ) + return [Ticket(**_parse_ticket_row(row)) for row in rows] async def delete_ticket(payment_hash: str) -> None: @@ -107,8 +176,8 @@ async def delete_event(event_id: str) -> None: async def get_event_tickets(event_id: str) -> list[Ticket]: - return await db.fetchall( + rows = await db.fetchall( "SELECT * FROM events.ticket WHERE event = :event", {"event": event_id}, - Ticket, ) + return [Ticket(**_parse_ticket_row(row)) for row in rows] diff --git a/migrations.py b/migrations.py index c055617..3664d69 100644 --- a/migrations.py +++ b/migrations.py @@ -189,3 +189,14 @@ async def m006_add_extra_fields(db): ) await _alter_add_column_safe(db, "ALTER TABLE events.events ADD COLUMN extra TEXT") await _alter_add_column_safe(db, "ALTER TABLE events.ticket ADD COLUMN extra TEXT") + + +async def m007_add_user_id_support(db): + """ + Add user_id column to ticket table so a ticket can reference an LNbits + user id instead of (name, email). Application logic enforces that exactly + one identifier scheme is used per ticket. + """ + await _alter_add_column_safe( + db, "ALTER TABLE events.ticket ADD COLUMN user_id TEXT" + ) diff --git a/models.py b/models.py index 14547d1..415a2e7 100644 --- a/models.py +++ b/models.py @@ -1,7 +1,7 @@ from datetime import datetime from fastapi import Query -from pydantic import BaseModel, EmailStr, Field, validator +from pydantic import BaseModel, EmailStr, Field, root_validator, validator class PromoCode(BaseModel): @@ -77,18 +77,33 @@ class TicketExtra(BaseModel): class CreateTicket(BaseModel): - name: str - email: EmailStr + name: str | None = None + email: EmailStr | None = None + user_id: str | None = None # LNbits user id (alternative to name+email) promo_code: str | None = None refund_address: str | None = None + @root_validator + def validate_identifiers(cls, values): + name = values.get("name") + email = values.get("email") + user_id = values.get("user_id") + if not user_id and not (name and email): + raise ValueError( + "Either user_id or both name and email must be provided" + ) + if user_id and (name or email): + raise ValueError("Cannot provide both user_id and name/email") + return values + class Ticket(BaseModel): id: str wallet: str event: str - name: str - email: str + name: str | None = None + email: str | None = None + user_id: str | None = None registered: bool paid: bool time: datetime @@ -98,7 +113,7 @@ class Ticket(BaseModel): class PublicTicket(BaseModel): event: str - name: str + name: str | None = None registered: bool paid: bool time: datetime diff --git a/views_api.py b/views_api.py index 73cecd3..d1d8631 100644 --- a/views_api.py +++ b/views_api.py @@ -33,6 +33,7 @@ get_events, get_ticket, get_tickets, + get_tickets_by_user_id, purge_unpaid_tickets, update_event, update_ticket, @@ -177,6 +178,16 @@ async def api_tickets( return await get_tickets(wallet_ids) +@tickets_api_router.get("/user/{user_id}") +async def api_tickets_by_user_id(user_id: str) -> list[Ticket]: + """Tickets bound to an LNbits user_id (used by external integrations). + + Declared before /{ticket_id} so FastAPI matches the literal `/user/` + prefix instead of treating "user" as a ticket id. + """ + return await get_tickets_by_user_id(user_id) + + @tickets_api_router.get("/{ticket_id}", response_model=PublicTicket) async def api_get_ticket(ticket_id: str) -> Ticket: ticket = await get_ticket(ticket_id) @@ -193,7 +204,9 @@ async def api_get_ticket(ticket_id: str) -> Ticket: @tickets_api_router.post("/{event_id}") -async def api_ticket_create(event_id: str, data: CreateTicket) -> TicketPaymentRequest: +async def api_ticket_create( + event_id: str, data: CreateTicket +) -> TicketPaymentRequest: event = await get_event(event_id) if not event: raise HTTPException( @@ -206,6 +219,14 @@ async def api_ticket_create(event_id: str, data: CreateTicket) -> TicketPaymentR if event.amount_tickets > 0 and event.sold >= event.amount_tickets: raise HTTPException(status_code=HTTPStatus.GONE, detail="Event is sold out.") + if data.user_id: + return await _create_user_id_ticket(event, data.user_id) + return await _create_named_ticket(event, data) + + +async def _create_named_ticket( + event: Event, data: CreateTicket +) -> TicketPaymentRequest: name = data.name email = data.email promo_code = data.promo_code.upper() if data.promo_code else None @@ -214,12 +235,10 @@ async def api_ticket_create(event_id: str, data: CreateTicket) -> TicketPaymentR extra: dict[str, Any] = {"tag": "events", "name": name, "email": email} if promo_code: - # check if promo_code exists in event.extra.promo_codes if promo_code not in [pc.code for pc in event.extra.promo_codes]: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail="Invalid promo code." ) - # get the promocode promo = next(pc for pc in event.extra.promo_codes if pc.code == promo_code) extra["promo_code"] = promo.code price = event.price_per_ticket * (1 - promo.discount_percent / 100) @@ -229,13 +248,12 @@ async def api_ticket_create(event_id: str, data: CreateTicket) -> TicketPaymentR extra["currency"] = event.currency extra["fiatAmount"] = price extra["rate"] = await get_fiat_rate_satoshis(event.currency) - price = await fiat_amount_as_satoshis(price, event.currency) payment = await create_invoice( wallet_id=event.wallet, amount=price, - memo=f"{event_id}", + memo=f"{event.id}", extra=extra, ) await create_ticket( @@ -250,7 +268,36 @@ async def api_ticket_create(event_id: str, data: CreateTicket) -> TicketPaymentR "sats_paid": int(price), }, ) + return TicketPaymentRequest( + payment_hash=payment.payment_hash, payment_request=payment.bolt11 + ) + +async def _create_user_id_ticket( + event: Event, user_id: str +) -> TicketPaymentRequest: + price = event.price_per_ticket + extra: dict[str, Any] = {"tag": "events", "user_id": user_id} + + if event.currency != "sats": + price = await fiat_amount_as_satoshis(event.price_per_ticket, event.currency) + extra["fiat"] = True + extra["currency"] = event.currency + extra["fiatAmount"] = event.price_per_ticket + extra["rate"] = await get_fiat_rate_satoshis(event.currency) + + payment = await create_invoice( + wallet_id=event.wallet, + amount=price, + memo=f"{event.id}", + extra=extra, + ) + await create_ticket( + payment_hash=payment.payment_hash, + wallet=event.wallet, + event=event.id, + user_id=user_id, + ) return TicketPaymentRequest( payment_hash=payment.payment_hash, payment_request=payment.bolt11 ) From 14f040d5479093f74f1287d5d0cd03ff865607c6 Mon Sep 17 00:00:00 2001 From: Patrick Mulligan <43773168+PatMulligan@users.noreply.github.com> Date: Tue, 5 May 2026 20:08:15 +0200 Subject: [PATCH 3/5] feat: add event approval workflow with admin UI Non-admin event submissions now land in a "proposed" queue that LNbits admins review before the event becomes ticketable and publicly listed. - m008 adds events.events.status (proposed/approved/rejected); m010 seeds an events.settings singleton row with the auto_approve toggle. - Models: Event/CreateEvent.status, EventsSettings, optional date fields with sensible defaults (closing_date defaults to event_end_date which defaults to event_start_date), PublicEvent.status surfaces the workflow state on the public endpoint. - crud: get_all/public/pending_events for the admin views; get/update_settings for the auto_approve toggle; create_event auto-fills missing date defaults. - views_api: * POST /api/v1/events accepts wallet invoice keys so anyone can submit; handler stamps status="proposed" for non-admins when auto_approve is off * /public, /all, /pending, /settings (GET+PUT), /{id}/{approve,reject}, /{id}/tickets endpoints; literal-prefix routes declared before /{event_id} so FastAPI matches them correctly * Public GET /{event_id} bypasses sold-out / closing-window gates for proposed/rejected events and returns the trimmed PublicEvent so the SFC can render a "pending approval" banner * POST /tickets/{event_id} rejects when event.status != "approved" - Frontend: index.vue gains an admin Settings card, Pending Approvals list, status badge column and approve/reject row actions, plus an All Users' Events admin table; index.js gains the data + methods + an isAdmin probe via GET /events/all; display.vue shows pending/rejected banners and hides the Buy Ticket form unless status === "approved". Co-Authored-By: Claude Opus 4.7 (1M context) --- crud.py | 51 +++++++++++- migrations.py | 32 +++++++ models.py | 42 ++++++---- static/js/display.vue | 27 +++++- static/js/index.js | 89 +++++++++++++++++++- static/js/index.vue | 148 ++++++++++++++++++++++++++++++++- views_api.py | 188 +++++++++++++++++++++++++++++++++++------- 7 files changed, 526 insertions(+), 51 deletions(-) diff --git a/crud.py b/crud.py index 450c091..004fa7f 100644 --- a/crud.py +++ b/crud.py @@ -4,7 +4,7 @@ from lnbits.db import Database from lnbits.helpers import urlsafe_short_hash -from .models import CreateEvent, Event, Ticket, TicketExtra +from .models import CreateEvent, Event, EventsSettings, Ticket, TicketExtra db = Database("ext_events") @@ -143,6 +143,11 @@ async def purge_unpaid_tickets(event_id: str) -> None: async def create_event(data: CreateEvent) -> Event: event_id = urlsafe_short_hash() + # Default end_date to start_date and closing_date to end_date when omitted. + if not data.event_end_date: + data.event_end_date = data.event_start_date + if not data.closing_date: + data.closing_date = data.event_end_date event = Event(id=event_id, time=datetime.now(timezone.utc), **data.dict()) await db.insert("events.events", event) return event @@ -171,6 +176,50 @@ async def get_events(wallet_ids: str | list[str]) -> list[Event]: ) +async def get_all_events() -> list[Event]: + """All events, no wallet filter. Admin-only callers.""" + return await db.fetchall( + "SELECT * FROM events.events ORDER BY time DESC", + model=Event, + ) + + +async def get_public_events() -> list[Event]: + """Approved, non-canceled events for the public listing.""" + return await db.fetchall( + """ + SELECT * FROM events.events + WHERE status = 'approved' AND canceled = FALSE + ORDER BY event_start_date ASC + """, + model=Event, + ) + + +async def get_pending_events() -> list[Event]: + """Proposed events awaiting admin approval.""" + return await db.fetchall( + "SELECT * FROM events.events WHERE status = 'proposed' ORDER BY time DESC", + model=Event, + ) + + +async def get_settings() -> EventsSettings: + """Singleton settings row, seeded by m010.""" + row = await db.fetchone("SELECT * FROM events.settings WHERE id = 1") + if row: + return EventsSettings(**dict(row)) + return EventsSettings() + + +async def update_settings(settings: EventsSettings) -> EventsSettings: + await db.execute( + "UPDATE events.settings SET auto_approve = :auto_approve WHERE id = 1", + {"auto_approve": settings.auto_approve}, + ) + return settings + + async def delete_event(event_id: str) -> None: await db.execute("DELETE FROM events.events WHERE id = :id", {"id": event_id}) diff --git a/migrations.py b/migrations.py index 3664d69..0c50f19 100644 --- a/migrations.py +++ b/migrations.py @@ -200,3 +200,35 @@ async def m007_add_user_id_support(db): await _alter_add_column_safe( db, "ALTER TABLE events.ticket ADD COLUMN user_id TEXT" ) + + +async def m008_add_event_status(db): + """ + Add status column to events table for the proposal/approval workflow. + Values: 'proposed', 'approved', 'rejected'. Existing rows default to + 'approved' so they stay visible after upgrade. + """ + await _alter_add_column_safe( + db, + "ALTER TABLE events.events ADD COLUMN status TEXT NOT NULL DEFAULT 'approved'", + ) + + +async def m010_add_events_settings(db): + """ + Create the extension settings singleton row used by the admin UI to + toggle e.g. auto_approve. + """ + await db.execute( + """ + CREATE TABLE IF NOT EXISTS events.settings ( + id INTEGER PRIMARY KEY DEFAULT 1, + auto_approve BOOLEAN NOT NULL DEFAULT FALSE + ) + """ + ) + await db.execute( + "INSERT INTO events.settings (id, auto_approve) " + "SELECT 1, FALSE WHERE NOT EXISTS " + "(SELECT 1 FROM events.settings WHERE id = 1)" + ) diff --git a/models.py b/models.py index 415a2e7..d6ae9c4 100644 --- a/models.py +++ b/models.py @@ -1,6 +1,5 @@ from datetime import datetime -from fastapi import Query from pydantic import BaseModel, EmailStr, Field, root_validator, validator @@ -27,46 +26,55 @@ class EventExtra(BaseModel): class CreateEvent(BaseModel): - wallet: str - name: str - info: str - closing_date: str - event_start_date: str - event_end_date: str + wallet: str | None = None # filled from caller's wallet if absent + name: str # title (required) + info: str = "" # description (optional) + closing_date: str | None = None # defaults to event_end_date + event_start_date: str # required + event_end_date: str | None = None # defaults to event_start_date currency: str = "sat" - amount_tickets: int = Query(..., ge=0) - price_per_ticket: float = Query(..., ge=0) + amount_tickets: int = 0 # 0 = unlimited / not ticketed + price_per_ticket: float = 0 # 0 = free banner: str | None = None extra: EventExtra = Field(default_factory=EventExtra) + status: str = "approved" # proposed, approved, rejected class Event(BaseModel): id: str wallet: str name: str - info: str - closing_date: str + info: str = "" + closing_date: str | None = None canceled: bool = False event_start_date: str - event_end_date: str - currency: str - amount_tickets: int - price_per_ticket: float + event_end_date: str | None = None + currency: str = "sat" + amount_tickets: int = 0 + price_per_ticket: float = 0 time: datetime sold: int = 0 banner: str | None = None extra: EventExtra = Field(default_factory=EventExtra) + status: str = "approved" class PublicEvent(BaseModel): id: str name: str info: str - closing_date: str + closing_date: str | None = None canceled: bool event_start_date: str - event_end_date: str + event_end_date: str | None = None banner: str | None + status: str = "approved" # surfaces "proposed"/"rejected" so SFC can render banner + + +class EventsSettings(BaseModel): + """Extension-level settings for the events extension.""" + + auto_approve: bool = False # Skip approval workflow for non-admin users class TicketExtra(BaseModel): diff --git a/static/js/display.vue b/static/js/display.vue index 3f80180..58fec04 100644 --- a/static/js/display.vue +++ b/static/js/display.vue @@ -12,7 +12,32 @@
- + + + + Pending approval — this + event is awaiting an admin review and is not yet open for tickets. + + + + + Not approved — this event + was reviewed and is not being published. + + +
Buy Ticket
diff --git a/static/js/index.js b/static/js/index.js index ca34383..b4e8afa 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -5,6 +5,12 @@ window.PageEvents = { events: [], tickets: [], currencies: [], + pendingEvents: [], + allUserEvents: [], + isAdmin: false, + settings: { + auto_approve: false + }, eventsTable: { columns: [ {name: 'id', align: 'left', label: 'ID', field: 'id'}, @@ -65,7 +71,8 @@ window.PageEvents = { field: 'sold' }, {name: 'info', align: 'left', label: 'Info', field: 'info'}, - {name: 'banner', align: 'left', label: 'Banner', field: 'banner'} + {name: 'banner', align: 'left', label: 'Banner', field: 'banner'}, + {name: 'status', align: 'left', label: 'Status', field: 'status'} ], pagination: { rowsPerPage: 10 @@ -152,6 +159,84 @@ window.PageEvents = { this.events = response.data this.checkCanceledEvents() }) + + // Admin probe: a 200 from /all means we're an LNbits admin. + LNbits.api + .request('GET', '/events/api/v1/events/all') + .then(response => { + this.isAdmin = true + const ownWalletIds = this.g.user.wallets.map(w => w.id) + this.allUserEvents = response.data.filter( + e => !ownWalletIds.includes(e.wallet) + ) + }) + .catch(() => { + this.isAdmin = false + this.allUserEvents = [] + }) + }, + getSettings() { + LNbits.api + .request('GET', '/events/api/v1/events/settings') + .then(response => { + this.settings = response.data + }) + .catch(() => { + // Not admin or settings unavailable; keep defaults. + }) + }, + saveSettings() { + LNbits.api + .request( + 'PUT', + '/events/api/v1/events/settings', + null, + this.settings + ) + .then(() => { + Quasar.Notify.create({type: 'positive', message: 'Settings saved'}) + }) + .catch(LNbits.utils.notifyApiError) + }, + getPendingEvents() { + LNbits.api + .request('GET', '/events/api/v1/events/pending') + .then(response => { + this.pendingEvents = response.data + }) + .catch(() => { + this.pendingEvents = [] + }) + }, + approveEvent(eventId) { + LNbits.utils.confirmDialog('Approve this event?').onOk(() => { + LNbits.api + .request('PUT', '/events/api/v1/events/' + eventId + '/approve') + .then(() => { + Quasar.Notify.create({ + type: 'positive', + message: 'Event approved' + }) + this.getEvents() + this.getPendingEvents() + }) + .catch(LNbits.utils.notifyApiError) + }) + }, + rejectEvent(eventId) { + LNbits.utils.confirmDialog('Reject this event?').onOk(() => { + LNbits.api + .request('PUT', '/events/api/v1/events/' + eventId + '/reject') + .then(() => { + Quasar.Notify.create({ + type: 'positive', + message: 'Event rejected' + }) + this.getEvents() + this.getPendingEvents() + }) + .catch(LNbits.utils.notifyApiError) + }) }, sendEventData() { const wallet = _.findWhere(this.g.user.wallets, { @@ -275,6 +360,8 @@ window.PageEvents = { if (this.g.user.wallets.length) { this.getTickets() this.getEvents() + this.getSettings() + this.getPendingEvents() if (this.g.allowedCurrencies && this.g.allowedCurrencies.length > 0) { this.currencies = ['sats', ...this.g.allowedCurrencies] } else { diff --git a/static/js/index.vue b/static/js/index.vue index 174f0c1..4a6c142 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -1,6 +1,23 @@