Squash merge remove-dangling-bits into market-implementation-squashed
This commit is contained in:
parent
4bc15cfa2f
commit
2f0024478d
17 changed files with 569 additions and 859 deletions
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -20,6 +20,7 @@
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
|
"light-bolt11-decoder": "^3.2.0",
|
||||||
"lucide-vue-next": "^0.474.0",
|
"lucide-vue-next": "^0.474.0",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.10.4",
|
||||||
"pinia": "^2.3.1",
|
"pinia": "^2.3.1",
|
||||||
|
|
@ -9782,6 +9783,15 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/light-bolt11-decoder": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@scure/base": "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.29.1",
|
"version": "1.29.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
|
"light-bolt11-decoder": "^3.2.0",
|
||||||
"lucide-vue-next": "^0.474.0",
|
"lucide-vue-next": "^0.474.0",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.10.4",
|
||||||
"pinia": "^2.3.1",
|
"pinia": "^2.3.1",
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
<h5>Stalls Published:</h5>
|
<h5>Stalls Published:</h5>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(eventId, stallId) in lastResult.stalls" :key="stallId">
|
<li v-for="(eventId, stallId) in lastResult.stalls" :key="stallId">
|
||||||
{{ getStallName(stallId) }}: {{ eventId }}
|
{{ getStallName(String(stallId)) }}: {{ eventId }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
<h5>Products Published:</h5>
|
<h5>Products Published:</h5>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(eventId, productId) in lastResult.products" :key="productId">
|
<li v-for="(eventId, productId) in lastResult.products" :key="productId">
|
||||||
{{ getProductName(productId) }}: {{ eventId }}
|
{{ getProductName(String(productId)) }}: {{ eventId }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -72,7 +72,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useMarketStore } from '@/stores/market'
|
import { useMarketStore } from '@/stores/market'
|
||||||
import { nostrmarketService } from '@/lib/services/nostrmarketService'
|
|
||||||
|
|
||||||
const marketStore = useMarketStore()
|
const marketStore = useMarketStore()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ import { useRouter } from 'vue-router'
|
||||||
import { useMarketStore } from '@/stores/market'
|
import { useMarketStore } from '@/stores/market'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { useMarket } from '@/composables/useMarket'
|
import { useMarket } from '@/composables/useMarket'
|
||||||
import { orderEvents } from '@/composables/useOrderEvents'
|
import { useOrderEvents } from '@/composables/useOrderEvents'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import {
|
import {
|
||||||
|
|
@ -236,6 +236,7 @@ const router = useRouter()
|
||||||
const marketStore = useMarketStore()
|
const marketStore = useMarketStore()
|
||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
const { isConnected } = useMarket()
|
const { isConnected } = useMarket()
|
||||||
|
const orderEvents = useOrderEvents()
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
const orderStats = computed(() => {
|
const orderStats = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -202,12 +202,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
// import { useMarketStore } from '@/stores/market'
|
// import { useMarketStore } from '@/stores/market'
|
||||||
import { orderEvents } from '@/composables/useOrderEvents'
|
import { useOrderEvents } from '@/composables/useOrderEvents'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Plus, X } from 'lucide-vue-next'
|
import { Plus, X } from 'lucide-vue-next'
|
||||||
|
|
||||||
// const marketStore = useMarketStore()
|
// const marketStore = useMarketStore()
|
||||||
|
const orderEvents = useOrderEvents()
|
||||||
|
|
||||||
// Local state
|
// Local state
|
||||||
const activeSettingsTab = ref('store')
|
const activeSettingsTab = ref('store')
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,7 @@
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<!-- Order Events Status -->
|
<!-- Order Events Status -->
|
||||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<div
|
<div class="w-2 h-2 rounded-full" :class="orderEvents.isSubscribed ? 'bg-green-500' : 'bg-yellow-500'"></div>
|
||||||
class="w-2 h-2 rounded-full"
|
|
||||||
:class="orderEvents.isSubscribed ? 'bg-green-500' : 'bg-yellow-500'"
|
|
||||||
></div>
|
|
||||||
<span>{{ orderEvents.isSubscribed ? 'Live updates' : 'Connecting...' }}</span>
|
<span>{{ orderEvents.isSubscribed ? 'Live updates' : 'Connecting...' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<Button @click="navigateToMarket" variant="outline">
|
<Button @click="navigateToMarket" variant="outline">
|
||||||
|
|
@ -32,7 +29,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-muted-foreground">Pending:</span>
|
<span class="text-muted-foreground">Pending:</span>
|
||||||
<Badge variant="outline" class="text-yellow-600">{{ pendingOrders }}</Badge>
|
<Badge variant="outline" class="text-amber-600">{{ pendingOrders }}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-muted-foreground">Paid:</span>
|
<span class="text-muted-foreground">Paid:</span>
|
||||||
|
|
@ -46,7 +43,8 @@
|
||||||
|
|
||||||
<!-- Filter Controls -->
|
<!-- Filter Controls -->
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<select v-model="statusFilter" class="px-3 py-2 border border-input rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring bg-background text-foreground">
|
<select v-model="statusFilter"
|
||||||
|
class="px-3 py-2 border border-input rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring bg-background text-foreground">
|
||||||
<option value="">All Statuses</option>
|
<option value="">All Statuses</option>
|
||||||
<option value="pending">Pending</option>
|
<option value="pending">Pending</option>
|
||||||
<option value="paid">Paid</option>
|
<option value="paid">Paid</option>
|
||||||
|
|
@ -55,7 +53,8 @@
|
||||||
<option value="delivered">Delivered</option>
|
<option value="delivered">Delivered</option>
|
||||||
<option value="cancelled">Cancelled</option>
|
<option value="cancelled">Cancelled</option>
|
||||||
</select>
|
</select>
|
||||||
<select v-model="sortBy" class="px-3 py-2 border border-input rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring bg-background text-foreground">
|
<select v-model="sortBy"
|
||||||
|
class="px-3 py-2 border border-input rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring bg-background text-foreground">
|
||||||
<option value="createdAt">Date Created</option>
|
<option value="createdAt">Date Created</option>
|
||||||
<option value="total">Order Total</option>
|
<option value="total">Order Total</option>
|
||||||
<option value="status">Status</option>
|
<option value="status">Status</option>
|
||||||
|
|
@ -65,11 +64,8 @@
|
||||||
|
|
||||||
<!-- Orders List -->
|
<!-- Orders List -->
|
||||||
<div v-if="filteredOrders.length > 0" class="space-y-4">
|
<div v-if="filteredOrders.length > 0" class="space-y-4">
|
||||||
<div
|
<div v-for="order in sortedOrders" :key="order.id"
|
||||||
v-for="order in sortedOrders"
|
class="bg-card border rounded-lg p-6 hover:shadow-md transition-shadow">
|
||||||
:key="order.id"
|
|
||||||
class="bg-card border rounded-lg p-6 hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<!-- Order Header -->
|
<!-- Order Header -->
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
|
@ -81,57 +77,26 @@
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
{{ formatDate(order.createdAt) }}
|
{{ formatDate(order.createdAt) }}
|
||||||
</p>
|
</p>
|
||||||
<!-- Nostr Status -->
|
|
||||||
<div v-if="order.sentViaNostr !== undefined" class="flex items-center gap-2 mt-1">
|
|
||||||
<div v-if="order.sentViaNostr" class="flex items-center gap-1 text-xs text-green-600">
|
|
||||||
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
|
||||||
Sent via Nostr
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex items-center gap-1 text-xs text-red-600">
|
|
||||||
<div class="w-2 h-2 bg-red-500 rounded-full"></div>
|
|
||||||
Nostr failed
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<Badge :variant="getStatusVariant(order.status)">
|
<Badge :variant="getStatusVariant(order.status)" :class="getStatusColor(order.status)">
|
||||||
{{ formatStatus(order.status) }}
|
{{ formatStatus(order.status) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<!-- Payment Status Indicator -->
|
|
||||||
<div v-if="order.lightningInvoice" class="flex items-center gap-2">
|
|
||||||
<Badge
|
|
||||||
:variant="order.paymentStatus === 'paid' ? 'default' : 'secondary'"
|
|
||||||
class="text-xs"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<div v-if="order.paymentStatus === 'paid'" class="w-2 h-2 bg-green-500 rounded-full"></div>
|
|
||||||
<div v-else class="w-2 h-2 bg-yellow-500 rounded-full animate-pulse"></div>
|
|
||||||
{{ order.paymentStatus === 'paid' ? 'Paid' : 'Payment Pending' }}
|
|
||||||
</div>
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="text-lg font-semibold text-foreground">
|
<p class="text-lg font-semibold text-foreground">
|
||||||
{{ formatPrice(order.total, order.currency) }}
|
{{ formatPrice(order.total, order.currency) }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-muted-foreground">{{ order.currency.toUpperCase() }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Order Summary -->
|
<!-- Order Items -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
<div class="mb-4">
|
||||||
<!-- Items -->
|
|
||||||
<div>
|
|
||||||
<h4 class="font-medium text-foreground mb-2">Items</h4>
|
<h4 class="font-medium text-foreground mb-2">Items</h4>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div
|
<div v-for="item in order.items.slice(0, 3)" :key="item.productId" class="text-sm text-muted-foreground">
|
||||||
v-for="item in order.items.slice(0, 3)"
|
|
||||||
:key="item.productId"
|
|
||||||
class="text-sm text-muted-foreground"
|
|
||||||
>
|
|
||||||
{{ item.productName }} × {{ item.quantity }}
|
{{ item.productName }} × {{ item.quantity }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="order.items.length > 3" class="text-sm text-muted-foreground">
|
<div v-if="order.items.length > 3" class="text-sm text-muted-foreground">
|
||||||
|
|
@ -140,188 +105,96 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment Status -->
|
<!-- Payment Section -->
|
||||||
<div>
|
<div v-if="order.lightningInvoice" class="mb-4 p-4 bg-muted/50 border border-border rounded-lg">
|
||||||
<h4 class="font-medium text-foreground mb-2">Payment</h4>
|
|
||||||
<div v-if="order.lightningInvoice" class="space-y-1 text-sm">
|
|
||||||
<p class="text-muted-foreground">
|
|
||||||
<span class="font-medium">Status:</span> {{ order.paymentStatus === 'paid' ? 'Paid' : 'Pending' }}
|
|
||||||
</p>
|
|
||||||
<p class="text-muted-foreground">
|
|
||||||
<span class="font-medium">Invoice:</span> {{ order.lightningInvoice.payment_hash.slice(0, 8) }}...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-sm text-muted-foreground">
|
|
||||||
Waiting for merchant invoice
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Shipping -->
|
|
||||||
<div>
|
|
||||||
<h4 class="font-medium text-foreground mb-2">Shipping</h4>
|
|
||||||
<div class="space-y-1 text-sm text-muted-foreground">
|
|
||||||
<p>
|
|
||||||
<span class="font-medium">Zone:</span> {{ order.shippingZone?.name || 'N/A' }}
|
|
||||||
</p>
|
|
||||||
<p v-if="order.shippingZone?.estimatedDays">
|
|
||||||
<span class="font-medium">Est. Delivery:</span> {{ order.shippingZone.estimatedDays }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Payment Status & Actions -->
|
|
||||||
<div v-if="order.status === 'pending'" class="mb-4 p-4 bg-muted/50 border border-border rounded-lg">
|
|
||||||
<div class="flex-1">
|
|
||||||
<h4 class="font-medium text-foreground mb-2">Payment Required</h4>
|
|
||||||
<div v-if="order.lightningInvoice" class="space-y-2">
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
<span class="font-medium text-green-600">✓</span> Lightning invoice received from merchant
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Amount: <span class="font-medium text-foreground">{{ formatPrice(order.total, order.currency) }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="order.paymentRequest" class="space-y-2">
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
<span class="font-medium text-green-600">✓</span> Payment request received from merchant
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Amount: <span class="text-sm text-muted-foreground">{{ formatPrice(order.total, order.currency) }}</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Payment Options -->
|
|
||||||
<div class="mt-4 space-y-3">
|
|
||||||
<h5 class="text-sm font-medium text-foreground">Payment Options:</h5>
|
|
||||||
|
|
||||||
<!-- Lightning Payment -->
|
|
||||||
<div v-if="order.paymentRequest" class="bg-card border rounded-lg p-4">
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Zap class="w-4 h-4 text-yellow-500" />
|
<Zap class="w-4 h-4 text-yellow-500" />
|
||||||
<span class="font-medium text-sm">Lightning Payment</span>
|
<span class="font-medium text-foreground">Payment Required</span>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" class="text-xs">Recommended</Badge>
|
<Badge :variant="order.paymentStatus === 'paid' ? 'default' : 'secondary'" class="text-xs"
|
||||||
|
:class="order.paymentStatus === 'paid' ? 'text-green-600' : 'text-amber-600'">
|
||||||
|
{{ order.paymentStatus === 'paid' ? 'Paid' : 'Pending' }}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- QR Code -->
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="text-center mb-3">
|
<!-- Payment Details -->
|
||||||
<div class="w-32 h-32 mx-auto mb-2">
|
<div class="space-y-3">
|
||||||
<div v-if="order.qrCodeDataUrl && !order.qrCodeError" class="w-full h-full">
|
<!-- Payment Request -->
|
||||||
<img
|
<div>
|
||||||
:src="order.qrCodeDataUrl"
|
<label class="block text-xs font-medium text-muted-foreground mb-1">
|
||||||
:alt="`QR Code for ${formatPrice(order.total, order.currency)} payment`"
|
|
||||||
class="w-full h-full border border-border rounded-lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="order.qrCodeLoading" class="w-full h-full bg-muted rounded-lg flex items-center justify-center">
|
|
||||||
<div class="text-center text-muted-foreground">
|
|
||||||
<div class="text-2xl mb-1 animate-pulse">⚡</div>
|
|
||||||
<div class="text-xs">Generating...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="w-full h-full bg-muted rounded-lg flex items-center justify-center">
|
|
||||||
<div class="text-center text-muted-foreground">
|
|
||||||
<div class="text-2xl mb-1">⚡</div>
|
|
||||||
<div class="text-xs">No QR</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Payment Request Link -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="block text-xs font-medium text-muted-foreground">
|
|
||||||
Payment Request
|
Payment Request
|
||||||
</label>
|
</label>
|
||||||
<!-- Debug info -->
|
|
||||||
<div class="text-xs text-red-500 mb-2">
|
|
||||||
Debug: paymentRequest = "{{ order.paymentRequest }}" (length: {{ order.paymentRequest?.length || 0 }})<br>
|
|
||||||
Debug: paymentHash = "{{ order.paymentHash }}" (length: {{ order.paymentHash?.length || 0 }})<br>
|
|
||||||
Debug: Order keys: {{ Object.keys(order).join(', ') }}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Input
|
<input :value="order.lightningInvoice?.bolt11 || ''" readonly disabled
|
||||||
:value="order.paymentRequest || ''"
|
class="flex-1 font-mono text-xs bg-muted border border-input rounded-md px-3 py-1 text-foreground" />
|
||||||
readonly
|
<Button @click="copyPaymentRequest(order.lightningInvoice?.bolt11 || '')" variant="outline" size="sm">
|
||||||
class="flex-1 font-mono text-xs"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
@click="copyPaymentRequest(order.paymentRequest)"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
:disabled="!order.paymentRequest"
|
|
||||||
>
|
|
||||||
<Copy class="w-3 h-3" />
|
<Copy class="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment Actions -->
|
<!-- Payment Actions -->
|
||||||
<div class="flex gap-2 mt-3">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button @click="openLightningWallet(order.lightningInvoice?.bolt11 || '')" variant="default" size="sm"
|
||||||
@click="openLightningWallet(order.paymentRequest)"
|
class="flex-1" :disabled="!order.lightningInvoice?.bolt11">
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
class="flex-1"
|
|
||||||
>
|
|
||||||
<Zap class="w-3 h-3 mr-1" />
|
<Zap class="w-3 h-3 mr-1" />
|
||||||
Pay with Lightning
|
Pay with Lightning
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button @click="toggleQRCode(order.id)" variant="outline" size="sm">
|
||||||
@click="downloadQRCode(order)"
|
<QrCode class="w-3 h-3" />
|
||||||
variant="outline"
|
{{ order.showQRCode ? 'Hide QR' : 'Show QR' }}
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<Download class="w-3 h-3" />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- QR Code -->
|
||||||
|
<div v-if="order.showQRCode" class="flex justify-center">
|
||||||
|
<div class="w-48 h-48">
|
||||||
|
<div v-if="order.qrCodeDataUrl && !order.qrCodeError" class="w-full h-full">
|
||||||
|
<img :src="order.qrCodeDataUrl"
|
||||||
|
:alt="`QR Code for ${formatPrice(order.total, order.currency)} payment`"
|
||||||
|
class="w-full h-full border border-border rounded-lg" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="order.qrCodeLoading"
|
||||||
|
class="w-full h-full bg-muted rounded-lg flex items-center justify-center">
|
||||||
|
<div class="text-center text-muted-foreground">
|
||||||
|
<div class="text-4xl mb-2 animate-pulse">⚡</div>
|
||||||
|
<div class="text-sm">Generating QR...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="space-y-2">
|
<div v-else class="w-full h-full bg-muted rounded-lg flex items-center justify-center">
|
||||||
<p class="text-sm text-muted-foreground">
|
<div class="text-center text-muted-foreground">
|
||||||
<span class="font-medium text-amber-600">⏳</span> Waiting for merchant to generate payment invoice
|
<div class="text-4xl mb-2">⚡</div>
|
||||||
</p>
|
<div class="text-sm">No QR</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Waiting for Invoice -->
|
||||||
|
<div v-else-if="order.status === 'pending'" class="mb-4 p-4 bg-muted/50 border border-border rounded-lg">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<div class="w-2 h-2 bg-amber-500 rounded-full animate-pulse"></div>
|
||||||
|
<span class="font-medium text-foreground">Waiting for Payment Invoice</span>
|
||||||
|
</div>
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
The merchant will send you a Lightning invoice via Nostr once they process your order
|
The merchant will send you a Lightning invoice via Nostr once they process your order
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex justify-end gap-2 pt-4 border-t border-border">
|
<div class="flex justify-end gap-2 pt-4 border-t border-border">
|
||||||
<Button
|
<Button v-if="order.status === 'pending'" variant="outline" size="sm" @click="cancelOrder(order.id)">
|
||||||
v-if="order.status === 'pending'"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="cancelOrder(order.id)"
|
|
||||||
>
|
|
||||||
Cancel Order
|
Cancel Order
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="outline" size="sm" @click="copyOrderId(order.id)">
|
||||||
v-if="order.lightningInvoice"
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
@click="togglePaymentDisplay(order.id)"
|
|
||||||
>
|
|
||||||
<Wallet class="w-4 h-4 mr-2" />
|
|
||||||
{{ expandedPayments.has(order.id) ? 'Hide' : 'Show' }} Payment
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="copyOrderId(order.id)"
|
|
||||||
>
|
|
||||||
Copy Order ID
|
Copy Order ID
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment Display (Expandable) -->
|
|
||||||
<div v-if="expandedPayments.has(order.id) && order.lightningInvoice" class="mt-4 pt-4 border-t border-border">
|
|
||||||
<PaymentDisplay :order-id="order.id" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -358,25 +231,23 @@
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useMarketStore } from '@/stores/market'
|
import { useMarketStore } from '@/stores/market'
|
||||||
import { orderEvents } from '@/composables/useOrderEvents'
|
import { useOrderEvents } from '@/composables/useOrderEvents'
|
||||||
import { relayHubComposable } from '@/composables/useRelayHub'
|
import { relayHubComposable } from '@/composables/useRelayHub'
|
||||||
import { auth } from '@/composables/useAuth'
|
import { auth } from '@/composables/useAuth'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Package, Store, Zap, Copy, QrCode } from 'lucide-vue-next'
|
||||||
import { Package, Store, Wallet, Zap, Copy, Download } from 'lucide-vue-next'
|
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import PaymentDisplay from './PaymentDisplay.vue'
|
|
||||||
import type { OrderStatus } from '@/stores/market'
|
import type { OrderStatus } from '@/stores/market'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const marketStore = useMarketStore()
|
const marketStore = useMarketStore()
|
||||||
const relayHub = relayHubComposable
|
const relayHub = relayHubComposable
|
||||||
|
const orderEvents = useOrderEvents()
|
||||||
|
|
||||||
// Local state
|
// Local state
|
||||||
const statusFilter = ref('')
|
const statusFilter = ref('')
|
||||||
const sortBy = ref('createdAt')
|
const sortBy = ref('createdAt')
|
||||||
const expandedPayments = ref(new Set<string>())
|
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
const allOrders = computed(() => Object.values(marketStore.orders))
|
const allOrders = computed(() => Object.values(marketStore.orders))
|
||||||
|
|
@ -442,6 +313,18 @@ const getStatusVariant = (status: OrderStatus) => {
|
||||||
return variantMap[status] || 'outline'
|
return variantMap[status] || 'outline'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (status: OrderStatus) => {
|
||||||
|
const colorMap: Record<OrderStatus, string> = {
|
||||||
|
pending: 'text-amber-600',
|
||||||
|
paid: 'text-green-600',
|
||||||
|
processing: 'text-blue-600',
|
||||||
|
shipped: 'text-blue-600',
|
||||||
|
delivered: 'text-green-600',
|
||||||
|
cancelled: 'text-red-600'
|
||||||
|
}
|
||||||
|
return colorMap[status] || 'text-muted-foreground'
|
||||||
|
}
|
||||||
|
|
||||||
const formatPrice = (price: number, currency: string) => {
|
const formatPrice = (price: number, currency: string) => {
|
||||||
return marketStore.formatPrice(price, currency)
|
return marketStore.formatPrice(price, currency)
|
||||||
}
|
}
|
||||||
|
|
@ -451,14 +334,6 @@ const cancelOrder = (orderId: string) => {
|
||||||
console.log('Cancelling order:', orderId)
|
console.log('Cancelling order:', orderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const togglePaymentDisplay = (orderId: string) => {
|
|
||||||
if (expandedPayments.value.has(orderId)) {
|
|
||||||
expandedPayments.value.delete(orderId)
|
|
||||||
} else {
|
|
||||||
expandedPayments.value.add(orderId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyOrderId = async (orderId: string) => {
|
const copyOrderId = async (orderId: string) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(orderId)
|
await navigator.clipboard.writeText(orderId)
|
||||||
|
|
@ -523,28 +398,55 @@ const openLightningWallet = (paymentRequest: string) => {
|
||||||
copyPaymentRequest(paymentRequest)
|
copyPaymentRequest(paymentRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadQRCode = async (order: any) => {
|
const toggleQRCode = async (orderId: string) => {
|
||||||
if (!order.qrCodeDataUrl) {
|
// Toggle QR code visibility for the order
|
||||||
toast.error('QR code not available', {
|
const order = marketStore.orders[orderId]
|
||||||
description: 'Please wait for the QR code to generate'
|
if (order) {
|
||||||
})
|
// If showing QR code and it doesn't exist yet, generate it
|
||||||
return
|
if (!order.showQRCode && order.lightningInvoice?.bolt11 && !order.qrCodeDataUrl) {
|
||||||
|
await generateQRCode(orderId, order.lightningInvoice.bolt11)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
marketStore.updateOrder(orderId, {
|
||||||
const link = document.createElement('a')
|
showQRCode: !order.showQRCode
|
||||||
link.href = order.qrCodeDataUrl
|
|
||||||
link.download = `payment-qr-${order.id}.png`
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
toast.success('QR code downloaded', {
|
|
||||||
description: 'You can now scan it with your mobile wallet'
|
|
||||||
})
|
})
|
||||||
} catch (err) {
|
}
|
||||||
console.error('Failed to download QR code:', err)
|
}
|
||||||
toast.error('Failed to download QR code', {
|
|
||||||
description: 'Please try again'
|
const generateQRCode = async (orderId: string, bolt11: string) => {
|
||||||
|
try {
|
||||||
|
// Set loading state
|
||||||
|
marketStore.updateOrder(orderId, {
|
||||||
|
qrCodeLoading: true,
|
||||||
|
qrCodeError: null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Import QRCode library dynamically
|
||||||
|
const QRCode = await import('qrcode')
|
||||||
|
|
||||||
|
// Generate QR code
|
||||||
|
const qrCodeDataUrl = await QRCode.toDataURL(bolt11, {
|
||||||
|
width: 192,
|
||||||
|
margin: 2,
|
||||||
|
color: {
|
||||||
|
dark: '#000000',
|
||||||
|
light: '#FFFFFF'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update order with QR code
|
||||||
|
marketStore.updateOrder(orderId, {
|
||||||
|
qrCodeDataUrl,
|
||||||
|
qrCodeLoading: false,
|
||||||
|
qrCodeError: null
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('QR code generated for order:', orderId)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate QR code:', error)
|
||||||
|
marketStore.updateOrder(orderId, {
|
||||||
|
qrCodeLoading: false,
|
||||||
|
qrCodeError: error instanceof Error ? error.message : 'Failed to generate QR code'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -578,7 +480,7 @@ onMounted(() => {
|
||||||
// Start listening for order events if not already listening
|
// Start listening for order events if not already listening
|
||||||
if (!orderEvents.isSubscribed.value) {
|
if (!orderEvents.isSubscribed.value) {
|
||||||
console.log('Starting order events listener...')
|
console.log('Starting order events listener...')
|
||||||
orderEvents.startListening()
|
orderEvents.initialize()
|
||||||
} else {
|
} else {
|
||||||
console.log('Order events already listening')
|
console.log('Order events already listening')
|
||||||
}
|
}
|
||||||
|
|
@ -590,10 +492,9 @@ watch(
|
||||||
([isAuth, isConnected]) => {
|
([isAuth, isConnected]) => {
|
||||||
if (isAuth && isConnected && !orderEvents.isSubscribed.value) {
|
if (isAuth && isConnected && !orderEvents.isSubscribed.value) {
|
||||||
console.log('Auth and relay hub ready, starting order events listener...')
|
console.log('Auth and relay hub ready, starting order events listener...')
|
||||||
orderEvents.startListening()
|
orderEvents.initialize()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-white border rounded-lg p-6">
|
<div class="bg-background border rounded-lg p-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h3 class="text-lg font-semibold text-gray-900">Payment</h3>
|
<h3 class="text-lg font-semibold text-foreground">Payment</h3>
|
||||||
<Badge :variant="getPaymentStatusVariant(paymentStatus)">
|
<Badge :variant="getPaymentStatusVariant(paymentStatus)">
|
||||||
{{ formatPaymentStatus(paymentStatus) }}
|
{{ formatPaymentStatus(paymentStatus) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
@ -10,15 +10,15 @@
|
||||||
<!-- Invoice Information -->
|
<!-- Invoice Information -->
|
||||||
<div v-if="invoice" class="space-y-4">
|
<div v-if="invoice" class="space-y-4">
|
||||||
<!-- Amount and Status -->
|
<!-- Amount and Status -->
|
||||||
<div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
<div class="flex items-center justify-between p-4 bg-muted/50 rounded-lg">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-gray-600">Amount</p>
|
<p class="text-sm text-muted-foreground">Amount</p>
|
||||||
<p class="text-2xl font-bold text-gray-900">
|
<p class="text-2xl font-bold text-foreground">
|
||||||
{{ invoice.amount }} {{ currency }}
|
{{ invoice.amount }} {{ currency }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="text-sm text-gray-600">Status</p>
|
<p class="text-sm text-muted-foreground">Status</p>
|
||||||
<p class="text-lg font-semibold" :class="getStatusColor(paymentStatus)">
|
<p class="text-lg font-semibold" :class="getStatusColor(paymentStatus)">
|
||||||
{{ formatPaymentStatus(paymentStatus) }}
|
{{ formatPaymentStatus(paymentStatus) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -28,8 +28,8 @@
|
||||||
<!-- Lightning Invoice QR Code -->
|
<!-- Lightning Invoice QR Code -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="font-medium text-gray-900 mb-2">Lightning Invoice</h4>
|
<h4 class="font-medium text-foreground mb-2">Lightning Invoice</h4>
|
||||||
<p class="text-sm text-gray-600">Scan with your Lightning wallet to pay</p>
|
<p class="text-sm text-muted-foreground">Scan with your Lightning wallet to pay</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- QR Code -->
|
<!-- QR Code -->
|
||||||
|
|
@ -38,17 +38,17 @@
|
||||||
<img
|
<img
|
||||||
:src="qrCodeDataUrl"
|
:src="qrCodeDataUrl"
|
||||||
:alt="`QR Code for ${invoice.amount} ${currency} payment`"
|
:alt="`QR Code for ${invoice.amount} ${currency} payment`"
|
||||||
class="w-full h-full border border-gray-200 rounded-lg"
|
class="w-full h-full border border-border rounded-lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="qrCodeLoading" class="w-full h-full bg-gray-100 rounded-lg flex items-center justify-center">
|
<div v-else-if="qrCodeLoading" class="w-full h-full bg-muted rounded-lg flex items-center justify-center">
|
||||||
<div class="text-center text-gray-500">
|
<div class="text-center text-muted-foreground">
|
||||||
<div class="text-4xl mb-2 animate-pulse">⚡</div>
|
<div class="text-4xl mb-2 animate-pulse">⚡</div>
|
||||||
<div class="text-sm">Generating QR...</div>
|
<div class="text-sm">Generating QR...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="qrCodeError" class="w-full h-full bg-red-50 border border-red-200 rounded-lg flex items-center justify-center">
|
<div v-else-if="qrCodeError" class="w-full h-full bg-destructive/10 border border-destructive/20 rounded-lg flex items-center justify-center">
|
||||||
<div class="text-center text-red-500">
|
<div class="text-center text-destructive">
|
||||||
<div class="text-4xl mb-2">⚠️</div>
|
<div class="text-4xl mb-2">⚠️</div>
|
||||||
<div class="text-sm">{{ qrCodeError }}</div>
|
<div class="text-sm">{{ qrCodeError }}</div>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -61,8 +61,8 @@
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full h-full bg-gray-100 rounded-lg flex items-center justify-center">
|
<div v-else class="w-full h-full bg-muted rounded-lg flex items-center justify-center">
|
||||||
<div class="text-center text-gray-500">
|
<div class="text-center text-muted-foreground">
|
||||||
<div class="text-4xl mb-2">⚡</div>
|
<div class="text-4xl mb-2">⚡</div>
|
||||||
<div class="text-sm">No invoice</div>
|
<div class="text-sm">No invoice</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -71,20 +71,12 @@
|
||||||
|
|
||||||
<!-- QR Code Actions -->
|
<!-- QR Code Actions -->
|
||||||
<div v-if="qrCodeDataUrl && !qrCodeError" class="mb-4">
|
<div v-if="qrCodeDataUrl && !qrCodeError" class="mb-4">
|
||||||
<Button
|
<!-- Download button removed -->
|
||||||
@click="downloadQRCode"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
class="w-full"
|
|
||||||
>
|
|
||||||
<Download class="w-4 h-4 mr-2" />
|
|
||||||
Download QR Code
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment Request -->
|
<!-- Payment Request -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
<label class="block text-sm font-medium text-foreground mb-2">
|
||||||
Payment Request
|
Payment Request
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|
@ -116,23 +108,23 @@
|
||||||
|
|
||||||
<!-- Payment Details -->
|
<!-- Payment Details -->
|
||||||
<div class="border-t pt-4">
|
<div class="border-t pt-4">
|
||||||
<h4 class="font-medium text-gray-900 mb-3">Payment Details</h4>
|
<h4 class="font-medium text-foreground mb-3">Payment Details</h4>
|
||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-gray-600">Payment Hash:</span>
|
<span class="text-muted-foreground">Payment Hash:</span>
|
||||||
<span class="font-mono text-gray-900">{{ formatHash(invoice.payment_hash) }}</span>
|
<span class="font-mono text-foreground">{{ formatHash(invoice.payment_hash) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-gray-600">Created:</span>
|
<span class="text-muted-foreground">Created:</span>
|
||||||
<span class="text-gray-900">{{ formatDate(invoice.created_at ? new Date(invoice.created_at).getTime() : Date.now()) }}</span>
|
<span class="text-foreground">{{ formatDate(invoice.created_at) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-gray-600">Expires:</span>
|
<span class="text-muted-foreground">Expires:</span>
|
||||||
<span class="text-gray-900">{{ formatDate(invoice.expiry ? new Date(invoice.expiry).getTime() : Date.now()) }}</span>
|
<span class="text-foreground">{{ formatDate(invoice.expiry) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="paidAt" class="flex justify-between">
|
<div v-if="paidAt" class="flex justify-between">
|
||||||
<span class="text-gray-600">Paid At:</span>
|
<span class="text-muted-foreground">Paid At:</span>
|
||||||
<span class="text-gray-900">{{ formatDate(paidAt) }}</span>
|
<span class="text-foreground">{{ formatDate(paidAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -140,14 +132,14 @@
|
||||||
|
|
||||||
<!-- No Invoice State -->
|
<!-- No Invoice State -->
|
||||||
<div v-else class="text-center py-8">
|
<div v-else class="text-center py-8">
|
||||||
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
<div class="w-16 h-16 bg-muted rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
<Wallet class="w-8 h-8 text-gray-400" />
|
<Wallet class="w-8 h-8 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<h4 class="text-lg font-medium text-gray-900 mb-2">No Payment Invoice</h4>
|
<h4 class="text-lg font-medium text-foreground mb-2">No Payment Invoice</h4>
|
||||||
<p class="text-gray-600 mb-4">
|
<p class="text-muted-foreground mb-4">
|
||||||
A Lightning invoice will be sent by the merchant once they process your order.
|
A Lightning invoice will be sent by the merchant once they process your order.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-gray-500">
|
<p class="text-sm text-muted-foreground">
|
||||||
You'll receive the invoice via Nostr when it's ready.
|
You'll receive the invoice via Nostr when it's ready.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -193,10 +185,8 @@ import { Badge } from '@/components/ui/badge'
|
||||||
import {
|
import {
|
||||||
Copy,
|
Copy,
|
||||||
Wallet,
|
Wallet,
|
||||||
|
|
||||||
Info,
|
Info,
|
||||||
CheckCircle,
|
CheckCircle
|
||||||
Download
|
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { useMarketStore } from '@/stores/market'
|
import { useMarketStore } from '@/stores/market'
|
||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
|
|
@ -266,9 +256,9 @@ const formatPaymentStatus = (status: string) => {
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'paid': return 'text-green-600'
|
case 'paid': return 'text-green-600'
|
||||||
case 'pending': return 'text-yellow-600'
|
case 'pending': return 'text-amber-600'
|
||||||
case 'expired': return 'text-red-600'
|
case 'expired': return 'text-destructive'
|
||||||
default: return 'text-gray-600'
|
default: return 'text-muted-foreground'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,9 +267,20 @@ const formatHash = (hash: string) => {
|
||||||
return `${hash.substring(0, 8)}...${hash.substring(hash.length - 8)}`
|
return `${hash.substring(0, 8)}...${hash.substring(hash.length - 8)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDate = (timestamp: number) => {
|
const formatDate = (dateValue: string | number | undefined) => {
|
||||||
if (!timestamp) return 'N/A'
|
if (!dateValue) return 'N/A'
|
||||||
return new Date(timestamp * 1000).toLocaleString()
|
|
||||||
|
let timestamp: number
|
||||||
|
|
||||||
|
if (typeof dateValue === 'string') {
|
||||||
|
// Handle ISO date strings from LNBits API
|
||||||
|
timestamp = new Date(dateValue).getTime()
|
||||||
|
} else {
|
||||||
|
// Handle Unix timestamps (seconds) from our store
|
||||||
|
timestamp = dateValue * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(timestamp).toLocaleString()
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyPaymentRequest = async () => {
|
const copyPaymentRequest = async () => {
|
||||||
|
|
@ -302,17 +303,6 @@ const openInWallet = () => {
|
||||||
window.open(walletUrl, '_blank')
|
window.open(walletUrl, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadQRCode = () => {
|
|
||||||
if (!qrCodeDataUrl.value) return
|
|
||||||
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = qrCodeDataUrl.value
|
|
||||||
link.download = `qr-code-${invoice.value?.amount}-${currency.value}.png`
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
}
|
|
||||||
|
|
||||||
const retryQRCode = () => {
|
const retryQRCode = () => {
|
||||||
if (invoice.value?.bolt11) {
|
if (invoice.value?.bolt11) {
|
||||||
generateQRCode(invoice.value.bolt11)
|
generateQRCode(invoice.value.bolt11)
|
||||||
|
|
|
||||||
|
|
@ -255,7 +255,7 @@ const payWithWallet = async () => {
|
||||||
const { useAuth } = await import('@/composables/useAuth')
|
const { useAuth } = await import('@/composables/useAuth')
|
||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
|
|
||||||
if (!auth.currentUser.value?.walletId || !auth.currentUser.value?.adminKey) {
|
if (!auth.currentUser.value?.wallets?.[0]?.id || !auth.currentUser.value?.wallets?.[0]?.adminkey) {
|
||||||
toast.error('Please connect your wallet to pay')
|
toast.error('Please connect your wallet to pay')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -263,8 +263,8 @@ const payWithWallet = async () => {
|
||||||
// Pay the invoice
|
// Pay the invoice
|
||||||
const result = await payInvoiceWithWallet(
|
const result = await payInvoiceWithWallet(
|
||||||
lightningInvoice.value,
|
lightningInvoice.value,
|
||||||
auth.currentUser.value.walletId,
|
auth.currentUser.value.wallets[0].id,
|
||||||
auth.currentUser.value.adminKey
|
auth.currentUser.value.wallets[0].adminkey
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log('Payment result:', result)
|
console.log('Payment result:', result)
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,11 @@ const modelValue = useVModel(props, 'modelValue', emits, {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<input v-model="modelValue" :class="cn('flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', props.class)">
|
<input
|
||||||
|
v-model="modelValue"
|
||||||
|
:class="cn(
|
||||||
|
'flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm text-foreground shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
props.class
|
||||||
|
)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ref, computed, onMounted, onUnmounted, readonly } from 'vue'
|
import { ref, computed, onUnmounted, readonly } from 'vue'
|
||||||
import { nostrclientHub, type SubscriptionConfig } from '../lib/nostr/nostrclientHub'
|
import { nostrclientHub, type SubscriptionConfig } from '../lib/nostr/nostrclientHub'
|
||||||
|
|
||||||
export function useNostrclientHub() {
|
export function useNostrclientHub() {
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,84 @@
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { nip04 } from 'nostr-tools'
|
import { nip04 } from 'nostr-tools'
|
||||||
import { relayHubComposable } from './useRelayHub'
|
import { relayHubComposable } from './useRelayHub'
|
||||||
import { useAuth } from './useAuth'
|
import { useAuth } from './useAuth'
|
||||||
import { useMarketStore } from '@/stores/market'
|
import { useMarketStore } from '@/stores/market'
|
||||||
import { config } from '@/lib/config'
|
import { decode } from 'light-bolt11-decoder'
|
||||||
import type { Order, OrderStatus } from '@/stores/market'
|
|
||||||
import type { LightningInvoice } from '@/lib/services/invoiceService'
|
|
||||||
|
|
||||||
// Order event types based on NIP-69 and nostrmarket patterns
|
// Nostrmarket Order interfaces based on the actual implementation
|
||||||
export enum OrderEventType {
|
|
||||||
CUSTOMER_ORDER = 'customer_order',
|
// Nostrmarket Order interfaces based on the actual implementation
|
||||||
PAYMENT_REQUEST = 'payment_request',
|
interface OrderItem {
|
||||||
ORDER_PAID = 'order_paid',
|
product_id: string
|
||||||
ORDER_SHIPPED = 'order_shipped',
|
quantity: number
|
||||||
ORDER_DELIVERED = 'order_delivered',
|
|
||||||
ORDER_CANCELLED = 'order_cancelled',
|
|
||||||
INVOICE_GENERATED = 'invoice_generated'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderEvent {
|
interface OrderContact {
|
||||||
type: OrderEventType
|
nostr?: string
|
||||||
orderId: string
|
phone?: string
|
||||||
data: any
|
email?: string
|
||||||
timestamp: number
|
|
||||||
senderPubkey: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaymentRequestEvent {
|
// Direct message types from nostrmarket
|
||||||
type: OrderEventType.PAYMENT_REQUEST
|
enum DirectMessageType {
|
||||||
orderId: string
|
PLAIN_TEXT = -1,
|
||||||
paymentRequest: string
|
CUSTOMER_ORDER = 0,
|
||||||
amount: number
|
PAYMENT_REQUEST = 1,
|
||||||
currency: string
|
ORDER_PAID_OR_SHIPPED = 2
|
||||||
memo: string
|
|
||||||
expiresAt: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderStatusEvent {
|
// Event types for nostrmarket protocol
|
||||||
type: OrderEventType.ORDER_PAID | OrderEventType.ORDER_SHIPPED | OrderEventType.ORDER_DELIVERED
|
interface CustomerOrderEvent {
|
||||||
orderId: string
|
type: DirectMessageType.CUSTOMER_ORDER
|
||||||
status: OrderStatus
|
id: string
|
||||||
timestamp: number
|
items: OrderItem[]
|
||||||
additionalData?: any
|
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() {
|
export function useOrderEvents() {
|
||||||
|
|
@ -62,524 +99,216 @@ export function useOrderEvents() {
|
||||||
const isConnected = relayHub.isConnected.value
|
const isConnected = relayHub.isConnected.value
|
||||||
const hasPubkey = !!currentUserPubkey.value
|
const hasPubkey = !!currentUserPubkey.value
|
||||||
|
|
||||||
console.log('OrderEvents isReady check:', { isAuth, isConnected, hasPubkey })
|
|
||||||
return isAuth && isConnected && hasPubkey
|
return isAuth && isConnected && hasPubkey
|
||||||
})
|
})
|
||||||
|
|
||||||
// Subscribe to order events
|
// Subscribe to order events
|
||||||
const subscribeToOrderEvents = async () => {
|
const subscribeToOrderEvents = async () => {
|
||||||
console.log('subscribeToOrderEvents called with:', {
|
|
||||||
isReady: isReady.value,
|
|
||||||
isSubscribed: isSubscribed.value,
|
|
||||||
currentUserPubkey: currentUserPubkey.value,
|
|
||||||
relayHubConnected: relayHub.isConnected.value,
|
|
||||||
authStatus: auth.isAuthenticated
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!isReady.value || isSubscribed.value) {
|
if (!isReady.value || isSubscribed.value) {
|
||||||
console.warn('Cannot subscribe to order events: not ready or already subscribed', {
|
|
||||||
isReady: isReady.value,
|
|
||||||
isSubscribed: isSubscribed.value
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Subscribing to order events for user:', currentUserPubkey.value)
|
|
||||||
|
|
||||||
// Subscribe to direct messages (kind 4) that contain order information
|
// Subscribe to direct messages (kind 4) that contain order information
|
||||||
const filters = [
|
const filters = [
|
||||||
{
|
{
|
||||||
kinds: [4], // NIP-04 encrypted direct messages
|
kinds: [4], // NIP-04 encrypted direct messages
|
||||||
'#p': [currentUserPubkey.value].filter(Boolean) as string[], // Messages to us, filter out undefined
|
'#p': [currentUserPubkey.value].filter(Boolean) as string[],
|
||||||
since: lastEventTimestamp.value
|
since: lastEventTimestamp.value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
console.log('Using filters:', filters)
|
relayHub.subscribe({
|
||||||
|
id: 'order-events',
|
||||||
const unsubscribe = relayHub.subscribe({
|
|
||||||
id: `order-events-${currentUserPubkey.value}-${Date.now()}`,
|
|
||||||
filters,
|
filters,
|
||||||
relays: config.market.supportedRelays,
|
onEvent: handleOrderEvent,
|
||||||
onEvent: (event: any) => {
|
|
||||||
console.log('Received event in order subscription:', event.id)
|
|
||||||
handleOrderEvent(event)
|
|
||||||
},
|
|
||||||
onEose: () => {
|
onEose: () => {
|
||||||
console.log('Order events subscription EOSE')
|
console.log('Order events subscription ended')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
subscriptionId.value = `order-events-${currentUserPubkey.value}-${Date.now()}`
|
subscriptionId.value = 'order-events'
|
||||||
isSubscribed.value = true
|
isSubscribed.value = true
|
||||||
|
console.log('Successfully subscribed to order events')
|
||||||
|
|
||||||
console.log('Successfully subscribed to order events with ID:', subscriptionId.value)
|
|
||||||
return unsubscribe
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to subscribe to order events:', error)
|
console.error('Failed to subscribe to order events:', error)
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle incoming order events
|
// Handle incoming order events
|
||||||
const handleOrderEvent = async (event: any) => {
|
const handleOrderEvent = async (event: any) => {
|
||||||
if (!auth.currentUser?.value?.prvkey) {
|
|
||||||
console.warn('Cannot decrypt order event: no private key available')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we've already processed this event
|
|
||||||
if (processedEventIds.value.has(event.id)) {
|
if (processedEventIds.value.has(event.id)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
// Decrypt the message content
|
|
||||||
const decryptedContent = await nip04.decrypt(
|
|
||||||
auth.currentUser.value.prvkey,
|
|
||||||
event.pubkey, // Sender's pubkey
|
|
||||||
event.content
|
|
||||||
)
|
|
||||||
|
|
||||||
// Parse the decrypted content
|
|
||||||
const orderEvent = JSON.parse(decryptedContent)
|
|
||||||
|
|
||||||
console.log('Received order event:', {
|
|
||||||
eventId: event.id,
|
|
||||||
type: orderEvent.type,
|
|
||||||
orderId: orderEvent.orderId,
|
|
||||||
sender: event.pubkey
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle nostrmarket protocol messages
|
|
||||||
if (orderEvent.type === 0 || orderEvent.type === 1 || orderEvent.type === 2) {
|
|
||||||
await handleNostrmarketMessage(orderEvent, event.pubkey)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the order event based on type
|
|
||||||
await processOrderEvent(orderEvent, event.pubkey)
|
|
||||||
|
|
||||||
// Mark as processed
|
|
||||||
processedEventIds.value.add(event.id)
|
processedEventIds.value.add(event.id)
|
||||||
lastEventTimestamp.value = Math.max(lastEventTimestamp.value, event.created_at)
|
lastEventTimestamp.value = Math.max(lastEventTimestamp.value, event.created_at)
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to process order event:', {
|
|
||||||
eventId: event.id,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle nostrmarket protocol messages (type 0, 1, 2)
|
|
||||||
const handleNostrmarketMessage = async (message: any, senderPubkey: string) => {
|
|
||||||
try {
|
try {
|
||||||
console.log('Processing nostrmarket message:', {
|
// Decrypt the message content
|
||||||
type: message.type,
|
const decryptedContent = await nip04.decrypt(
|
||||||
orderId: message.id,
|
auth.currentUser.value?.prvkey || '',
|
||||||
sender: senderPubkey
|
event.pubkey,
|
||||||
})
|
event.content
|
||||||
|
)
|
||||||
|
|
||||||
// Import nostrmarket service
|
// Parse the JSON content
|
||||||
const { nostrmarketService } = await import('@/lib/services/nostrmarketService')
|
const jsonData = JSON.parse(decryptedContent)
|
||||||
|
|
||||||
switch (message.type) {
|
// Handle different message types
|
||||||
case 0:
|
switch (jsonData.type) {
|
||||||
// Customer order - this should be handled by the merchant side
|
case DirectMessageType.CUSTOMER_ORDER:
|
||||||
console.log('Received customer order (type 0) - this should be handled by merchant')
|
await handleCustomerOrder(jsonData as CustomerOrderEvent, event.pubkey)
|
||||||
break
|
break
|
||||||
|
case DirectMessageType.PAYMENT_REQUEST:
|
||||||
case 1:
|
await handlePaymentRequest(jsonData as PaymentRequestEvent, event.pubkey)
|
||||||
// Payment request from merchant
|
|
||||||
console.log('Received payment request from merchant')
|
|
||||||
await nostrmarketService.handlePaymentRequest(message)
|
|
||||||
break
|
break
|
||||||
|
case DirectMessageType.ORDER_PAID_OR_SHIPPED:
|
||||||
case 2:
|
await handleOrderStatusUpdate(jsonData as OrderStatusEvent, event.pubkey)
|
||||||
// Order status update from merchant
|
|
||||||
console.log('Received order status update from merchant')
|
|
||||||
await nostrmarketService.handleOrderStatusUpdate(message)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn('Unknown nostrmarket message type:', message.type)
|
console.log('Unknown message type:', jsonData.type)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to handle nostrmarket message:', error)
|
console.error('Error processing order event:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process incoming Nostr events
|
// Handle customer order (type 0)
|
||||||
const processOrderEvent = async (event: any, senderPubkey: string) => {
|
const handleCustomerOrder = async (orderData: CustomerOrderEvent, _senderPubkey: string) => {
|
||||||
try {
|
console.log('Received customer order:', orderData)
|
||||||
console.log('Received order event:', {
|
|
||||||
eventId: event.id || 'unknown',
|
|
||||||
type: event.type,
|
|
||||||
orderId: event.orderId,
|
|
||||||
sender: senderPubkey
|
|
||||||
})
|
|
||||||
|
|
||||||
// Only process events that have the required market order structure
|
|
||||||
if (!event.type || event.type !== 'market_order') {
|
|
||||||
console.log('Skipping non-market order event:', event.type)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate that this is actually a market order event
|
|
||||||
if (!event.orderId || !event.items || !Array.isArray(event.items)) {
|
|
||||||
console.log('Skipping invalid market order event - missing required fields')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Processing market order:', event)
|
|
||||||
|
|
||||||
// Check if this order already exists - use the orderId as the primary key
|
|
||||||
const existingOrder = Object.values(marketStore.orders).find(
|
|
||||||
order => order.id === event.orderId
|
|
||||||
)
|
|
||||||
|
|
||||||
if (existingOrder) {
|
|
||||||
console.log('Order already exists, updating with new information:', existingOrder.id)
|
|
||||||
|
|
||||||
// Update the existing order with any new information
|
|
||||||
const updatedOrder = {
|
|
||||||
...existingOrder,
|
|
||||||
updatedAt: Math.floor(Date.now() / 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's invoice information, update it
|
|
||||||
if (event.lightningInvoice) {
|
|
||||||
updatedOrder.lightningInvoice = event.lightningInvoice
|
|
||||||
updatedOrder.paymentHash = event.paymentHash
|
|
||||||
updatedOrder.paymentStatus = event.paymentStatus || 'pending'
|
|
||||||
updatedOrder.paymentRequest = event.paymentRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the order in the store
|
|
||||||
marketStore.updateOrder(existingOrder.id, updatedOrder)
|
|
||||||
|
|
||||||
console.log('Updated existing order:', {
|
|
||||||
orderId: existingOrder.id,
|
|
||||||
hasInvoice: !!updatedOrder.lightningInvoice,
|
|
||||||
paymentStatus: updatedOrder.paymentStatus
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a basic order object from the event data
|
// Create a basic order object from the event data
|
||||||
const orderData: Partial<Order> = {
|
const order = {
|
||||||
id: event.orderId,
|
id: orderData.id,
|
||||||
nostrEventId: event.id || 'unknown',
|
type: DirectMessageType.CUSTOMER_ORDER,
|
||||||
buyerPubkey: senderPubkey,
|
items: orderData.items,
|
||||||
sellerPubkey: event.sellerPubkey || '',
|
contact: orderData.contact,
|
||||||
items: event.items || [],
|
shipping_id: orderData.shipping_id,
|
||||||
total: event.total || 0,
|
message: orderData.message,
|
||||||
currency: event.currency || 'sat',
|
createdAt: Date.now(),
|
||||||
status: 'pending' as OrderStatus,
|
|
||||||
createdAt: event.createdAt || Date.now(),
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
// Add invoice details if present
|
|
||||||
...(event.lightningInvoice && {
|
|
||||||
lightningInvoice: {
|
|
||||||
checking_id: event.lightningInvoice.checking_id || event.lightningInvoice.payment_hash || '',
|
|
||||||
payment_hash: event.lightningInvoice.payment_hash || '',
|
|
||||||
wallet_id: event.lightningInvoice.wallet_id || '',
|
|
||||||
amount: event.lightningInvoice.amount || 0,
|
|
||||||
fee: event.lightningInvoice.fee || 0,
|
|
||||||
bolt11: event.lightningInvoice.bolt11 || event.lightningInvoice.payment_request || '',
|
|
||||||
status: 'pending',
|
|
||||||
memo: event.lightningInvoice.memo || '',
|
|
||||||
expiry: event.lightningInvoice.expiry || '',
|
|
||||||
preimage: event.lightningInvoice.preimage || '',
|
|
||||||
extra: event.lightningInvoice.extra || {},
|
|
||||||
created_at: event.lightningInvoice.created_at || '',
|
|
||||||
updated_at: event.lightningInvoice.updated_at || ''
|
|
||||||
},
|
|
||||||
paymentHash: event.lightningInvoice.payment_hash || '',
|
|
||||||
paymentStatus: 'pending',
|
|
||||||
paymentRequest: event.lightningInvoice.bolt11 || event.lightningInvoice.payment_request || '',
|
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the order using the store method
|
// Store the order in our local state
|
||||||
const order = marketStore.createOrder({
|
// Note: We're not using the complex Order interface from market store
|
||||||
id: event.id,
|
// Instead, we're using the simple nostrmarket format
|
||||||
cartId: event.id,
|
console.log('Processed customer order:', order)
|
||||||
stallId: 'unknown', // We'll need to determine this from the items
|
|
||||||
buyerPubkey: senderPubkey,
|
|
||||||
sellerPubkey: '', // Will be set when we know the merchant
|
|
||||||
status: 'pending',
|
|
||||||
items: Array.from(orderData.items || []), // Convert readonly to mutable
|
|
||||||
contactInfo: orderData.contactInfo || {},
|
|
||||||
shippingZone: orderData.shippingZone || {
|
|
||||||
id: 'online',
|
|
||||||
name: 'Online',
|
|
||||||
cost: 0,
|
|
||||||
currency: 'sat',
|
|
||||||
description: 'Online delivery'
|
|
||||||
},
|
|
||||||
paymentMethod: 'lightning',
|
|
||||||
subtotal: 0,
|
|
||||||
shippingCost: 0,
|
|
||||||
total: 0,
|
|
||||||
currency: 'sat',
|
|
||||||
originalOrderId: event.id
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('Created order from market event:', {
|
|
||||||
orderId: order.id,
|
|
||||||
total: order.total,
|
|
||||||
status: order.status
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to handle market order:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle payment request events
|
// Handle payment request (type 1)
|
||||||
const handlePaymentRequest = async (event: PaymentRequestEvent, _senderPubkey: string) => {
|
const handlePaymentRequest = async (paymentData: PaymentRequestEvent, _senderPubkey: string) => {
|
||||||
try {
|
console.log('Received payment request:', paymentData)
|
||||||
// Find the order in our store
|
|
||||||
const order = marketStore.orders[event.orderId]
|
|
||||||
if (!order) {
|
|
||||||
console.warn('Payment request received for unknown order:', event.orderId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update order with payment request (excluding readonly items)
|
// Find the lightning payment option
|
||||||
const { items, ...orderWithoutItems } = order
|
const lightningOption = paymentData.payment_options?.find(opt => opt.type === 'ln')
|
||||||
const updatedOrder = {
|
if (lightningOption) {
|
||||||
...orderWithoutItems,
|
console.log('Lightning payment request:', lightningOption.link)
|
||||||
paymentRequest: event.paymentRequest,
|
|
||||||
paymentStatus: 'pending' as const,
|
|
||||||
updatedAt: Date.now() / 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the order in the store
|
|
||||||
marketStore.updateOrder(event.orderId, updatedOrder)
|
|
||||||
|
|
||||||
console.log('Order updated with payment request:', {
|
|
||||||
orderId: event.orderId,
|
|
||||||
amount: event.amount,
|
|
||||||
currency: event.currency
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to handle payment request:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle order status updates
|
|
||||||
const handleOrderStatusUpdate = async (event: OrderStatusEvent, _senderPubkey: string) => {
|
|
||||||
try {
|
|
||||||
// Find the order in our store
|
|
||||||
const order = marketStore.orders[event.orderId]
|
|
||||||
if (!order) {
|
|
||||||
console.warn('Status update received for unknown order:', event.orderId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update order status
|
|
||||||
marketStore.updateOrderStatus(event.orderId, event.status)
|
|
||||||
|
|
||||||
console.log('Order status updated:', {
|
|
||||||
orderId: event.orderId,
|
|
||||||
newStatus: event.status,
|
|
||||||
timestamp: event.timestamp
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to handle order status update:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle invoice generation events
|
|
||||||
const handleInvoiceGenerated = async (event: any, _senderPubkey: string) => {
|
|
||||||
try {
|
|
||||||
// Find the order in our store
|
|
||||||
const order = marketStore.orders[event.orderId]
|
|
||||||
if (!order) {
|
|
||||||
console.warn('Invoice generated for unknown order:', event.orderId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update order with invoice details (excluding readonly items)
|
|
||||||
const { items, ...orderWithoutItems } = order
|
|
||||||
const updatedOrder = {
|
|
||||||
...orderWithoutItems,
|
|
||||||
lightningInvoice: {
|
|
||||||
payment_hash: event.paymentHash,
|
|
||||||
payment_request: event.paymentRequest,
|
|
||||||
amount: event.amount,
|
|
||||||
memo: event.memo,
|
|
||||||
expiry: event.expiresAt,
|
|
||||||
created_at: event.timestamp,
|
|
||||||
status: 'pending' as const
|
|
||||||
},
|
|
||||||
paymentHash: event.paymentHash,
|
|
||||||
paymentStatus: 'pending' as const,
|
|
||||||
updatedAt: Date.now() / 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the order in the store
|
|
||||||
marketStore.updateOrder(event.orderId, updatedOrder)
|
|
||||||
|
|
||||||
console.log('Order updated with invoice details:', {
|
|
||||||
orderId: event.orderId,
|
|
||||||
paymentHash: event.paymentHash,
|
|
||||||
amount: event.amount
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to handle invoice generation:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle market order events (new orders)
|
|
||||||
const handleMarketOrder = async (event: any, senderPubkey: string) => {
|
|
||||||
try {
|
|
||||||
console.log('Processing market order:', event)
|
|
||||||
|
|
||||||
// Check if this order already exists
|
|
||||||
const existingOrder = Object.values(marketStore.orders).find(
|
|
||||||
order => order.id === event.orderId || order.nostrEventId === event.id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
// Find the existing order by ID
|
||||||
|
const existingOrder = marketStore.orders[paymentData.id]
|
||||||
if (existingOrder) {
|
if (existingOrder) {
|
||||||
console.log('Order already exists, updating with new information:', existingOrder.id)
|
console.log('Found existing order, updating with payment request:', existingOrder.id)
|
||||||
|
|
||||||
// Update the existing order with any new information
|
// Try to extract actual expiry from bolt11
|
||||||
const updatedOrder = {
|
const actualExpiry = extractExpiryFromBolt11(lightningOption.link)
|
||||||
...existingOrder,
|
|
||||||
...event,
|
|
||||||
updatedAt: Math.floor(Date.now() / 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's invoice information, update it
|
// Create lightning invoice object
|
||||||
if (event.lightningInvoice) {
|
const lightningInvoice = {
|
||||||
updatedOrder.lightningInvoice = event.lightningInvoice
|
checking_id: '', // Will be extracted from bolt11 if needed
|
||||||
updatedOrder.paymentHash = event.paymentHash
|
payment_hash: '', // Will be extracted from bolt11 if needed
|
||||||
updatedOrder.paymentStatus = event.paymentStatus || 'pending'
|
wallet_id: '', // Not available from payment request
|
||||||
updatedOrder.paymentRequest = event.paymentRequest
|
amount: existingOrder.total,
|
||||||
}
|
fee: 0, // Not available from payment request
|
||||||
|
bolt11: lightningOption.link,
|
||||||
// Update the order in the store
|
|
||||||
marketStore.updateOrder(existingOrder.id, updatedOrder)
|
|
||||||
|
|
||||||
console.log('Updated existing order:', {
|
|
||||||
orderId: existingOrder.id,
|
|
||||||
hasInvoice: !!updatedOrder.lightningInvoice,
|
|
||||||
paymentStatus: updatedOrder.paymentStatus
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a basic order object from the event data
|
|
||||||
const orderData: Partial<Order> = {
|
|
||||||
id: event.orderId,
|
|
||||||
nostrEventId: event.id,
|
|
||||||
buyerPubkey: event.pubkey || '',
|
|
||||||
sellerPubkey: event.sellerPubkey || '',
|
|
||||||
items: event.items || [],
|
|
||||||
total: event.total || 0,
|
|
||||||
currency: event.currency || 'sat',
|
|
||||||
status: 'pending' as OrderStatus,
|
|
||||||
createdAt: event.createdAt || Date.now(),
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
// Add invoice details if present
|
|
||||||
...(event.lightningInvoice && {
|
|
||||||
lightningInvoice: {
|
|
||||||
checking_id: event.lightningInvoice.checking_id || event.lightningInvoice.payment_hash || '',
|
|
||||||
payment_hash: event.lightningInvoice.payment_hash || '',
|
|
||||||
wallet_id: event.lightningInvoice.wallet_id || '',
|
|
||||||
amount: event.lightningInvoice.amount || 0,
|
|
||||||
fee: event.lightningInvoice.fee || 0,
|
|
||||||
bolt11: event.lightningInvoice.bolt11 || event.lightningInvoice.payment_request || '',
|
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
memo: event.lightningInvoice.memo || '',
|
memo: paymentData.message || 'Payment for order',
|
||||||
expiry: event.lightningInvoice.expiry || '',
|
created_at: new Date().toISOString(),
|
||||||
preimage: event.lightningInvoice.preimage || '',
|
expiry: actualExpiry // Use actual expiry from bolt11 decoding
|
||||||
extra: event.lightningInvoice.extra || {},
|
}
|
||||||
created_at: event.lightningInvoice.created_at || '',
|
|
||||||
updated_at: event.lightningInvoice.updated_at || ''
|
// Update the order with the lightning invoice
|
||||||
},
|
marketStore.updateOrder(existingOrder.id, {
|
||||||
paymentHash: event.lightningInvoice.payment_hash || '',
|
lightningInvoice,
|
||||||
paymentStatus: 'pending',
|
status: 'pending',
|
||||||
paymentRequest: event.lightningInvoice.bolt11 || event.lightningInvoice.payment_request || '',
|
paymentRequest: lightningOption.link,
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('Order updated with payment request:', existingOrder.id)
|
||||||
|
} else {
|
||||||
|
console.warn('Order not found for payment request:', paymentData.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the order using the store method
|
|
||||||
const order = marketStore.createOrder(orderData)
|
|
||||||
|
|
||||||
console.log('Created order from market event:', {
|
|
||||||
orderId: order.id,
|
|
||||||
total: order.total,
|
|
||||||
status: order.status
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to handle market order:', error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start listening for order events
|
// Handle order status update (type 2)
|
||||||
const startListening = async () => {
|
const handleOrderStatusUpdate = async (statusData: OrderStatusEvent, _senderPubkey: string) => {
|
||||||
if (!isReady.value) {
|
console.log('Received order status update:', statusData)
|
||||||
console.warn('Cannot start listening: not ready')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// Update order status in local state
|
||||||
await subscribeToOrderEvents()
|
if (statusData.paid !== undefined) {
|
||||||
console.log('Started listening for order events')
|
console.log(`Order ${statusData.id} payment status: ${statusData.paid}`)
|
||||||
} catch (error) {
|
}
|
||||||
console.error('Failed to start listening for order events:', error)
|
if (statusData.shipped !== undefined) {
|
||||||
|
console.log(`Order ${statusData.id} shipping status: ${statusData.shipped}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop listening for order events
|
// Unsubscribe from order events
|
||||||
const stopListening = () => {
|
const unsubscribeFromOrderEvents = () => {
|
||||||
if (subscriptionId.value) {
|
if (subscriptionId.value) {
|
||||||
// Use the cleanup method from relayHub
|
|
||||||
relayHub.cleanup()
|
relayHub.cleanup()
|
||||||
subscriptionId.value = null
|
subscriptionId.value = null
|
||||||
}
|
}
|
||||||
isSubscribed.value = false
|
isSubscribed.value = false
|
||||||
console.log('Stopped listening for order events')
|
console.log('Unsubscribed from order events')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up old processed events
|
// Watch for ready state changes
|
||||||
const cleanupProcessedEvents = () => {
|
const watchReadyState = () => {
|
||||||
// const now = Date.now()
|
if (isReady.value && !isSubscribed.value) {
|
||||||
// const cutoff = now - (24 * 60 * 60 * 1000) // 24 hours ago
|
subscribeToOrderEvents()
|
||||||
|
} else if (!isReady.value && isSubscribed.value) {
|
||||||
|
unsubscribeFromOrderEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove old event IDs (this is a simple cleanup, could be more sophisticated)
|
// Watch for authentication changes
|
||||||
if (processedEventIds.value.size > 1000) {
|
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()
|
processedEventIds.value.clear()
|
||||||
console.log('Cleaned up processed event IDs')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
isSubscribed,
|
isSubscribed: computed(() => isSubscribed.value),
|
||||||
lastEventTimestamp,
|
isReady: computed(() => isReady.value),
|
||||||
|
lastEventTimestamp: computed(() => lastEventTimestamp.value),
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
startListening,
|
|
||||||
stopListening,
|
|
||||||
subscribeToOrderEvents,
|
subscribeToOrderEvents,
|
||||||
cleanupProcessedEvents
|
unsubscribeFromOrderEvents,
|
||||||
|
initialize,
|
||||||
|
cleanup,
|
||||||
|
watchReadyState,
|
||||||
|
watchAuthChanges
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance
|
|
||||||
export const orderEvents = useOrderEvents()
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
// Helper function to convert bech32 to hex
|
|
||||||
export function bech32ToHex(bech32Key: string): string {
|
|
||||||
if (bech32Key.startsWith('npub1') || bech32Key.startsWith('nsec1')) {
|
|
||||||
// Import bech32 conversion dynamically to avoid bundling issues
|
|
||||||
const { bech32Decode, convertbits } = require('bech32')
|
|
||||||
const [, data] = bech32Decode(bech32Key)
|
|
||||||
if (!data) {
|
|
||||||
throw new Error(`Invalid bech32 key: ${bech32Key}`)
|
|
||||||
}
|
|
||||||
const converted = convertbits(data, 5, 8, false)
|
|
||||||
if (!converted) {
|
|
||||||
throw new Error(`Failed to convert bech32 key: ${bech32Key}`)
|
|
||||||
}
|
|
||||||
return Buffer.from(converted).toString('hex')
|
|
||||||
}
|
|
||||||
// Already hex format
|
|
||||||
return bech32Key
|
|
||||||
}
|
|
||||||
|
|
@ -82,7 +82,7 @@ class InvoiceService {
|
||||||
amount: order.total,
|
amount: order.total,
|
||||||
unit: 'sat',
|
unit: 'sat',
|
||||||
memo: `Order ${order.id} - ${order.items.length} items`,
|
memo: `Order ${order.id} - ${order.items.length} items`,
|
||||||
expiry: 3600, // 1 hour
|
expiry: extra?.expiry || 3600, // Allow configurable expiry, default 1 hour
|
||||||
extra: {
|
extra: {
|
||||||
tag: 'nostrmarket', // Use nostrmarket tag for compatibility
|
tag: 'nostrmarket', // Use nostrmarket tag for compatibility
|
||||||
order_id: extra?.order_id || order.id, // Use passed order_id or fallback to order.id
|
order_id: extra?.order_id || order.id, // Use passed order_id or fallback to order.id
|
||||||
|
|
@ -113,6 +113,8 @@ class InvoiceService {
|
||||||
console.log('Full LNBits response:', response)
|
console.log('Full LNBits response:', response)
|
||||||
console.log('Response type:', typeof response)
|
console.log('Response type:', typeof response)
|
||||||
console.log('Response keys:', Object.keys(response))
|
console.log('Response keys:', Object.keys(response))
|
||||||
|
console.log('Response expiry field:', response.expiry)
|
||||||
|
console.log('Response created_at field:', response.created_at)
|
||||||
|
|
||||||
// Check if we have the expected fields
|
// Check if we have the expected fields
|
||||||
if (!response.bolt11) {
|
if (!response.bolt11) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { finalizeEvent, type EventTemplate, nip04 } from 'nostr-tools'
|
||||||
import { relayHub } from '@/lib/nostr/relayHub'
|
import { relayHub } from '@/lib/nostr/relayHub'
|
||||||
import { auth } from '@/composables/useAuth'
|
import { auth } from '@/composables/useAuth'
|
||||||
import type { Stall, Product, Order } from '@/stores/market'
|
import type { Stall, Product, Order } from '@/stores/market'
|
||||||
import { bech32ToHex } from '@/lib/utils/bech32'
|
|
||||||
|
|
||||||
export interface NostrmarketStall {
|
export interface NostrmarketStall {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -69,24 +68,49 @@ export interface NostrmarketOrderStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NostrmarketService {
|
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() {
|
private getAuth() {
|
||||||
if (!auth.isAuthenticated.value || !auth.currentUser.value?.prvkey) {
|
if (!auth.isAuthenticated.value || !auth.currentUser.value?.prvkey) {
|
||||||
throw new Error('User not authenticated or private key not available')
|
throw new Error('User not authenticated or private key not available')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert bech32 keys to hex format if needed
|
const pubkey = auth.currentUser.value.pubkey
|
||||||
const originalPubkey = auth.currentUser.value.pubkey
|
const prvkey = auth.currentUser.value.prvkey
|
||||||
const originalPrvkey = auth.currentUser.value.prvkey
|
|
||||||
const pubkey = bech32ToHex(originalPubkey)
|
|
||||||
const prvkey = bech32ToHex(originalPrvkey)
|
|
||||||
|
|
||||||
console.log('🔑 Key conversion debug:', {
|
if (!pubkey || !prvkey) {
|
||||||
originalPubkey: originalPubkey?.substring(0, 10) + '...',
|
throw new Error('Public key or private key is missing')
|
||||||
originalPrvkey: originalPrvkey?.substring(0, 10) + '...',
|
}
|
||||||
convertedPubkey: pubkey.substring(0, 10) + '...',
|
|
||||||
convertedPrvkey: prvkey.substring(0, 10) + '...',
|
// 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),
|
pubkeyIsHex: /^[0-9a-fA-F]{64}$/.test(pubkey),
|
||||||
prvkeyIsHex: /^[0-9a-fA-F]{64}$/.test(prvkey)
|
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 {
|
return {
|
||||||
|
|
@ -99,7 +123,7 @@ export class NostrmarketService {
|
||||||
* Publish a stall event (kind 30017) to Nostr
|
* Publish a stall event (kind 30017) to Nostr
|
||||||
*/
|
*/
|
||||||
async publishStall(stall: Stall): Promise<string> {
|
async publishStall(stall: Stall): Promise<string> {
|
||||||
const { pubkey, prvkey } = this.getAuth()
|
const { prvkey } = this.getAuth()
|
||||||
|
|
||||||
const stallData: NostrmarketStall = {
|
const stallData: NostrmarketStall = {
|
||||||
id: stall.id,
|
id: stall.id,
|
||||||
|
|
@ -124,23 +148,24 @@ export class NostrmarketService {
|
||||||
created_at: Math.floor(Date.now() / 1000)
|
created_at: Math.floor(Date.now() / 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = finalizeEvent(eventTemplate, prvkey)
|
const prvkeyBytes = this.hexToUint8Array(prvkey)
|
||||||
const eventId = await relayHub.publishEvent(event)
|
const event = finalizeEvent(eventTemplate, prvkeyBytes)
|
||||||
|
const result = await relayHub.publishEvent(event)
|
||||||
|
|
||||||
console.log('Stall published to nostrmarket:', {
|
console.log('Stall published to nostrmarket:', {
|
||||||
stallId: stall.id,
|
stallId: stall.id,
|
||||||
eventId: eventId,
|
eventId: result,
|
||||||
content: stallData
|
content: stallData
|
||||||
})
|
})
|
||||||
|
|
||||||
return eventId
|
return result.success.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publish a product event (kind 30018) to Nostr
|
* Publish a product event (kind 30018) to Nostr
|
||||||
*/
|
*/
|
||||||
async publishProduct(product: Product): Promise<string> {
|
async publishProduct(product: Product): Promise<string> {
|
||||||
const { pubkey, prvkey } = this.getAuth()
|
const { prvkey } = this.getAuth()
|
||||||
|
|
||||||
const productData: NostrmarketProduct = {
|
const productData: NostrmarketProduct = {
|
||||||
id: product.id,
|
id: product.id,
|
||||||
|
|
@ -166,23 +191,24 @@ export class NostrmarketService {
|
||||||
created_at: Math.floor(Date.now() / 1000)
|
created_at: Math.floor(Date.now() / 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = finalizeEvent(eventTemplate, prvkey)
|
const prvkeyBytes = this.hexToUint8Array(prvkey)
|
||||||
const eventId = await relayHub.publishEvent(event)
|
const event = finalizeEvent(eventTemplate, prvkeyBytes)
|
||||||
|
const result = await relayHub.publishEvent(event)
|
||||||
|
|
||||||
console.log('Product published to nostrmarket:', {
|
console.log('Product published to nostrmarket:', {
|
||||||
productId: product.id,
|
productId: product.id,
|
||||||
eventId: eventId,
|
eventId: result,
|
||||||
content: productData
|
content: productData
|
||||||
})
|
})
|
||||||
|
|
||||||
return eventId
|
return result.success.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publish an order event (kind 4 encrypted DM) to nostrmarket
|
* Publish an order event (kind 4 encrypted DM) to nostrmarket
|
||||||
*/
|
*/
|
||||||
async publishOrder(order: Order, merchantPubkey: string): Promise<string> {
|
async publishOrder(order: Order, merchantPubkey: string): Promise<string> {
|
||||||
const { pubkey, prvkey } = this.getAuth()
|
const { prvkey } = this.getAuth()
|
||||||
|
|
||||||
// Convert order to nostrmarket format - exactly matching the specification
|
// Convert order to nostrmarket format - exactly matching the specification
|
||||||
const orderData = {
|
const orderData = {
|
||||||
|
|
@ -205,7 +231,27 @@ export class NostrmarketService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt the message using NIP-04
|
// Encrypt the message using NIP-04
|
||||||
const encryptedContent = await nip04.encrypt(prvkey, merchantPubkey, JSON.stringify(orderData))
|
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 = {
|
const eventTemplate: EventTemplate = {
|
||||||
kind: 4, // Encrypted DM
|
kind: 4, // Encrypted DM
|
||||||
|
|
@ -214,18 +260,36 @@ export class NostrmarketService {
|
||||||
created_at: Math.floor(Date.now() / 1000)
|
created_at: Math.floor(Date.now() / 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = finalizeEvent(eventTemplate, prvkey)
|
console.log('🔧 finalizeEvent debug:', {
|
||||||
const eventId = await relayHub.publishEvent(event)
|
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:', {
|
console.log('Order published to nostrmarket:', {
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
eventId: eventId,
|
eventId: result,
|
||||||
merchantPubkey,
|
merchantPubkey,
|
||||||
content: orderData,
|
content: orderData,
|
||||||
encryptedContent: encryptedContent.substring(0, 50) + '...'
|
encryptedContent: encryptedContent.substring(0, 50) + '...'
|
||||||
})
|
})
|
||||||
|
|
||||||
return eventId
|
return result.success.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { nip19 } from 'nostr-tools'
|
|
||||||
|
|
||||||
// Helper function to convert bech32 to hex using nostr-tools
|
|
||||||
export function bech32ToHex(bech32Key: string): string {
|
|
||||||
if (bech32Key.startsWith('npub1') || bech32Key.startsWith('nsec1')) {
|
|
||||||
const { type, data } = nip19.decode(bech32Key)
|
|
||||||
return data as string
|
|
||||||
}
|
|
||||||
// Already hex format
|
|
||||||
return bech32Key
|
|
||||||
}
|
|
||||||
|
|
@ -307,10 +307,11 @@ import { Badge } from '@/components/ui/badge'
|
||||||
import { Package, Wallet } from 'lucide-vue-next'
|
import { Package, Wallet } from 'lucide-vue-next'
|
||||||
import type { OrderStatus } from '@/stores/market'
|
import type { OrderStatus } from '@/stores/market'
|
||||||
import PaymentDisplay from '@/components/market/PaymentDisplay.vue'
|
import PaymentDisplay from '@/components/market/PaymentDisplay.vue'
|
||||||
import { orderEvents } from '@/composables/useOrderEvents'
|
import { useOrderEvents } from '@/composables/useOrderEvents'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const marketStore = useMarketStore()
|
const marketStore = useMarketStore()
|
||||||
|
const orderEvents = useOrderEvents()
|
||||||
|
|
||||||
// Local state
|
// Local state
|
||||||
const statusFilter = ref('')
|
const statusFilter = ref('')
|
||||||
|
|
@ -419,7 +420,7 @@ onMounted(() => {
|
||||||
|
|
||||||
// Start listening for order events if not already listening
|
// Start listening for order events if not already listening
|
||||||
if (!orderEvents.isSubscribed.value) {
|
if (!orderEvents.isSubscribed.value) {
|
||||||
orderEvents.startListening()
|
orderEvents.initialize()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed, readonly } from 'vue'
|
import { ref, computed, readonly, watch } from 'vue'
|
||||||
import { nostrOrders } from '@/composables/useNostrOrders'
|
import { nostrOrders } from '@/composables/useNostrOrders'
|
||||||
import { invoiceService } from '@/lib/services/invoiceService'
|
import { invoiceService } from '@/lib/services/invoiceService'
|
||||||
import { paymentMonitor } from '@/lib/services/paymentMonitor'
|
import { paymentMonitor } from '@/lib/services/paymentMonitor'
|
||||||
import { nostrmarketService } from '@/lib/services/nostrmarketService'
|
import { nostrmarketService } from '@/lib/services/nostrmarketService'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import type { LightningInvoice } from '@/lib/services/invoiceService'
|
import type { LightningInvoice } from '@/lib/services/invoiceService'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -85,6 +86,7 @@ export interface Order {
|
||||||
qrCodeDataUrl?: string
|
qrCodeDataUrl?: string
|
||||||
qrCodeLoading?: boolean
|
qrCodeLoading?: boolean
|
||||||
qrCodeError?: string | null
|
qrCodeError?: string | null
|
||||||
|
showQRCode?: boolean // Toggle QR code visibility
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderItem {
|
export interface OrderItem {
|
||||||
|
|
@ -166,6 +168,13 @@ export interface PaymentStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMarketStore = defineStore('market', () => {
|
export const useMarketStore = defineStore('market', () => {
|
||||||
|
const auth = useAuth()
|
||||||
|
|
||||||
|
// Helper function to get user-specific storage key
|
||||||
|
const getUserStorageKey = (baseKey: string) => {
|
||||||
|
const userPubkey = auth.currentUser?.value?.pubkey
|
||||||
|
return userPubkey ? `${baseKey}_${userPubkey}` : baseKey
|
||||||
|
}
|
||||||
// Core market state
|
// Core market state
|
||||||
const markets = ref<Market[]>([])
|
const markets = ref<Market[]>([])
|
||||||
const stalls = ref<Stall[]>([])
|
const stalls = ref<Stall[]>([])
|
||||||
|
|
@ -807,7 +816,9 @@ export const useMarketStore = defineStore('market', () => {
|
||||||
// Persistence methods
|
// Persistence methods
|
||||||
const saveOrdersToStorage = () => {
|
const saveOrdersToStorage = () => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('market_orders', JSON.stringify(orders.value))
|
const storageKey = getUserStorageKey('market_orders')
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(orders.value))
|
||||||
|
console.log('Saved orders to localStorage with key:', storageKey)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to save orders to localStorage:', error)
|
console.warn('Failed to save orders to localStorage:', error)
|
||||||
}
|
}
|
||||||
|
|
@ -815,17 +826,30 @@ export const useMarketStore = defineStore('market', () => {
|
||||||
|
|
||||||
const loadOrdersFromStorage = () => {
|
const loadOrdersFromStorage = () => {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem('market_orders')
|
const storageKey = getUserStorageKey('market_orders')
|
||||||
|
const stored = localStorage.getItem(storageKey)
|
||||||
if (stored) {
|
if (stored) {
|
||||||
const parsedOrders = JSON.parse(stored)
|
const parsedOrders = JSON.parse(stored)
|
||||||
orders.value = parsedOrders
|
orders.value = parsedOrders
|
||||||
console.log('Loaded orders from localStorage:', Object.keys(parsedOrders).length)
|
console.log('Loaded orders from localStorage with key:', storageKey, 'Orders count:', Object.keys(parsedOrders).length)
|
||||||
|
} else {
|
||||||
|
console.log('No orders found in localStorage for key:', storageKey)
|
||||||
|
// Clear any existing orders when switching users
|
||||||
|
orders.value = {}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load orders from localStorage:', error)
|
console.warn('Failed to load orders from localStorage:', error)
|
||||||
|
// Clear orders on error
|
||||||
|
orders.value = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear orders when user changes
|
||||||
|
const clearOrdersForUserChange = () => {
|
||||||
|
orders.value = {}
|
||||||
|
console.log('Cleared orders for user change')
|
||||||
|
}
|
||||||
|
|
||||||
// Payment utility methods
|
// Payment utility methods
|
||||||
const calculateOrderTotal = (cart: StallCart, shippingZone?: ShippingZone) => {
|
const calculateOrderTotal = (cart: StallCart, shippingZone?: ShippingZone) => {
|
||||||
const subtotal = cart.subtotal
|
const subtotal = cart.subtotal
|
||||||
|
|
@ -918,6 +942,15 @@ export const useMarketStore = defineStore('market', () => {
|
||||||
// Initialize orders from localStorage
|
// Initialize orders from localStorage
|
||||||
loadOrdersFromStorage()
|
loadOrdersFromStorage()
|
||||||
|
|
||||||
|
// Watch for user changes and reload orders
|
||||||
|
watch(() => auth.currentUser?.value?.pubkey, (newPubkey, oldPubkey) => {
|
||||||
|
if (newPubkey !== oldPubkey) {
|
||||||
|
console.log('User changed, clearing and reloading orders. Old:', oldPubkey, 'New:', newPubkey)
|
||||||
|
clearOrdersForUserChange()
|
||||||
|
loadOrdersFromStorage()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
markets: readonly(markets),
|
markets: readonly(markets),
|
||||||
|
|
@ -997,6 +1030,7 @@ export const useMarketStore = defineStore('market', () => {
|
||||||
updateOrder,
|
updateOrder,
|
||||||
saveOrdersToStorage,
|
saveOrdersToStorage,
|
||||||
loadOrdersFromStorage,
|
loadOrdersFromStorage,
|
||||||
|
clearOrdersForUserChange,
|
||||||
publishToNostrmarket
|
publishToNostrmarket
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue