Enhance market module with new chat and events features
- Introduce chat module with components, services, and composables for real-time messaging. - Implement events module with API service, components, and ticket purchasing functionality. - Update app configuration to include new modules and their respective settings. - Refactor existing components to integrate with the new chat and events features. - Enhance market store and services to support new functionalities and improve order management. - Update routing to accommodate new views for chat and events, ensuring seamless navigation.
This commit is contained in:
parent
519a9003d4
commit
e40ac91417
46 changed files with 6305 additions and 3264 deletions
|
|
@ -1,460 +1,3 @@
|
|||
import { finalizeEvent, type EventTemplate, nip04 } from 'nostr-tools'
|
||||
import { relayHub } from '@/lib/nostr/relayHub'
|
||||
import { auth } from '@/composables/useAuth'
|
||||
import type { Stall, Product, Order } from '@/stores/market'
|
||||
|
||||
export interface NostrmarketStall {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
currency: string
|
||||
shipping: Array<{
|
||||
id: string
|
||||
name: string
|
||||
cost: number
|
||||
countries: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export interface NostrmarketProduct {
|
||||
id: string
|
||||
stall_id: string
|
||||
name: string
|
||||
description?: string
|
||||
images: string[]
|
||||
categories: string[]
|
||||
price: number
|
||||
quantity: number
|
||||
currency: string
|
||||
}
|
||||
|
||||
export interface NostrmarketOrder {
|
||||
id: string
|
||||
items: Array<{
|
||||
product_id: string
|
||||
quantity: number
|
||||
}>
|
||||
contact: {
|
||||
name: string
|
||||
email?: string
|
||||
phone?: string
|
||||
}
|
||||
address?: {
|
||||
street: string
|
||||
city: string
|
||||
state: string
|
||||
country: string
|
||||
postal_code: string
|
||||
}
|
||||
shipping_id: string
|
||||
}
|
||||
|
||||
export interface NostrmarketPaymentRequest {
|
||||
type: 1
|
||||
id: string
|
||||
message?: string
|
||||
payment_options: Array<{
|
||||
type: string
|
||||
link: string
|
||||
}>
|
||||
}
|
||||
|
||||
export interface NostrmarketOrderStatus {
|
||||
type: 2
|
||||
id: string
|
||||
message?: string
|
||||
paid?: boolean
|
||||
shipped?: boolean
|
||||
}
|
||||
|
||||
export class NostrmarketService {
|
||||
/**
|
||||
* Convert hex string to Uint8Array (browser-compatible)
|
||||
*/
|
||||
private hexToUint8Array(hex: string): Uint8Array {
|
||||
const bytes = new Uint8Array(hex.length / 2)
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
private getAuth() {
|
||||
if (!auth.isAuthenticated.value || !auth.currentUser.value?.prvkey) {
|
||||
throw new Error('User not authenticated or private key not available')
|
||||
}
|
||||
|
||||
const pubkey = auth.currentUser.value.pubkey
|
||||
const prvkey = auth.currentUser.value.prvkey
|
||||
|
||||
if (!pubkey || !prvkey) {
|
||||
throw new Error('Public key or private key is missing')
|
||||
}
|
||||
|
||||
// Validate that we have proper hex strings
|
||||
if (!/^[0-9a-fA-F]{64}$/.test(pubkey)) {
|
||||
throw new Error(`Invalid public key format: ${pubkey.substring(0, 10)}...`)
|
||||
}
|
||||
|
||||
if (!/^[0-9a-fA-F]{64}$/.test(prvkey)) {
|
||||
throw new Error(`Invalid private key format: ${prvkey.substring(0, 10)}...`)
|
||||
}
|
||||
|
||||
console.log('🔑 Key debug:', {
|
||||
pubkey: pubkey.substring(0, 10) + '...',
|
||||
prvkey: prvkey.substring(0, 10) + '...',
|
||||
pubkeyIsHex: /^[0-9a-fA-F]{64}$/.test(pubkey),
|
||||
prvkeyIsHex: /^[0-9a-fA-F]{64}$/.test(prvkey),
|
||||
pubkeyLength: pubkey.length,
|
||||
prvkeyLength: prvkey.length,
|
||||
pubkeyType: typeof pubkey,
|
||||
prvkeyType: typeof prvkey,
|
||||
pubkeyIsString: typeof pubkey === 'string',
|
||||
prvkeyIsString: typeof prvkey === 'string'
|
||||
})
|
||||
|
||||
return {
|
||||
pubkey,
|
||||
prvkey
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a stall event (kind 30017) to Nostr
|
||||
*/
|
||||
async publishStall(stall: Stall): Promise<string> {
|
||||
const { prvkey } = this.getAuth()
|
||||
|
||||
const stallData: NostrmarketStall = {
|
||||
id: stall.id,
|
||||
name: stall.name,
|
||||
description: stall.description,
|
||||
currency: stall.currency,
|
||||
shipping: (stall.shipping || []).map(zone => ({
|
||||
id: zone.id,
|
||||
name: zone.name,
|
||||
cost: zone.cost,
|
||||
countries: []
|
||||
}))
|
||||
}
|
||||
|
||||
const eventTemplate: EventTemplate = {
|
||||
kind: 30017,
|
||||
tags: [
|
||||
['t', 'stall'],
|
||||
['t', 'nostrmarket']
|
||||
],
|
||||
content: JSON.stringify(stallData),
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
const prvkeyBytes = this.hexToUint8Array(prvkey)
|
||||
const event = finalizeEvent(eventTemplate, prvkeyBytes)
|
||||
const result = await relayHub.publishEvent(event)
|
||||
|
||||
console.log('Stall published to nostrmarket:', {
|
||||
stallId: stall.id,
|
||||
eventId: result,
|
||||
content: stallData
|
||||
})
|
||||
|
||||
return result.success.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a product event (kind 30018) to Nostr
|
||||
*/
|
||||
async publishProduct(product: Product): Promise<string> {
|
||||
const { prvkey } = this.getAuth()
|
||||
|
||||
const productData: NostrmarketProduct = {
|
||||
id: product.id,
|
||||
stall_id: product.stall_id,
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
images: product.images || [],
|
||||
categories: product.categories || [],
|
||||
price: product.price,
|
||||
quantity: product.quantity,
|
||||
currency: product.currency
|
||||
}
|
||||
|
||||
const eventTemplate: EventTemplate = {
|
||||
kind: 30018,
|
||||
tags: [
|
||||
['t', 'product'],
|
||||
['t', 'nostrmarket'],
|
||||
['t', 'stall', product.stall_id],
|
||||
...(product.categories || []).map(cat => ['t', cat])
|
||||
],
|
||||
content: JSON.stringify(productData),
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
const prvkeyBytes = this.hexToUint8Array(prvkey)
|
||||
const event = finalizeEvent(eventTemplate, prvkeyBytes)
|
||||
const result = await relayHub.publishEvent(event)
|
||||
|
||||
console.log('Product published to nostrmarket:', {
|
||||
productId: product.id,
|
||||
eventId: result,
|
||||
content: productData
|
||||
})
|
||||
|
||||
return result.success.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an order event (kind 4 encrypted DM) to nostrmarket
|
||||
*/
|
||||
async publishOrder(order: Order, merchantPubkey: string): Promise<string> {
|
||||
const { prvkey } = this.getAuth()
|
||||
|
||||
// Convert order to nostrmarket format - exactly matching the specification
|
||||
const orderData = {
|
||||
type: 0, // DirectMessageType.CUSTOMER_ORDER
|
||||
id: order.id,
|
||||
items: order.items.map(item => ({
|
||||
product_id: item.productId,
|
||||
quantity: item.quantity
|
||||
})),
|
||||
contact: {
|
||||
name: order.contactInfo?.message || order.contactInfo?.email || 'Unknown',
|
||||
email: order.contactInfo?.email || ''
|
||||
// Remove phone field - not in nostrmarket specification
|
||||
},
|
||||
// Only include address if it's a physical good and address is provided
|
||||
...(order.shippingZone?.requiresPhysicalShipping && order.contactInfo?.address ? {
|
||||
address: order.contactInfo.address
|
||||
} : {}),
|
||||
shipping_id: order.shippingZone?.id || 'online'
|
||||
}
|
||||
|
||||
// Encrypt the message using NIP-04
|
||||
console.log('🔐 NIP-04 encryption debug:', {
|
||||
prvkeyType: typeof prvkey,
|
||||
prvkeyIsString: typeof prvkey === 'string',
|
||||
prvkeyLength: prvkey.length,
|
||||
prvkeySample: prvkey.substring(0, 10) + '...',
|
||||
merchantPubkeyType: typeof merchantPubkey,
|
||||
merchantPubkeyLength: merchantPubkey.length,
|
||||
orderDataString: JSON.stringify(orderData).substring(0, 50) + '...'
|
||||
})
|
||||
|
||||
let encryptedContent: string
|
||||
try {
|
||||
encryptedContent = await nip04.encrypt(prvkey, merchantPubkey, JSON.stringify(orderData))
|
||||
console.log('🔐 NIP-04 encryption successful:', {
|
||||
encryptedContentLength: encryptedContent.length,
|
||||
encryptedContentSample: encryptedContent.substring(0, 50) + '...'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('🔐 NIP-04 encryption failed:', error)
|
||||
throw error
|
||||
}
|
||||
|
||||
const eventTemplate: EventTemplate = {
|
||||
kind: 4, // Encrypted DM
|
||||
tags: [['p', merchantPubkey]], // Recipient (merchant)
|
||||
content: encryptedContent, // Use encrypted content
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
console.log('🔧 finalizeEvent debug:', {
|
||||
prvkeyType: typeof prvkey,
|
||||
prvkeyIsString: typeof prvkey === 'string',
|
||||
prvkeyLength: prvkey.length,
|
||||
prvkeySample: prvkey.substring(0, 10) + '...',
|
||||
encodedPrvkeyType: typeof new TextEncoder().encode(prvkey),
|
||||
encodedPrvkeyLength: new TextEncoder().encode(prvkey).length,
|
||||
eventTemplate
|
||||
})
|
||||
|
||||
// Convert hex string to Uint8Array properly
|
||||
const prvkeyBytes = this.hexToUint8Array(prvkey)
|
||||
console.log('🔧 prvkeyBytes debug:', {
|
||||
prvkeyBytesType: typeof prvkeyBytes,
|
||||
prvkeyBytesLength: prvkeyBytes.length,
|
||||
prvkeyBytesIsUint8Array: prvkeyBytes instanceof Uint8Array
|
||||
})
|
||||
|
||||
const event = finalizeEvent(eventTemplate, prvkeyBytes)
|
||||
const result = await relayHub.publishEvent(event)
|
||||
|
||||
console.log('Order published to nostrmarket:', {
|
||||
orderId: order.id,
|
||||
eventId: result,
|
||||
merchantPubkey,
|
||||
content: orderData,
|
||||
encryptedContent: encryptedContent.substring(0, 50) + '...'
|
||||
})
|
||||
|
||||
return result.success.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming payment request from merchant (type 1)
|
||||
*/
|
||||
async handlePaymentRequest(paymentRequest: NostrmarketPaymentRequest): Promise<void> {
|
||||
console.log('Received payment request from merchant:', {
|
||||
orderId: paymentRequest.id,
|
||||
message: paymentRequest.message,
|
||||
paymentOptions: paymentRequest.payment_options
|
||||
})
|
||||
|
||||
// Find the Lightning payment option
|
||||
const lightningOption = paymentRequest.payment_options.find(option => option.type === 'ln')
|
||||
if (!lightningOption) {
|
||||
console.error('No Lightning payment option found in payment request')
|
||||
return
|
||||
}
|
||||
|
||||
// Update the order in the store with payment request
|
||||
const { useMarketStore } = await import('@/stores/market')
|
||||
const marketStore = useMarketStore()
|
||||
|
||||
const order = Object.values(marketStore.orders).find(o =>
|
||||
o.id === paymentRequest.id || o.originalOrderId === paymentRequest.id
|
||||
)
|
||||
|
||||
if (order) {
|
||||
// Update order with payment request details
|
||||
const updatedOrder = {
|
||||
...order,
|
||||
paymentRequest: lightningOption.link,
|
||||
paymentStatus: 'pending' as const,
|
||||
status: 'pending' as const, // Ensure status is pending for payment
|
||||
updatedAt: Math.floor(Date.now() / 1000),
|
||||
items: [...order.items] // Convert readonly to mutable
|
||||
}
|
||||
|
||||
// Generate QR code for the payment request
|
||||
try {
|
||||
const QRCode = await import('qrcode')
|
||||
const qrCodeDataUrl = await QRCode.toDataURL(lightningOption.link, {
|
||||
width: 256,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF'
|
||||
}
|
||||
})
|
||||
updatedOrder.qrCodeDataUrl = qrCodeDataUrl
|
||||
updatedOrder.qrCodeLoading = false
|
||||
updatedOrder.qrCodeError = null
|
||||
} catch (error) {
|
||||
console.error('Failed to generate QR code:', error)
|
||||
updatedOrder.qrCodeError = 'Failed to generate QR code'
|
||||
updatedOrder.qrCodeLoading = false
|
||||
}
|
||||
|
||||
marketStore.updateOrder(order.id, updatedOrder)
|
||||
|
||||
console.log('Order updated with payment request:', {
|
||||
orderId: paymentRequest.id,
|
||||
paymentRequest: lightningOption.link.substring(0, 50) + '...',
|
||||
status: updatedOrder.status,
|
||||
paymentStatus: updatedOrder.paymentStatus,
|
||||
hasQRCode: !!updatedOrder.qrCodeDataUrl
|
||||
})
|
||||
} else {
|
||||
console.warn('Payment request received for unknown order:', paymentRequest.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming order status update from merchant (type 2)
|
||||
*/
|
||||
async handleOrderStatusUpdate(statusUpdate: NostrmarketOrderStatus): Promise<void> {
|
||||
console.log('Received order status update from merchant:', {
|
||||
orderId: statusUpdate.id,
|
||||
message: statusUpdate.message,
|
||||
paid: statusUpdate.paid,
|
||||
shipped: statusUpdate.shipped
|
||||
})
|
||||
|
||||
const { useMarketStore } = await import('@/stores/market')
|
||||
const marketStore = useMarketStore()
|
||||
|
||||
const order = Object.values(marketStore.orders).find(o =>
|
||||
o.id === statusUpdate.id || o.originalOrderId === statusUpdate.id
|
||||
)
|
||||
|
||||
if (order) {
|
||||
// Update order status
|
||||
if (statusUpdate.paid !== undefined) {
|
||||
const newStatus = statusUpdate.paid ? 'paid' : 'pending'
|
||||
marketStore.updateOrderStatus(order.id, newStatus)
|
||||
|
||||
// Also update payment status
|
||||
const updatedOrder = {
|
||||
...order,
|
||||
paymentStatus: (statusUpdate.paid ? 'paid' : 'pending') as 'paid' | 'pending' | 'expired',
|
||||
paidAt: statusUpdate.paid ? Math.floor(Date.now() / 1000) : undefined,
|
||||
updatedAt: Math.floor(Date.now() / 1000),
|
||||
items: [...order.items] // Convert readonly to mutable
|
||||
}
|
||||
marketStore.updateOrder(order.id, updatedOrder)
|
||||
}
|
||||
|
||||
if (statusUpdate.shipped !== undefined) {
|
||||
// Update shipping status if you have that field
|
||||
const updatedOrder = {
|
||||
...order,
|
||||
shipped: statusUpdate.shipped,
|
||||
status: statusUpdate.shipped ? 'shipped' : order.status,
|
||||
updatedAt: Math.floor(Date.now() / 1000),
|
||||
items: [...order.items] // Convert readonly to mutable
|
||||
}
|
||||
marketStore.updateOrder(order.id, updatedOrder)
|
||||
}
|
||||
|
||||
console.log('Order status updated:', {
|
||||
orderId: statusUpdate.id,
|
||||
paid: statusUpdate.paid,
|
||||
shipped: statusUpdate.shipped,
|
||||
newStatus: statusUpdate.paid ? 'paid' : 'pending'
|
||||
})
|
||||
} else {
|
||||
console.warn('Status update received for unknown order:', statusUpdate.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish all stalls and products for a merchant
|
||||
*/
|
||||
async publishMerchantCatalog(stalls: Stall[], products: Product[]): Promise<{
|
||||
stalls: Record<string, string>, // stallId -> eventId
|
||||
products: Record<string, string> // productId -> eventId
|
||||
}> {
|
||||
const results = {
|
||||
stalls: {} as Record<string, string>,
|
||||
products: {} as Record<string, string>
|
||||
}
|
||||
|
||||
// Publish stalls first
|
||||
for (const stall of stalls) {
|
||||
try {
|
||||
const eventId = await this.publishStall(stall)
|
||||
results.stalls[stall.id] = eventId
|
||||
} catch (error) {
|
||||
console.error(`Failed to publish stall ${stall.id}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// Publish products
|
||||
for (const product of products) {
|
||||
try {
|
||||
const eventId = await this.publishProduct(product)
|
||||
results.products[product.id] = eventId
|
||||
} catch (error) {
|
||||
console.error(`Failed to publish product ${product.id}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const nostrmarketService = new NostrmarketService()
|
||||
// Compatibility re-export for the moved nostrmarketService
|
||||
export * from '@/modules/market/services/nostrmarketService'
|
||||
export { nostrmarketService } from '@/modules/market/services/nostrmarketService'
|
||||
Loading…
Add table
Add a link
Reference in a new issue