web-app/src/composables/useOrderEvents.ts

314 lines
9.3 KiB
TypeScript

import { ref, computed } from 'vue'
import { nip04 } from 'nostr-tools'
import { relayHubComposable } from './useRelayHub'
import { useAuth } from './useAuth'
import { useMarketStore } from '@/stores/market'
import { decode } from 'light-bolt11-decoder'
// Nostrmarket Order interfaces based on the actual implementation
// Nostrmarket Order interfaces based on the actual implementation
interface OrderItem {
product_id: string
quantity: number
}
interface OrderContact {
nostr?: string
phone?: string
email?: string
}
// Direct message types from nostrmarket
enum DirectMessageType {
PLAIN_TEXT = -1,
CUSTOMER_ORDER = 0,
PAYMENT_REQUEST = 1,
ORDER_PAID_OR_SHIPPED = 2
}
// Event types for nostrmarket protocol
interface CustomerOrderEvent {
type: DirectMessageType.CUSTOMER_ORDER
id: string
items: OrderItem[]
contact?: OrderContact
shipping_id: string
message?: string
}
interface PaymentRequestEvent {
type: DirectMessageType.PAYMENT_REQUEST
id: string
message?: string
payment_options: Array<{
type: string
link: string
}>
}
interface OrderStatusEvent {
type: DirectMessageType.ORDER_PAID_OR_SHIPPED
id: string
message?: string
paid?: boolean
shipped?: boolean
}
// Helper function to extract expiry from bolt11 invoice
function extractExpiryFromBolt11(bolt11String: string): string | undefined {
try {
const decoded = decode(bolt11String)
console.log('Decoded bolt11 invoice:', {
amount: decoded.sections.find(section => section.name === 'amount')?.value,
expiry: decoded.expiry,
timestamp: decoded.sections.find(section => section.name === 'timestamp')?.value
})
// Calculate expiry date from timestamp + expiry seconds
const timestamp = decoded.sections.find(section => section.name === 'timestamp')?.value as number
const expirySeconds = decoded.expiry as number
if (timestamp && expirySeconds) {
const expiryDate = new Date((timestamp + expirySeconds) * 1000)
return expiryDate.toISOString()
}
return undefined
} catch (error) {
console.warn('Failed to extract expiry from bolt11:', error)
return undefined
}
}
export function useOrderEvents() {
const relayHub = relayHubComposable
const auth = useAuth()
const marketStore = useMarketStore()
// State
const isSubscribed = ref(false)
const lastEventTimestamp = ref(0)
const processedEventIds = ref(new Set<string>())
const subscriptionId = ref<string | null>(null)
// Computed
const currentUserPubkey = computed(() => auth.currentUser?.value?.pubkey)
const isReady = computed(() => {
const isAuth = auth.isAuthenticated
const isConnected = relayHub.isConnected.value
const hasPubkey = !!currentUserPubkey.value
return isAuth && isConnected && hasPubkey
})
// Subscribe to order events
const subscribeToOrderEvents = async () => {
if (!isReady.value || isSubscribed.value) {
return
}
try {
// Subscribe to direct messages (kind 4) that contain order information
const filters = [
{
kinds: [4], // NIP-04 encrypted direct messages
'#p': [currentUserPubkey.value].filter(Boolean) as string[],
since: lastEventTimestamp.value
}
]
relayHub.subscribe({
id: 'order-events',
filters,
onEvent: handleOrderEvent,
onEose: () => {
console.log('Order events subscription ended')
}
})
subscriptionId.value = 'order-events'
isSubscribed.value = true
console.log('Successfully subscribed to order events')
} catch (error) {
console.error('Failed to subscribe to order events:', error)
}
}
// Handle incoming order events
const handleOrderEvent = async (event: any) => {
if (processedEventIds.value.has(event.id)) {
return
}
processedEventIds.value.add(event.id)
lastEventTimestamp.value = Math.max(lastEventTimestamp.value, event.created_at)
try {
// Decrypt the message content
const decryptedContent = await nip04.decrypt(
auth.currentUser.value?.prvkey || '',
event.pubkey,
event.content
)
// Parse the JSON content
const jsonData = JSON.parse(decryptedContent)
// Handle different message types
switch (jsonData.type) {
case DirectMessageType.CUSTOMER_ORDER:
await handleCustomerOrder(jsonData as CustomerOrderEvent, event.pubkey)
break
case DirectMessageType.PAYMENT_REQUEST:
await handlePaymentRequest(jsonData as PaymentRequestEvent, event.pubkey)
break
case DirectMessageType.ORDER_PAID_OR_SHIPPED:
await handleOrderStatusUpdate(jsonData as OrderStatusEvent, event.pubkey)
break
default:
console.log('Unknown message type:', jsonData.type)
}
} catch (error) {
console.error('Error processing order event:', error)
}
}
// Handle customer order (type 0)
const handleCustomerOrder = async (orderData: CustomerOrderEvent, _senderPubkey: string) => {
console.log('Received customer order:', orderData)
// Create a basic order object from the event data
const order = {
id: orderData.id,
type: DirectMessageType.CUSTOMER_ORDER,
items: orderData.items,
contact: orderData.contact,
shipping_id: orderData.shipping_id,
message: orderData.message,
createdAt: Date.now(),
updatedAt: Date.now()
}
// Store the order in our local state
// Note: We're not using the complex Order interface from market store
// Instead, we're using the simple nostrmarket format
console.log('Processed customer order:', order)
}
// Handle payment request (type 1)
const handlePaymentRequest = async (paymentData: PaymentRequestEvent, _senderPubkey: string) => {
console.log('Received payment request:', paymentData)
// Find the lightning payment option
const lightningOption = paymentData.payment_options?.find(opt => opt.type === 'ln')
if (lightningOption) {
console.log('Lightning payment request:', lightningOption.link)
// Find the existing order by ID
const existingOrder = marketStore.orders[paymentData.id]
if (existingOrder) {
console.log('Found existing order, updating with payment request:', existingOrder.id)
// Try to extract actual expiry from bolt11
const actualExpiry = extractExpiryFromBolt11(lightningOption.link)
// Create lightning invoice object
const lightningInvoice = {
checking_id: '', // Will be extracted from bolt11 if needed
payment_hash: '', // Will be extracted from bolt11 if needed
wallet_id: '', // Not available from payment request
amount: existingOrder.total,
fee: 0, // Not available from payment request
bolt11: lightningOption.link,
status: 'pending',
memo: paymentData.message || 'Payment for order',
created_at: new Date().toISOString(),
expiry: actualExpiry // Use actual expiry from bolt11 decoding
}
// Update the order with the lightning invoice
marketStore.updateOrder(existingOrder.id, {
lightningInvoice,
status: 'pending',
paymentRequest: lightningOption.link,
updatedAt: Date.now()
})
console.log('Order updated with payment request:', existingOrder.id)
} else {
console.warn('Order not found for payment request:', paymentData.id)
}
}
}
// Handle order status update (type 2)
const handleOrderStatusUpdate = async (statusData: OrderStatusEvent, _senderPubkey: string) => {
console.log('Received order status update:', statusData)
// Update order status in local state
if (statusData.paid !== undefined) {
console.log(`Order ${statusData.id} payment status: ${statusData.paid}`)
}
if (statusData.shipped !== undefined) {
console.log(`Order ${statusData.id} shipping status: ${statusData.shipped}`)
}
}
// Unsubscribe from order events
const unsubscribeFromOrderEvents = () => {
if (subscriptionId.value) {
relayHub.cleanup()
subscriptionId.value = null
}
isSubscribed.value = false
console.log('Unsubscribed from order events')
}
// Watch for ready state changes
const watchReadyState = () => {
if (isReady.value && !isSubscribed.value) {
subscribeToOrderEvents()
} else if (!isReady.value && isSubscribed.value) {
unsubscribeFromOrderEvents()
}
}
// Watch for authentication changes
const watchAuthChanges = () => {
if (auth.isAuthenticated && relayHub.isConnected.value) {
subscribeToOrderEvents()
} else {
unsubscribeFromOrderEvents()
}
}
// Initialize subscription when ready
const initialize = () => {
if (isReady.value) {
subscribeToOrderEvents()
}
}
// Cleanup
const cleanup = () => {
unsubscribeFromOrderEvents()
processedEventIds.value.clear()
}
return {
// State
isSubscribed: computed(() => isSubscribed.value),
isReady: computed(() => isReady.value),
lastEventTimestamp: computed(() => lastEventTimestamp.value),
// Methods
subscribeToOrderEvents,
unsubscribeFromOrderEvents,
initialize,
cleanup,
watchReadyState,
watchAuthChanges
}
}