From e61d3c4d46fb1c542cbda80cc5b9047b72bad21a Mon Sep 17 00:00:00 2001 From: Padreug Date: Sun, 26 Apr 2026 22:26:28 +0200 Subject: [PATCH] Consolidate events ticketing into activities module Absorb the disabled events module into the active activities module, eliminating duplicated LNbits events extension API surface and legacy imports. Activities now owns all ticketing UI (EventsPage, MyTicketsPage with QR codes, PurchaseTicketDialog, CreateEventDialog) alongside its existing Nostr NIP-52 calendar event discovery. - Internalize payInvoiceWithWallet in PaymentService (core LNbits endpoint) - Enhance TicketApiService with createEvent and getCurrencies methods - Add TICKET_API DI token for canonical ticket service access - Port composables (useTicketPurchase, useUserTickets, useEvents) with DI - Port components (PurchaseTicketDialog, CreateEventDialog) - Replace MyTicketsPage placeholder with full QR-code ticket display - Add /events route to activities module - Delete src/modules/events/, src/lib/api/events.ts, src/lib/types/event.ts Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app.config.ts | 13 - src/app.ts | 9 - src/composables/useModularNavigation.ts | 14 +- src/core/di-container.ts | 6 +- src/core/services/PaymentService.ts | 42 +- src/lib/api/events.ts | 169 ----- src/lib/types/event.ts | 36 -- .../components/CreateEventDialog.vue | 106 +--- .../components/PurchaseTicketDialog.vue | 11 +- .../composables/useEvents.ts | 21 +- .../composables/useTicketPurchase.ts | 85 ++- .../composables/useUserTickets.ts | 43 +- src/modules/activities/index.ts | 20 +- .../activities/services/TicketApiService.ts | 31 + src/modules/activities/types/ticket.ts | 33 + .../views/EventsPage.vue | 111 +--- .../activities/views/MyTicketsPage.vue | 314 ++++++++- src/modules/events/index.ts | 99 --- src/modules/events/services/events-api.ts | 216 ------- src/modules/events/types/event.ts | 49 -- src/modules/events/views/MyTicketsPage.vue | 599 ------------------ 21 files changed, 547 insertions(+), 1480 deletions(-) delete mode 100644 src/lib/api/events.ts delete mode 100644 src/lib/types/event.ts rename src/modules/{events => activities}/components/CreateEventDialog.vue (72%) rename src/modules/{events => activities}/components/PurchaseTicketDialog.vue (98%) rename src/modules/{events => activities}/composables/useEvents.ts (69%) rename src/modules/{events => activities}/composables/useTicketPurchase.ts (74%) rename src/modules/{events => activities}/composables/useUserTickets.ts (77%) rename src/modules/{events => activities}/views/EventsPage.vue (67%) delete mode 100644 src/modules/events/index.ts delete mode 100644 src/modules/events/services/events-api.ts delete mode 100644 src/modules/events/types/event.ts delete mode 100644 src/modules/events/views/MyTicketsPage.vue diff --git a/src/app.config.ts b/src/app.config.ts index a414bd2..da83333 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -88,19 +88,6 @@ export const appConfig: AppConfig = { } } }, - events: { - name: 'events', - enabled: false, - lazy: false, - config: { - apiConfig: { - baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000', - apiKey: import.meta.env.VITE_API_KEY || '' - }, - ticketValidationEndpoint: '/api/tickets/validate', - maxTicketsPerUser: 10 - } - }, activities: { name: 'activities', enabled: true, diff --git a/src/app.ts b/src/app.ts index d5281b5..f23f021 100644 --- a/src/app.ts +++ b/src/app.ts @@ -13,7 +13,6 @@ import appConfig from './app.config' import baseModule from './modules/base' import nostrFeedModule from './modules/nostr-feed' import chatModule from './modules/chat' -import eventsModule from './modules/events' import marketModule from './modules/market' import walletModule from './modules/wallet' import expensesModule from './modules/expenses' @@ -44,7 +43,6 @@ export async function createAppInstance() { ...baseModule.routes || [], ...nostrFeedModule.routes || [], ...chatModule.routes || [], - ...eventsModule.routes || [], ...marketModule.routes || [], ...walletModule.routes || [], ...expensesModule.routes || [], @@ -113,13 +111,6 @@ export async function createAppInstance() { ) } - // Register events module - if (appConfig.modules.events.enabled) { - moduleRegistrations.push( - pluginManager.register(eventsModule, appConfig.modules.events) - ) - } - // Register market module if (appConfig.modules.market.enabled) { moduleRegistrations.push( diff --git a/src/composables/useModularNavigation.ts b/src/composables/useModularNavigation.ts index b03bbe8..a29efd2 100644 --- a/src/composables/useModularNavigation.ts +++ b/src/composables/useModularNavigation.ts @@ -26,11 +26,11 @@ export function useModularNavigation() { items.push({ name: t('nav.home'), href: '/', requiresAuth: true }) // Add navigation items based on enabled modules - if (appConfig.modules.events.enabled) { - items.push({ - name: t('nav.events'), - href: '/events', - requiresAuth: true + if (appConfig.modules.activities?.enabled) { + items.push({ + name: t('nav.events'), + href: '/events', + requiresAuth: true }) } @@ -67,8 +67,8 @@ export function useModularNavigation() { const userMenuItems = computed(() => { const items: NavigationItem[] = [] - // Events module items - if (appConfig.modules.events.enabled) { + // Activities module items (events + tickets) + if (appConfig.modules.activities?.enabled) { items.push({ name: 'My Tickets', href: '/my-tickets', diff --git a/src/core/di-container.ts b/src/core/di-container.ts index bcd8177..411ebad 100644 --- a/src/core/di-container.ts +++ b/src/core/di-container.ts @@ -149,12 +149,10 @@ export const SERVICE_TOKENS = { // Nostr metadata services NOSTR_METADATA_SERVICE: Symbol('nostrMetadataService'), - // Events services - EVENTS_SERVICE: Symbol('eventsService'), - - // Activities services (Nostr-native events module) + // Activities services (Nostr-native events + ticketing module) ACTIVITIES_NOSTR_SERVICE: Symbol('activitiesNostrService'), ACTIVITIES_TICKET_API: Symbol('activitiesTicketApi'), + TICKET_API: Symbol('ticketApi'), // Invoice services INVOICE_SERVICE: Symbol('invoiceService'), diff --git a/src/core/services/PaymentService.ts b/src/core/services/PaymentService.ts index 8f5733b..0021e46 100644 --- a/src/core/services/PaymentService.ts +++ b/src/core/services/PaymentService.ts @@ -1,6 +1,6 @@ import { ref, computed } from 'vue' import { BaseService } from '@/core/base/BaseService' -import { payInvoiceWithWallet } from '@/lib/api/events' +import { config } from '@/lib/config' import { toast } from 'vue-sonner' export interface PaymentResult { @@ -198,6 +198,41 @@ export class PaymentService extends BaseService { } } + /** + * Pay a Lightning invoice via LNbits POST /api/v1/payments. + * This is a core LNbits operation, not extension-specific. + */ + private async payInvoice( + paymentRequest: string, + adminKey: string + ): Promise { + const baseUrl = config.api.baseUrl + const response = await fetch(`${baseUrl}/api/v1/payments`, { + method: 'POST', + headers: { + 'accept': 'application/json', + 'Content-Type': 'application/json', + 'X-API-KEY': adminKey, + }, + body: JSON.stringify({ + out: true, + bolt11: paymentRequest, + }), + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: 'Payment failed' })) + const errorMessage = typeof error.detail === 'string' + ? error.detail + : Array.isArray(error.detail) + ? error.detail[0]?.msg ?? 'Payment failed' + : 'Payment failed' + throw new Error(errorMessage) + } + + return await response.json() + } + /** * Pay Lightning invoice with user's wallet */ @@ -224,9 +259,8 @@ export class PaymentService extends BaseService { this.debug(`Paying invoice with wallet: ${wallet.id.slice(0, 8)}`) // Make payment - const paymentResult = await payInvoiceWithWallet( - paymentRequest, - wallet.id, + const paymentResult = await this.payInvoice( + paymentRequest, wallet.adminkey ) diff --git a/src/lib/api/events.ts b/src/lib/api/events.ts deleted file mode 100644 index dbb783c..0000000 --- a/src/lib/api/events.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { Event, Ticket } from '../types/event' -import { config } from '@/lib/config' -import { injectService, SERVICE_TOKENS } from '@/core/di-container' -import type { LnbitsAPI } from './lnbits' - -const API_BASE_URL = config.api.baseUrl || 'http://lnbits' -const API_KEY = config.api.key - -// Generic error type for API responses -interface ApiError { - detail: string | Array<{ loc: [string, number]; msg: string; type: string }> -} - -export async function fetchEvents(): Promise { - try { - // Use the new public endpoint that allows access to all events without authentication - const response = await fetch( - `${API_BASE_URL}/events/api/v1/events/public`, - { - headers: { - 'accept': 'application/json', - }, - } - ) - - if (!response.ok) { - const error: ApiError = await response.json() - const errorMessage = typeof error.detail === 'string' - ? error.detail - : error.detail[0]?.msg || 'Failed to fetch events' - throw new Error(errorMessage) - } - - return await response.json() as Event[] - } catch (error) { - console.error('Error fetching events:', error) - throw error - } -} - -export async function purchaseTicket(eventId: string): Promise<{ payment_hash: string; payment_request: string }> { - try { - // Get injected LnbitsAPI service - const lnbitsAPI = injectService(SERVICE_TOKENS.LNBITS_API) as LnbitsAPI - - // Get current user to ensure authentication - const user = await lnbitsAPI.getCurrentUser() - if (!user) { - throw new Error('User not authenticated') - } - - const response = await fetch( - `${API_BASE_URL}/events/api/v1/tickets/${eventId}/user/${user.id}`, - { - method: 'GET', - headers: { - 'accept': 'application/json', - 'X-API-KEY': API_KEY, - 'Authorization': `Bearer ${lnbitsAPI.getAccessToken()}`, - }, - } - ) - - if (!response.ok) { - const error: ApiError = await response.json() - const errorMessage = typeof error.detail === 'string' - ? error.detail - : error.detail[0]?.msg || 'Failed to purchase ticket' - throw new Error(errorMessage) - } - - return await response.json() - } catch (error) { - console.error('Error purchasing ticket:', error) - throw error - } -} - -export async function payInvoiceWithWallet(paymentRequest: string, _walletId: string, adminKey: string): Promise<{ payment_hash: string; fee_msat: number; preimage: string }> { - try { - const response = await fetch( - `${API_BASE_URL}/api/v1/payments`, - { - method: 'POST', - headers: { - 'accept': 'application/json', - 'Content-Type': 'application/json', - 'X-API-KEY': adminKey, - }, - body: JSON.stringify({ - out: true, - bolt11: paymentRequest, - }), - } - ) - - if (!response.ok) { - const error: ApiError = await response.json() - const errorMessage = typeof error.detail === 'string' - ? error.detail - : error.detail[0]?.msg || 'Failed to pay invoice' - throw new Error(errorMessage) - } - - return await response.json() - } catch (error) { - console.error('Error paying invoice:', error) - throw error - } -} - -export async function checkPaymentStatus(eventId: string, paymentHash: string): Promise<{ paid: boolean; ticket_id?: string }> { - try { - const response = await fetch( - `${API_BASE_URL}/events/api/v1/tickets/${eventId}/${paymentHash}`, - { - method: 'POST', - headers: { - 'accept': 'application/json', - 'X-API-KEY': API_KEY, - }, - } - ) - - if (!response.ok) { - const error: ApiError = await response.json() - const errorMessage = typeof error.detail === 'string' - ? error.detail - : error.detail[0]?.msg || 'Failed to check payment status' - throw new Error(errorMessage) - } - - return await response.json() - } catch (error) { - console.error('Error checking payment status:', error) - throw error - } -} - -export async function fetchUserTickets(userId: string): Promise { - try { - // Get injected LnbitsAPI service - const lnbitsAPI = injectService(SERVICE_TOKENS.LNBITS_API) as LnbitsAPI - - const response = await fetch( - `${API_BASE_URL}/events/api/v1/tickets/user/${userId}`, - { - headers: { - 'accept': 'application/json', - 'X-API-KEY': API_KEY, - 'Authorization': `Bearer ${lnbitsAPI.getAccessToken()}`, - }, - } - ) - - if (!response.ok) { - const error: ApiError = await response.json() - const errorMessage = typeof error.detail === 'string' - ? error.detail - : error.detail[0]?.msg || 'Failed to fetch user tickets' - throw new Error(errorMessage) - } - - return await response.json() - } catch (error) { - console.error('Error fetching user tickets:', error) - throw error - } -} \ No newline at end of file diff --git a/src/lib/types/event.ts b/src/lib/types/event.ts deleted file mode 100644 index 65a965b..0000000 --- a/src/lib/types/event.ts +++ /dev/null @@ -1,36 +0,0 @@ -export interface Event { - id: string - wallet: string - name: string - info: string - closing_date: string - event_start_date: string - event_end_date: string - currency: string - amount_tickets: number - price_per_ticket: number - time: string - sold: number - banner: string | null -} - -export interface Ticket { - id: string - wallet: string - event: string - name: string | null - email: string | null - user_id: string | null - registered: boolean - paid: boolean - time: string - reg_timestamp: string -} - -export interface EventsApiError { - detail: Array<{ - loc: [string, number] - msg: string - type: string - }> -} \ No newline at end of file diff --git a/src/modules/events/components/CreateEventDialog.vue b/src/modules/activities/components/CreateEventDialog.vue similarity index 72% rename from src/modules/events/components/CreateEventDialog.vue rename to src/modules/activities/components/CreateEventDialog.vue index 75bf327..774514f 100644 --- a/src/modules/events/components/CreateEventDialog.vue +++ b/src/modules/activities/components/CreateEventDialog.vue @@ -32,10 +32,9 @@ import { import { Calendar, Loader2 } from 'lucide-vue-next' import { toastService } from '@/core/services/ToastService' import { injectService, SERVICE_TOKENS } from '@/core/di-container' -import { EVENTS_API_TOKEN } from '../composables/useEvents' -import type { CreateEventRequest } from '../types/event' +import type { TicketApiService } from '../services/TicketApiService' +import type { CreateEventRequest } from '../types/ticket' -// Props interface Props { open: boolean onCreateEvent: (eventData: CreateEventRequest) => Promise @@ -47,8 +46,6 @@ const emit = defineEmits<{ 'event-created': [] }>() -// Form validation schema (removed wallet field - will be auto-selected) -// Note: Ticket sales will automatically close when the event ends const formSchema = toTypedSchema(z.object({ name: z.string().min(1, "Event name is required").max(200, "Name too long"), info: z.string().min(1, "Event description is required").max(2000, "Description too long"), @@ -67,7 +64,6 @@ const formSchema = toTypedSchema(z.object({ path: ["event_end_date"] })) -// Form setup const form = useForm({ validationSchema: formSchema, initialValues: { @@ -82,29 +78,19 @@ const form = useForm({ } }) -// Get PaymentService for wallet selection const paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE) +const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService | null -// Get EventsApiService for currency loading -const eventsApi = injectService(EVENTS_API_TOKEN) - -// Load available currencies const availableCurrencies = ref(['sats']) const loadingCurrencies = ref(false) -// Load currencies when dialog opens watch(() => props.open, async (isOpen) => { - if (isOpen && eventsApi && !loadingCurrencies.value) { + if (isOpen && ticketApi && !loadingCurrencies.value) { loadingCurrencies.value = true try { - // Type cast to ensure getCurrencies method exists - const apiService = eventsApi as any - if (apiService.getCurrencies) { - availableCurrencies.value = await apiService.getCurrencies() - } + availableCurrencies.value = await ticketApi.getCurrencies() } catch (error) { console.warn('Failed to load currencies:', error) - // Keep default currencies } finally { loadingCurrencies.value = false } @@ -115,35 +101,31 @@ const { resetForm, meta } = form const isFormValid = computed(() => meta.value.valid) const isLoading = ref(false) -// Get today's date in YYYY-MM-DD format for min date validation const today = computed(() => format(new Date(), 'yyyy-MM-dd')) -// Form submission const onSubmit = form.handleSubmit(async (formValues) => { if (!isFormValid.value) return - + if (!paymentService) { toastService.error('Payment service not available') return } - // Type cast to ensure getPreferredWallet method exists const paymentSvc = paymentService as any const preferredWallet = paymentSvc?.getPreferredWallet?.() if (!preferredWallet) { toastService.error('No wallet available. Please connect a wallet first.') return } - + isLoading.value = true try { - // Add the selected wallet ID and set closing_date to event_end_date const eventData: CreateEventRequest = { ...formValues, wallet: preferredWallet.id, - closing_date: formValues.event_end_date // Ticket sales close when event ends + closing_date: formValues.event_end_date } - + await props.onCreateEvent(eventData) toastService.success('Event created successfully!') resetForm() @@ -157,7 +139,6 @@ const onSubmit = form.handleSubmit(async (formValues) => { } }) -// Handle dialog close const handleOpenChange = (open: boolean) => { if (!open && !isLoading.value) { resetForm() @@ -180,47 +161,33 @@ const handleOpenChange = (open: boolean) => {
- Event Name * - + - Event Description * -