Compare commits
No commits in common. "dfc4ad7322ab7b454818e21e14370dc9a34652d4" and "b3db5e81efad06db253bedaf4b870b86ea79f32e" have entirely different histories.
dfc4ad7322
...
b3db5e81ef
6 changed files with 18 additions and 439 deletions
|
|
@ -24,9 +24,6 @@ import { Input } from '@/components/ui/input'
|
|||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
import { Bell, ChevronDown } from 'lucide-vue-next'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import {
|
||||
Select,
|
||||
|
|
@ -101,14 +98,8 @@ const formSchema = toTypedSchema(
|
|||
event_end_time: z.string().optional().default(''),
|
||||
location: z.string().max(500).optional().default(''),
|
||||
currency: z.string().default("sat"),
|
||||
allow_fiat: z.boolean().default(false),
|
||||
fiat_currency: z.string().default("USD"),
|
||||
amount_tickets: z.number().min(0).max(100000).default(0),
|
||||
price_per_ticket: z.number().min(0).default(0),
|
||||
email_notifications: z.boolean().default(false),
|
||||
nostr_notifications: z.boolean().default(false),
|
||||
notification_subject: z.string().max(200).default(''),
|
||||
notification_body: z.string().max(2000).default(''),
|
||||
})
|
||||
.superRefine((v, ctx) => {
|
||||
// End must not precede start. Compare on the folded date+time
|
||||
|
|
@ -137,14 +128,8 @@ const form = useForm({
|
|||
event_end_time: '',
|
||||
location: '',
|
||||
currency: 'sat',
|
||||
allow_fiat: false,
|
||||
fiat_currency: 'USD',
|
||||
amount_tickets: 0,
|
||||
price_per_ticket: 0,
|
||||
email_notifications: false,
|
||||
nostr_notifications: false,
|
||||
notification_subject: '',
|
||||
notification_body: '',
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -164,7 +149,6 @@ function splitDateTime(value: string | null | undefined): { date: string; time:
|
|||
// When `true`, suppress the auto-mirror watcher so we don't clobber an
|
||||
// edit-mode population with start-date side effects mid-setValues.
|
||||
const isPopulating = ref(false)
|
||||
const notificationsOpen = ref(false)
|
||||
|
||||
// Auto-mirror end date to start: when the user picks a start date,
|
||||
// surface that same date in the end-date picker so a one-day event
|
||||
|
|
@ -204,14 +188,8 @@ async function populateFromEvent(event: TicketedEvent) {
|
|||
event_end_time: end.time,
|
||||
location: event.location ?? '',
|
||||
currency: event.currency ?? 'sat',
|
||||
allow_fiat: event.allow_fiat ?? false,
|
||||
fiat_currency: event.fiat_currency ?? 'USD',
|
||||
amount_tickets: event.amount_tickets ?? 0,
|
||||
price_per_ticket: event.price_per_ticket ?? 0,
|
||||
email_notifications: event.extra?.email_notifications ?? false,
|
||||
nostr_notifications: event.extra?.nostr_notifications ?? false,
|
||||
notification_subject: event.extra?.notification_subject ?? '',
|
||||
notification_body: event.extra?.notification_body ?? '',
|
||||
})
|
||||
selectedCategories.value = [...(event.categories ?? [])]
|
||||
if (event.banner) {
|
||||
|
|
@ -317,26 +295,10 @@ const onSubmit = form.handleSubmit(async (formValues) => {
|
|||
eventData.banner = null
|
||||
}
|
||||
if (formValues.currency) eventData.currency = formValues.currency
|
||||
// allow_fiat / fiat_currency: always send so the toggle reads
|
||||
// both directions on edit (a true→false flip must propagate).
|
||||
eventData.allow_fiat = formValues.allow_fiat
|
||||
if (formValues.fiat_currency) eventData.fiat_currency = formValues.fiat_currency
|
||||
if (formValues.amount_tickets) eventData.amount_tickets = formValues.amount_tickets
|
||||
if (formValues.price_per_ticket) eventData.price_per_ticket = formValues.price_per_ticket
|
||||
if (selectedCategories.value.length > 0) eventData.categories = selectedCategories.value
|
||||
|
||||
// Notification config goes inside the `extra` envelope. On edit
|
||||
// overlay onto the existing event.extra so unrelated fields the
|
||||
// LNbits admin UI sets (promo_codes, conditional, min_tickets)
|
||||
// survive the round-trip.
|
||||
eventData.extra = {
|
||||
...(props.event?.extra ?? {}),
|
||||
email_notifications: formValues.email_notifications,
|
||||
nostr_notifications: formValues.nostr_notifications,
|
||||
notification_subject: formValues.notification_subject,
|
||||
notification_body: formValues.notification_body,
|
||||
}
|
||||
|
||||
if (isEditMode.value) {
|
||||
if (!props.onUpdateEvent || !props.event?.id) {
|
||||
toastService.error('Update handler missing')
|
||||
|
|
@ -606,108 +568,6 @@ const handleOpenChange = (open: boolean) => {
|
|||
</FormField>
|
||||
</div>
|
||||
|
||||
<!-- Fiat checkout (organizer opts in). Backend requires the
|
||||
host's LNbits admin to have configured a fiat provider
|
||||
(Stripe etc.) under settings.fiat_providers; toggling
|
||||
this on without one will fail at purchase time. -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 items-end">
|
||||
<FormField v-slot="{ value, handleChange }" name="allow_fiat">
|
||||
<FormItem class="sm:col-span-2 flex flex-row items-center justify-between rounded-md border p-3">
|
||||
<div class="space-y-0.5">
|
||||
<FormLabel>Accept fiat payments</FormLabel>
|
||||
<FormDescription class="text-xs">
|
||||
Buyers can pay with the LNbits instance's configured fiat provider (e.g. Stripe).
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch :model-value="value as boolean" @update:model-value="handleChange" />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="fiat_currency">
|
||||
<FormItem v-show="form.values.allow_fiat">
|
||||
<FormLabel>Fiat currency</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="USD" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="USD">USD</SelectItem>
|
||||
<SelectItem value="EUR">EUR</SelectItem>
|
||||
<SelectItem value="GBP">GBP</SelectItem>
|
||||
<SelectItem value="CHF">CHF</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<!-- Ticket buyer notifications (collapsible). The backend
|
||||
sends email + NIP-04 Nostr DM confirmations on
|
||||
payment when these are on. notification_subject /
|
||||
body let the organizer customize the message; empty
|
||||
strings fall back to the extension's defaults. -->
|
||||
<Collapsible v-model:open="notificationsOpen">
|
||||
<CollapsibleTrigger as-child>
|
||||
<Button type="button" variant="ghost" size="sm" class="w-full justify-between text-muted-foreground">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<Bell class="w-4 h-4" />
|
||||
Buyer notifications
|
||||
</span>
|
||||
<ChevronDown class="w-4 h-4 transition-transform" :class="{ 'rotate-180': notificationsOpen }" />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent class="space-y-3 pt-2">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<FormField v-slot="{ value, handleChange }" name="email_notifications">
|
||||
<FormItem class="flex flex-row items-center justify-between rounded-md border p-3">
|
||||
<FormLabel class="text-sm">Email confirmation</FormLabel>
|
||||
<FormControl>
|
||||
<Switch :model-value="value as boolean" @update:model-value="handleChange" />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ value, handleChange }" name="nostr_notifications">
|
||||
<FormItem class="flex flex-row items-center justify-between rounded-md border p-3">
|
||||
<FormLabel class="text-sm">Nostr DM confirmation</FormLabel>
|
||||
<FormControl>
|
||||
<Switch :model-value="value as boolean" @update:model-value="handleChange" />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="notification_subject">
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm">Subject</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your ticket for {event_name}" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription class="text-xs">Leave blank to use the default.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="notification_body">
|
||||
<FormItem>
|
||||
<FormLabel class="text-sm">Body</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="See you there!" rows="3" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription class="text-xs">
|
||||
Leave blank to use the default. The ticket link is appended automatically.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<Button type="button" variant="outline" @click="handleOpenChange(false)" :disabled="isLoading">
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { onUnmounted } from 'vue'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useTicketPurchase } from '../composables/useTicketPurchase'
|
||||
import { useAuth } from '@/composables/useAuthService'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import type { TicketApiService } from '../services/TicketApiService'
|
||||
import type { PaymentMethod } from '../types/ticket'
|
||||
import { User, Wallet, CreditCard, Zap, Ticket, ExternalLink } from 'lucide-vue-next'
|
||||
import { User, Wallet, CreditCard, Zap, Ticket } from 'lucide-vue-next'
|
||||
import { formatEventPrice, formatWalletBalance } from '@/lib/utils/formatting'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -17,9 +14,6 @@ interface Props {
|
|||
name: string
|
||||
price_per_ticket: number
|
||||
currency: string
|
||||
/** Whether the event accepts fiat payments. From v1.4.0+ */
|
||||
allow_fiat?: boolean
|
||||
fiat_currency?: string
|
||||
}
|
||||
isOpen: boolean
|
||||
}
|
||||
|
|
@ -51,74 +45,19 @@ const {
|
|||
showTicketQR
|
||||
} = useTicketPurchase()
|
||||
|
||||
const paymentMethod = ref<PaymentMethod>('lightning')
|
||||
const fiatRedirectUrl = ref<string | null>(null)
|
||||
const fiatProviderLabel = ref<string | null>(null)
|
||||
const isFiatPending = ref(false)
|
||||
const fiatError = ref<string | null>(null)
|
||||
|
||||
const canChooseFiat = computed(() => Boolean(props.event.allow_fiat))
|
||||
|
||||
async function handlePurchase() {
|
||||
if (!canPurchase.value) return
|
||||
fiatError.value = null
|
||||
|
||||
// Lightning path: existing composable handles QR + wallet auto-pay.
|
||||
if (paymentMethod.value === 'lightning') {
|
||||
try {
|
||||
await purchaseTicketForEvent(props.event.id)
|
||||
} catch (err) {
|
||||
console.error('Error purchasing ticket:', err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Fiat path: composable can't drive it (no QR / no bolt11). Hit the
|
||||
// API directly, then redirect the buyer to the provider's checkout
|
||||
// URL. Payment confirmation happens via webhook on the backend and
|
||||
// shows up next time the buyer reloads MyTickets.
|
||||
try {
|
||||
isFiatPending.value = true
|
||||
const lnbitsAPI = injectService(SERVICE_TOKENS.LNBITS_API) as any
|
||||
const accessToken = lnbitsAPI?.getAccessToken?.() || ''
|
||||
const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService
|
||||
const currentUser = (lnbitsAPI?.currentUser?.value) || null
|
||||
const userId = currentUser?.id
|
||||
if (!userId) {
|
||||
fiatError.value = 'Missing user id'
|
||||
return
|
||||
}
|
||||
const invoice = await ticketApi.requestTicket(
|
||||
props.event.id,
|
||||
userId,
|
||||
accessToken,
|
||||
{ paymentMethod: 'fiat' },
|
||||
)
|
||||
if (!invoice.isFiat || !invoice.fiatPaymentRequest) {
|
||||
fiatError.value = 'Fiat provider did not return a checkout URL.'
|
||||
return
|
||||
}
|
||||
fiatRedirectUrl.value = invoice.fiatPaymentRequest
|
||||
fiatProviderLabel.value = invoice.fiatProvider ?? 'provider'
|
||||
await purchaseTicketForEvent(props.event.id)
|
||||
} catch (err) {
|
||||
fiatError.value = err instanceof Error ? err.message : 'Fiat checkout failed'
|
||||
} finally {
|
||||
isFiatPending.value = false
|
||||
console.error('Error purchasing ticket:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function openFiatCheckout() {
|
||||
if (!fiatRedirectUrl.value) return
|
||||
window.open(fiatRedirectUrl.value, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('update:isOpen', false)
|
||||
resetPaymentState()
|
||||
paymentMethod.value = 'lightning'
|
||||
fiatRedirectUrl.value = null
|
||||
fiatProviderLabel.value = null
|
||||
fiatError.value = null
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
@ -222,64 +161,16 @@ onUnmounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment method selector (only shown when fiat is enabled
|
||||
on the event). Hidden entirely for Lightning-only events
|
||||
to keep the dialog uncluttered. -->
|
||||
<div v-if="canChooseFiat" class="bg-muted/50 rounded-lg p-4 space-y-2">
|
||||
<div class="text-sm font-medium">Payment method</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
:variant="paymentMethod === 'lightning' ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
@click="paymentMethod = 'lightning'"
|
||||
>
|
||||
<Zap class="w-4 h-4 mr-1.5" />
|
||||
Lightning
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
:variant="paymentMethod === 'fiat' ? 'default' : 'outline'"
|
||||
size="sm"
|
||||
@click="paymentMethod = 'fiat'"
|
||||
>
|
||||
<CreditCard class="w-4 h-4 mr-1.5" />
|
||||
{{ event.fiat_currency ?? 'Fiat' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error || fiatError" class="text-sm text-destructive bg-destructive/10 p-3 rounded-lg">
|
||||
{{ error || fiatError }}
|
||||
</div>
|
||||
|
||||
<!-- Fiat checkout panel — shown after a successful fiat
|
||||
POST when we have a provider URL to redirect to. -->
|
||||
<div v-if="fiatRedirectUrl" class="space-y-3">
|
||||
<div class="bg-muted/50 rounded-lg p-4 space-y-2">
|
||||
<div class="text-sm font-medium">Ready to pay with {{ fiatProviderLabel }}</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Opens the provider's checkout in a new tab. Your ticket
|
||||
appears in My Tickets once the payment settles.
|
||||
</p>
|
||||
</div>
|
||||
<Button @click="openFiatCheckout" class="w-full">
|
||||
<ExternalLink class="w-4 h-4 mr-2" />
|
||||
Open {{ fiatProviderLabel }} checkout
|
||||
</Button>
|
||||
<div v-if="error" class="text-sm text-destructive bg-destructive/10 p-3 rounded-lg">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-else
|
||||
@click="handlePurchase"
|
||||
:disabled="isLoading || isFiatPending || (paymentMethod === 'lightning' && !canPurchase)"
|
||||
:disabled="isLoading || !canPurchase"
|
||||
class="w-full"
|
||||
>
|
||||
<span v-if="isLoading || isFiatPending" class="animate-spin mr-2">⚡</span>
|
||||
<span v-else-if="paymentMethod === 'fiat'" class="flex items-center gap-2">
|
||||
<CreditCard class="w-4 h-4" />
|
||||
Continue to {{ event.fiat_currency ?? 'fiat' }} checkout
|
||||
</span>
|
||||
<span v-if="isLoading" class="animate-spin mr-2">⚡</span>
|
||||
<span v-else-if="hasWalletWithBalance" class="flex items-center gap-2">
|
||||
<Zap class="w-4 h-4" />
|
||||
Pay with Wallet
|
||||
|
|
|
|||
|
|
@ -98,31 +98,16 @@ export function useTicketPurchase() {
|
|||
currentUser.value!.id,
|
||||
accessToken
|
||||
)
|
||||
|
||||
// Backend now returns either a Lightning invoice or a fiat
|
||||
// checkout URL (post-events-v1.4.0). This composable only knows
|
||||
// how to drive the Lightning path; fiat would need a separate
|
||||
// redirect-to-provider flow that lives in PurchaseTicketDialog
|
||||
// (it has the user-visible payment-method selector). Reject the
|
||||
// fiat response here so callers get a clear error instead of a
|
||||
// silent broken QR.
|
||||
if (invoice.isFiat || !invoice.paymentRequest) {
|
||||
throw new Error(
|
||||
'This event uses fiat checkout. Use the purchase dialog ' +
|
||||
'to follow the provider link.',
|
||||
)
|
||||
}
|
||||
const bolt11: string = invoice.paymentRequest
|
||||
paymentHash.value = invoice.paymentHash
|
||||
paymentRequest.value = bolt11
|
||||
paymentRequest.value = invoice.paymentRequest
|
||||
|
||||
// Generate QR code for payment
|
||||
await generateQRCode(bolt11)
|
||||
await generateQRCode(invoice.paymentRequest)
|
||||
|
||||
// Try to pay with wallet if available
|
||||
if (hasWalletWithBalance.value) {
|
||||
try {
|
||||
await payWithWallet(bolt11)
|
||||
await payWithWallet(invoice.paymentRequest)
|
||||
await startPaymentStatusCheck(eventId, invoice.paymentHash)
|
||||
} catch (walletError) {
|
||||
console.log('Wallet payment failed, falling back to manual payment:', walletError)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import type {
|
||||
ActivityTicket,
|
||||
ActivityTicketExtra,
|
||||
CreateTicketRequest,
|
||||
PaymentMethod,
|
||||
TicketPurchaseInvoice,
|
||||
TicketPaymentStatus,
|
||||
TicketedEvent,
|
||||
|
|
@ -52,38 +49,14 @@ export class TicketApiService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Request a ticket purchase. Returns either a Lightning invoice
|
||||
* (`paymentRequest` = bolt11) or a fiat invoice (`fiatPaymentRequest`
|
||||
* = follow-the-URL string from the configured fiat provider). The
|
||||
* `isFiat` flag is the discriminator.
|
||||
*
|
||||
* `paymentMethod` defaults to "lightning"; pass "fiat" to opt into
|
||||
* the fiat path (requires the event to have `allow_fiat=true`).
|
||||
* `fiatProvider` is optional — backend picks the user's configured
|
||||
* default when omitted.
|
||||
*
|
||||
* Additional ticket metadata (promo code, refund address, nostr
|
||||
* identifier for DM delivery) can be supplied via `options`.
|
||||
* Request a ticket purchase (creates a Lightning invoice).
|
||||
* Uses POST /tickets/{event_id} with user_id in body (upstream API).
|
||||
*/
|
||||
async requestTicket(
|
||||
eventId: string,
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
options: {
|
||||
paymentMethod?: PaymentMethod
|
||||
fiatProvider?: string
|
||||
promoCode?: string
|
||||
refundAddress?: string
|
||||
nostrIdentifier?: string
|
||||
} = {},
|
||||
accessToken: string
|
||||
): Promise<TicketPurchaseInvoice> {
|
||||
const body: CreateTicketRequest = { user_id: userId }
|
||||
if (options.paymentMethod) body.payment_method = options.paymentMethod
|
||||
if (options.fiatProvider) body.fiat_provider = options.fiatProvider
|
||||
if (options.promoCode) body.promo_code = options.promoCode
|
||||
if (options.refundAddress) body.refund_address = options.refundAddress
|
||||
if (options.nostrIdentifier) body.nostr_identifier = options.nostrIdentifier
|
||||
|
||||
const data = await this.request(
|
||||
`/events/api/v1/tickets/${eventId}`,
|
||||
{
|
||||
|
|
@ -92,16 +65,13 @@ export class TicketApiService {
|
|||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
body: JSON.stringify({ user_id: userId }),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
paymentHash: data.payment_hash,
|
||||
paymentRequest: data.payment_request ?? undefined,
|
||||
fiatPaymentRequest: data.fiat_payment_request ?? undefined,
|
||||
fiatProvider: data.fiat_provider ?? undefined,
|
||||
isFiat: Boolean(data.is_fiat),
|
||||
paymentRequest: data.payment_request,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +121,6 @@ export class TicketApiService {
|
|||
paid: t.paid,
|
||||
time: t.time,
|
||||
regTimestamp: t.reg_timestamp,
|
||||
extra: t.extra as ActivityTicketExtra | undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +144,6 @@ export class TicketApiService {
|
|||
paid: t.paid,
|
||||
time: t.time,
|
||||
regTimestamp: t.reg_timestamp,
|
||||
extra: t.extra as ActivityTicketExtra | undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -215,39 +183,6 @@ export class TicketApiService {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend the ticket confirmation email for a paid ticket. Requires
|
||||
* the event's wallet admin key (organizer-only). Returns the updated
|
||||
* Ticket with the `email_notification_sent` flag refreshed.
|
||||
*
|
||||
* Endpoint added upstream in v1.6.1 (PR #51).
|
||||
*/
|
||||
async resendTicketEmail(
|
||||
ticketId: string,
|
||||
adminKey: string,
|
||||
): Promise<ActivityTicket> {
|
||||
const t = await this.request(
|
||||
`/events/api/v1/tickets/${ticketId}/resend-email`,
|
||||
{
|
||||
method: 'POST',
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -1,44 +1,7 @@
|
|||
/**
|
||||
* Database-backed ticket types (via LNbits events extension).
|
||||
*
|
||||
* Wire-format types — names match the snake_case fields the events
|
||||
* extension serves over HTTP. Camel-cased aliases (e.g. ActivityTicket
|
||||
* below) are the webapp-internal view models after adapter conversion.
|
||||
* Database-backed ticket types (via LNbits events extension)
|
||||
*/
|
||||
|
||||
export interface PromoCode {
|
||||
code: string
|
||||
discount_percent: number
|
||||
active: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* EventExtra mirrors the EventExtra Pydantic model in
|
||||
* `events/models.py`. Carries promo codes, conditional-event config,
|
||||
* and the per-event notification toggles + custom subject/body added
|
||||
* in upstream v1.4.0 (PR #50) and v1.6.0.
|
||||
*/
|
||||
export interface EventExtra {
|
||||
promo_codes: PromoCode[]
|
||||
conditional: boolean
|
||||
min_tickets: number
|
||||
email_notifications: boolean
|
||||
nostr_notifications: boolean
|
||||
notification_subject: string
|
||||
notification_body: string
|
||||
}
|
||||
|
||||
export interface ActivityTicketExtra {
|
||||
applied_promo_code?: string | null
|
||||
sats_paid?: number | null
|
||||
refund_address?: string | null
|
||||
nostr_identifier?: string | null
|
||||
ticket_base_url?: string | null
|
||||
email_notification_sent: boolean
|
||||
nostr_notification_sent: boolean
|
||||
refunded: boolean
|
||||
}
|
||||
|
||||
export interface ActivityTicket {
|
||||
id: string
|
||||
wallet: string
|
||||
|
|
@ -58,40 +21,19 @@ export interface ActivityTicket {
|
|||
time: string
|
||||
/** Registration/scan timestamp */
|
||||
regTimestamp: string
|
||||
/** Optional metadata — promo code applied, sats paid, notification
|
||||
* delivery flags, refund state. May be absent on older tickets. */
|
||||
extra?: ActivityTicketExtra
|
||||
}
|
||||
|
||||
export type TicketStatus = 'pending' | 'paid' | 'registered' | 'cancelled'
|
||||
|
||||
export type PaymentMethod = 'lightning' | 'fiat'
|
||||
|
||||
export interface TicketPurchaseRequest {
|
||||
activityId: string
|
||||
userId: string
|
||||
accessToken: string
|
||||
/** Lightning (default) or fiat. Only meaningful if the event has
|
||||
* `allow_fiat=true` on the backend; otherwise the backend coerces
|
||||
* to lightning. */
|
||||
paymentMethod?: PaymentMethod
|
||||
/** Specific fiat provider id (e.g. "stripe"). Backend picks the
|
||||
* user's default if omitted. */
|
||||
fiatProvider?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Server response from `POST /tickets/{event_id}`. Either Lightning
|
||||
* (`paymentRequest` = bolt11) or fiat (`fiatPaymentRequest` = a URL
|
||||
* the buyer follows to complete payment with `fiatProvider`).
|
||||
* `isFiat` is the discriminator.
|
||||
*/
|
||||
export interface TicketPurchaseInvoice {
|
||||
paymentHash: string
|
||||
paymentRequest?: string
|
||||
fiatPaymentRequest?: string
|
||||
fiatProvider?: string
|
||||
isFiat: boolean
|
||||
paymentRequest: string
|
||||
}
|
||||
|
||||
export interface TicketPaymentStatus {
|
||||
|
|
@ -116,10 +58,6 @@ export interface TicketedEvent {
|
|||
event_start_date: string
|
||||
event_end_date: string | null
|
||||
currency: string
|
||||
/** Whether the event accepts fiat payments. Upstream v1.4.0+. */
|
||||
allow_fiat: boolean
|
||||
/** Fiat currency code (ISO 4217). Defaults to "GBP" upstream. */
|
||||
fiat_currency: string
|
||||
amount_tickets: number
|
||||
price_per_ticket: number
|
||||
time: string
|
||||
|
|
@ -127,7 +65,6 @@ export interface TicketedEvent {
|
|||
banner: string | null
|
||||
location: string | null
|
||||
categories: string[]
|
||||
extra: EventExtra
|
||||
status: string
|
||||
}
|
||||
|
||||
|
|
@ -139,34 +76,9 @@ export interface CreateEventRequest {
|
|||
event_start_date: string
|
||||
event_end_date?: string
|
||||
currency?: string
|
||||
allow_fiat?: boolean
|
||||
fiat_currency?: string
|
||||
amount_tickets?: number
|
||||
price_per_ticket?: number
|
||||
banner?: string | null
|
||||
location?: string | null
|
||||
categories?: string[]
|
||||
/** Optional — notification toggles + custom subject/body, promo
|
||||
* codes, conditional-event config. Backend defaults to a fresh
|
||||
* EventExtra if omitted. */
|
||||
extra?: Partial<EventExtra>
|
||||
}
|
||||
|
||||
/**
|
||||
* Body for `POST /tickets/{event_id}`. Either `user_id` OR the
|
||||
* `name`+`email` pair is required (backend root_validator enforces
|
||||
* mutual exclusion). `nostr_identifier` opts the buyer into Nostr DM
|
||||
* delivery when the event has nostr_notifications enabled. The
|
||||
* `payment_method` + `fiat_provider` pair selects between Lightning
|
||||
* and fiat checkout.
|
||||
*/
|
||||
export interface CreateTicketRequest {
|
||||
name?: string
|
||||
email?: string
|
||||
user_id?: string
|
||||
promo_code?: string
|
||||
refund_address?: string
|
||||
nostr_identifier?: string
|
||||
payment_method?: PaymentMethod
|
||||
fiat_provider?: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@ const selectedEvent = ref<{
|
|||
name: string
|
||||
price_per_ticket: number
|
||||
currency: string
|
||||
allow_fiat?: boolean
|
||||
fiat_currency?: string
|
||||
} | null>(null)
|
||||
|
||||
const showEventDialog = ref(false)
|
||||
|
|
@ -58,8 +56,6 @@ function handlePurchaseClick(event: {
|
|||
name: string
|
||||
price_per_ticket: number
|
||||
currency: string
|
||||
allow_fiat?: boolean
|
||||
fiat_currency?: string
|
||||
}) {
|
||||
if (!isAuthenticated.value) return
|
||||
selectedEvent.value = event
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue