diff --git a/src/modules/activities/composables/useTicketScanner.ts b/src/modules/activities/composables/useTicketScanner.ts index 4538d3e..7053f5e 100644 --- a/src/modules/activities/composables/useTicketScanner.ts +++ b/src/modules/activities/composables/useTicketScanner.ts @@ -1,8 +1,7 @@ import { ref, onMounted, type Ref } from 'vue' import { useLocalStorage } from '@vueuse/core' import { injectService, SERVICE_TOKENS } from '@/core/di-container' -import { useAuth } from '@/composables/useAuthService' -import type { TicketApiService } from '../services/TicketApiService' +import type { NostrTransportService } from '@/modules/base/services/NostrTransportService' export type ScanStatus = 'ok' | 'duplicate-session' | 'error' @@ -40,26 +39,21 @@ export interface EventStats { /** * Stateful scanner driver. Owns the camera lifecycle (delegated to - * useQRScanner upstream), the QR decode, the register-ticket call, - * an authoritative event roster fetch, and a session-local dedup - * cache. + * useQRScanner upstream), the QR decode, the + * `events_ticket_register` RPC call, an authoritative event roster + * fetched from `events_list_event_tickets`, and a session-local + * dedup cache. * * Counts + the displayed scanned list come from the backend so the * UI agrees with reality even when a second organizer is scanning * on another device. The localStorage cache is kept as a silent - * dedup so the 5-fps decode loop doesn't re-fire the request on a - * QR the camera held in frame for multiple ticks. - * - * Auth: the organizer's wallet admin_key. The events extension's - * `GET /tickets/event/{id}/stats` + `PUT /tickets/register/{id}` - * endpoints both check the event's wallet is in the caller's - * wallet set, so admin_key alone is sufficient. We deliberately - * route via HTTP rather than the kind-21000 nostr-transport RPC - * because post-#9 the webapp no longer holds a raw user prvkey. + * dedup so the 5-fps decode loop doesn't re-fire the RPC on a QR + * the camera held in frame for multiple ticks. */ export function useTicketScanner(activityId: Ref) { - const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) - const { currentUser } = useAuth() + const transport = injectService( + SERVICE_TOKENS.NOSTR_TRANSPORT_SERVICE, + ) const isProcessing = ref(false) const lastScan = ref(null) @@ -92,15 +86,20 @@ export function useTicketScanner(activityId: Ref) { async function refreshStats(): Promise { if (!activityId.value) return - const adminKey = currentUser.value?.wallets?.[0]?.adminkey - if (!adminKey) { - statsError.value = 'No wallet admin key available' - return - } statsLoading.value = true statsError.value = null try { - const data = await ticketApi.getEventStats(activityId.value, adminKey) + const data = await transport.call<{ + sold: number + registered: number + remaining: number + tickets: Array<{ + id: string + name?: string | null + registered: boolean + registered_at: string | null + }> + }>('events_list_event_tickets', { event_id: activityId.value }) eventStats.value = { sold: data.sold, registered: data.registered, @@ -133,38 +132,26 @@ export function useTicketScanner(activityId: Ref) { return } - const adminKey = currentUser.value?.wallets?.[0]?.adminkey - if (!adminKey) { - lastScan.value = { - status: 'error', - ticketId, - message: 'No wallet admin key available', - } - isPaused.value = true - return - } - isProcessing.value = true try { - const ticket = await ticketApi.registerTicket(ticketId, adminKey) - scanned.value = [ + const ticket = await transport.call>( + 'events_ticket_register', { - ticketId, - name: ticket.name ?? null, - registeredAt: new Date().toISOString(), + event_id: activityId.value, + ticket_id: ticketId, }, + ) + const name = (ticket?.name as string | null | undefined) ?? null + scanned.value = [ + { ticketId, name, registeredAt: new Date().toISOString() }, ...scanned.value, ] - lastScan.value = { - status: 'ok', - ticketId, - ticket: ticket as unknown as Record, - } + lastScan.value = { status: 'ok', ticketId, ticket } } catch (e) { - // Backend errors surface via TicketApiService.request as the - // HTTP `detail` string: "Ticket not paid for", "Ticket - // already registered", "Ticket does not exist.", "You do not - // own this event.", etc. + // Backend RPC errors arrive as NostrRpcError with the + // string in `.message`: "Ticket not paid for", "Ticket + // already registered", "Ticket does not exist on this + // event", "You do not own this event", etc. lastScan.value = { status: 'error', ticketId, diff --git a/src/modules/activities/services/TicketApiService.ts b/src/modules/activities/services/TicketApiService.ts index 43c2308..f127d5e 100644 --- a/src/modules/activities/services/TicketApiService.ts +++ b/src/modules/activities/services/TicketApiService.ts @@ -252,60 +252,6 @@ export class TicketApiService { } } - /** - * Door-scanner roster + counts for one event. Organizer-only — - * requires the event-owning wallet's admin_key. Returns the same - * shape the `events_list_event_tickets` nostr-transport RPC does; - * we route via HTTP because post-#9 the webapp no longer holds a - * raw user prvkey to sign kind-21000 envelopes with. - */ - async getEventStats( - eventId: string, - adminKey: string, - ): Promise<{ - event_id: string - sold: number - registered: number - remaining: number - tickets: Array<{ - id: string - name?: string | null - registered: boolean - registered_at: string | null - }> - }> { - return this.request(`/events/api/v1/tickets/event/${eventId}/stats`, { - method: 'GET', - headers: { 'X-API-KEY': adminKey }, - }) - } - - /** - * Mark a paid ticket as registered at the door. Organizer-only — - * requires the event-owning wallet's admin_key. Backend rejects - * unpaid / already-registered / not-owned cases with HTTP errors - * whose `detail` becomes the thrown Error message. - */ - async registerTicket(ticketId: string, adminKey: string): Promise { - const t = await this.request(`/events/api/v1/tickets/register/${ticketId}`, { - method: 'PUT', - headers: { 'X-API-KEY': adminKey }, - }) - return { - id: t.id, - wallet: t.wallet, - activityId: t.event, - name: t.name, - email: t.email, - userId: t.user_id, - registered: t.registered, - paid: t.paid, - time: t.time, - regTimestamp: t.reg_timestamp, - extra: t.extra as ActivityTicketExtra | undefined, - } - } - /** * Probe whether the current user has LNbits admin privileges. The * `/all` endpoint is `check_admin`-gated, so a 200 means "admin",