Compare commits

...

3 commits

Author SHA1 Message Date
9b1b56e05d feat(activities): status badge + buy-disabled on own pending events
Show a "Pending review" (or "Rejected") badge on the user's own
non-approved events, and disable the Buy Ticket button on any
non-approved event with a "Not yet available" label. Probe
auto_approve via the public endpoint with inkey, not adminkey, so the
warning copy works for non-admin owners.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:55:19 +02:00
01b871e7fa feat(activities): merge own events into the feed
When authenticated, parallel-fetch the caller's own events (any
status) alongside the public approved feed and merge with public-wins
dedup. Without this, an event that drops to `proposed` after a
non-admin edit disappears from the user's view — they couldn't find
it to make a follow-up edit or watch for re-approval.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:55:19 +02:00
3047565920 feat(activities): fetchMyEvents + invoice-key auto_approve probe
`fetchMyEvents` hits the existing all_wallets=true endpoint to surface
the caller's own events regardless of status. `getAutoApprove` now
calls the public probe (invoice-key-gated) added in events extension
v1.3.0-aio.5 so non-admin webapp users get accurate edit-flow copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:55:19 +02:00
3 changed files with 73 additions and 12 deletions

View file

@ -1,14 +1,43 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useAsyncState } from '@vueuse/core' import { useAsyncState } from '@vueuse/core'
import { injectService, SERVICE_TOKENS } from '@/core/di-container' import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import { useAuth } from '@/composables/useAuthService'
import type { TicketApiService } from '../services/TicketApiService' import type { TicketApiService } from '../services/TicketApiService'
import type { TicketedEvent } from '../types/ticket' import type { TicketedEvent } from '../types/ticket'
export function useEvents() { export function useEvents() {
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService
const { isAuthenticated, currentUser } = useAuth()
// When authenticated, also fetch the user's own events (any status)
// and merge them into the feed. Otherwise an event that drops to
// `proposed` after a non-admin edit disappears from the user's view
// entirely — they'd be unable to find it to make a follow-up edit or
// monitor its approval status. Public approved events from other
// users take precedence on dedup (server is the source of truth for
// the public view).
const fetchAll = async (): Promise<TicketedEvent[]> => {
const publicEvents = (await ticketApi.fetchTicketedEvents()) as TicketedEvent[]
if (!isAuthenticated.value) return publicEvents
const invoiceKey = currentUser.value?.wallets?.[0]?.inkey
if (!invoiceKey) return publicEvents
try {
const myEvents = (await ticketApi.fetchMyEvents(invoiceKey)) as TicketedEvent[]
const seen = new Set(publicEvents.map((e) => e.id))
const own = myEvents.filter((e) => !seen.has(e.id))
return [...publicEvents, ...own]
} catch {
// Falling back to just the public feed is acceptable — the user
// can still browse, they just won't see their own pending events.
return publicEvents
}
}
const { state: events, isLoading, error: asyncError, execute: refresh } = useAsyncState( const { state: events, isLoading, error: asyncError, execute: refresh } = useAsyncState(
() => ticketApi.fetchTicketedEvents() as Promise<TicketedEvent[]>, fetchAll,
[] as TicketedEvent[], [] as TicketedEvent[],
{ {
immediate: true, immediate: true,

View file

@ -34,6 +34,20 @@ export class TicketApiService {
return response return response
} }
/**
* Fetch the authenticated user's own events across all their wallets,
* regardless of status. Lets the webapp show the user's pending /
* rejected events alongside the public approved feed without this
* a user who edits under `auto_approve=false` loses sight of their
* own event the moment it drops to `proposed`.
*/
async fetchMyEvents(invoiceKey: string): Promise<any[]> {
return this.request('/events/api/v1/events?all_wallets=true', {
method: 'GET',
headers: { 'X-API-KEY': invoiceKey },
})
}
/** /**
* Request a ticket purchase (creates a Lightning invoice). * Request a ticket purchase (creates a Lightning invoice).
* Uses POST /tickets/{event_id} with user_id in body (upstream API). * Uses POST /tickets/{event_id} with user_id in body (upstream API).
@ -187,14 +201,16 @@ export class TicketApiService {
} }
/** /**
* Read the extension's auto_approve flag. Admin-only endpoint, so * Read the extension's auto_approve flag. Hits the public probe
* non-admin callers see false (the safe default for UI gating). * (invoice-key-gated, available to any wallet holder), so non-admin
* users see the real value and the edit-flow copy is accurate.
* Degrades to `false` on failure the safer default for warning UI.
*/ */
async getAutoApprove(adminKey: string): Promise<boolean> { async getAutoApprove(invoiceKey: string): Promise<boolean> {
try { try {
const settings = await this.request('/events/api/v1/events/settings', { const settings = await this.request('/events/api/v1/events/settings/public', {
method: 'GET', method: 'GET',
headers: { 'X-API-KEY': adminKey }, headers: { 'X-API-KEY': invoiceKey },
}) })
return Boolean(settings?.auto_approve) return Boolean(settings?.auto_approve)
} catch { } catch {

View file

@ -48,11 +48,13 @@ function canEdit(event: TicketedEvent): boolean {
onMounted(async () => { onMounted(async () => {
if (!isAuthenticated.value) return if (!isAuthenticated.value) return
const adminKey = currentUser.value?.wallets?.[0]?.adminkey const wallet = currentUser.value?.wallets?.[0]
if (!adminKey) return if (!wallet?.inkey) return
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService
isAdmin.value = await ticketApi.isAdmin(adminKey) autoApprove.value = await ticketApi.getAutoApprove(wallet.inkey)
autoApprove.value = await ticketApi.getAutoApprove(adminKey) if (wallet.adminkey) {
isAdmin.value = await ticketApi.isAdmin(wallet.adminkey)
}
}) })
function formatDate(dateStr: string | null | undefined) { function formatDate(dateStr: string | null | undefined) {
@ -159,7 +161,16 @@ function handleEventChanged() {
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card v-for="event in upcomingEvents" :key="event.id" class="flex flex-col"> <Card v-for="event in upcomingEvents" :key="event.id" class="flex flex-col">
<CardHeader> <CardHeader>
<div class="flex items-start justify-between gap-2">
<CardTitle class="text-foreground">{{ event.name }}</CardTitle> <CardTitle class="text-foreground">{{ event.name }}</CardTitle>
<Badge
v-if="canEdit(event) && event.status !== 'approved'"
:variant="event.status === 'rejected' ? 'destructive' : 'secondary'"
class="shrink-0 capitalize"
>
{{ event.status === 'rejected' ? 'Rejected' : 'Pending review' }}
</Badge>
</div>
<CardDescription>{{ event.info }}</CardDescription> <CardDescription>{{ event.info }}</CardDescription>
</CardHeader> </CardHeader>
<CardContent class="flex-grow"> <CardContent class="flex-grow">
@ -186,13 +197,18 @@ function handleEventChanged() {
<Button <Button
class="flex-1" class="flex-1"
variant="default" variant="default"
:disabled="event.amount_tickets <= event.sold || !isAuthenticated" :disabled="
event.status !== 'approved' ||
event.amount_tickets <= event.sold ||
!isAuthenticated
"
@click="handlePurchaseClick(event)" @click="handlePurchaseClick(event)"
> >
<span v-if="!isAuthenticated" class="flex items-center gap-2"> <span v-if="!isAuthenticated" class="flex items-center gap-2">
<LogIn class="w-4 h-4" /> <LogIn class="w-4 h-4" />
Login to Purchase Login to Purchase
</span> </span>
<span v-else-if="event.status !== 'approved'">Not yet available</span>
<span v-else>Buy Ticket</span> <span v-else>Buy Ticket</span>
</Button> </Button>
<Button <Button