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()) const subscriptionId = ref(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 } }