Remove useNostrOrders composable and related Checkout page

- Delete the useNostrOrders composable as it is no longer needed.
- Update MerchantStore.vue to utilize nostrmarketService for publishing orders instead of the removed composable.
- Refactor market store to check the readiness of nostrmarketService instead of useNostrOrders.
- Remove the Checkout.vue page, streamlining the checkout process and improving code maintainability.
This commit is contained in:
padreug 2025-09-05 04:22:54 +02:00
parent e504b1f7e2
commit 36638d1080
6 changed files with 16 additions and 701 deletions

View file

@ -1,438 +0,0 @@
<template>
<div class="container mx-auto px-4 py-8">
<!-- Loading State -->
<div v-if="!isReady" class="flex justify-center items-center min-h-64">
<div class="flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
<p class="text-muted-foreground">Loading checkout...</p>
</div>
</div>
<!-- Error State -->
<div v-else-if="error" class="text-center py-12">
<h2 class="text-2xl font-bold text-red-600 mb-4">Checkout Error</h2>
<p class="text-muted-foreground mb-4">{{ error }}</p>
<Button @click="$router.push('/cart')" variant="outline">
Back to Cart
</Button>
</div>
<!-- Checkout Content -->
<div v-else-if="checkoutCart && checkoutStall" class="max-w-4xl mx-auto">
<!-- Page Header -->
<div class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-foreground">Checkout</h1>
<p class="text-muted-foreground mt-2">
Complete your purchase from {{ checkoutStall.name }}
</p>
</div>
<Button @click="$router.push('/cart')" variant="outline">
Back to Cart
</Button>
</div>
</div>
<!-- Checkout Grid -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Main Checkout Form -->
<div class="lg:col-span-2 space-y-6">
<!-- Stall Information -->
<div class="bg-card border rounded-lg p-6">
<div class="flex items-center space-x-4 mb-4">
<div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
<Store class="w-6 h-6 text-primary" />
</div>
<div>
<h3 class="text-lg font-semibold text-foreground">{{ checkoutStall.name }}</h3>
<p v-if="checkoutStall.description" class="text-muted-foreground">
{{ checkoutStall.description }}
</p>
</div>
</div>
</div>
<!-- Contact Information -->
<div class="bg-card border rounded-lg p-6">
<h3 class="text-lg font-semibold text-foreground mb-4">Contact Information</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-foreground mb-2">
Email (optional)
</label>
<Input
v-model="contactInfo.email"
type="email"
placeholder="your@email.com"
class="w-full"
/>
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-2">
Message to Merchant (optional)
</label>
<textarea
v-model="contactInfo.message"
rows="3"
placeholder="Any special requests or notes for the merchant..."
class="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring bg-background text-foreground"
></textarea>
</div>
</div>
</div>
<!-- Shipping Information -->
<div class="bg-card border rounded-lg p-6">
<h3 class="text-lg font-semibold text-foreground mb-4">Shipping Information</h3>
<!-- Shipping Zone Selection -->
<div class="mb-4">
<label class="block text-sm font-medium text-foreground mb-2">
Shipping Zone
</label>
<div v-if="availableShippingZones.length > 0" class="space-y-2">
<div
v-for="zone in availableShippingZones"
:key="zone.id"
@click="selectShippingZone(zone)"
class="flex items-center justify-between p-3 border rounded cursor-pointer hover:bg-muted/50"
:class="{ 'border-primary bg-primary/10': selectedShippingZone?.id === zone.id }"
>
<div>
<p class="font-medium text-foreground">{{ zone.name }}</p>
<p v-if="zone.description" class="text-sm text-muted-foreground">
{{ zone.description }}
</p>
<p v-if="zone.estimatedDays" class="text-xs text-muted-foreground">
Estimated: {{ zone.estimatedDays }}
</p>
</div>
<p class="font-semibold text-foreground">
{{ formatPrice(zone.cost, zone.currency) }}
</p>
</div>
</div>
<p v-else class="text-sm text-muted-foreground">
No shipping zones available for this stall.
</p>
</div>
<!-- Shipping Address (only show for physical shipping) -->
<div v-if="selectedShippingZone && requiresPhysicalShipping" class="mt-4">
<label class="block text-sm font-medium text-foreground mb-2">
Shipping Address <span class="text-red-500">*</span>
</label>
<textarea
v-model="contactInfo.address"
rows="3"
placeholder="Enter your shipping address..."
class="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring bg-background text-foreground"
required
></textarea>
<p class="text-sm text-muted-foreground mt-1">
Required for physical product delivery
</p>
</div>
<!-- Digital Delivery Note -->
<div v-if="selectedShippingZone && !requiresPhysicalShipping" class="mt-4 p-3 bg-muted/50 border border-border rounded-md">
<div class="flex items-center space-x-2">
<div class="w-5 h-5 text-muted-foreground">📧</div>
<div class="text-sm text-muted-foreground">
<p class="font-medium text-foreground">Digital Delivery</p>
<p>This product will be delivered digitally. No shipping address required.</p>
</div>
</div>
</div>
</div>
<!-- Payment Method -->
<div class="bg-card border rounded-lg p-6">
<h3 class="text-lg font-semibold text-foreground mb-4">Payment Method</h3>
<div class="space-y-3">
<div class="flex items-center space-x-3">
<input
type="radio"
id="lightning"
v-model="paymentMethod"
value="lightning"
class="text-primary focus:ring-primary"
/>
<label for="lightning" class="text-sm font-medium text-foreground">
Lightning Network (Recommended)
</label>
</div>
<div class="flex items-center space-x-3">
<input
type="radio"
id="btc_onchain"
v-model="paymentMethod"
value="btc_onchain"
class="text-primary focus:ring-primary"
/>
<label for="btc_onchain" class="text-sm font-medium text-foreground">
Bitcoin Onchain
</label>
</div>
</div>
</div>
<!-- Place Order Button -->
<div class="bg-card border rounded-lg p-6">
<!-- Nostr Status Indicator -->
<div class="mb-4 p-3 rounded-lg border" :class="{
'bg-green-500/10 border-green-200': nostrOrders.isReady.value,
'bg-yellow-500/10 border-yellow-200': !nostrOrders.isReady.value
}">
<div class="flex items-center gap-2 text-sm">
<div v-if="nostrOrders.isReady.value" class="flex items-center gap-2 text-green-700">
<Wifi class="w-4 h-4" />
<span>Connected to Nostr network</span>
</div>
<div v-else class="flex items-center gap-2 text-yellow-700">
<WifiOff class="w-4 h-4" />
<span>Nostr network unavailable</span>
</div>
</div>
<p v-if="!nostrOrders.isReady.value" class="text-xs text-yellow-600 mt-1">
Orders will be stored locally only. Please log in to send orders to merchants.
</p>
<!-- Test Encryption Button -->
<div v-if="nostrOrders.isReady.value" class="mt-3">
<Button
@click="testEncryption"
variant="outline"
size="sm"
:disabled="isTestingEncryption"
class="text-xs"
>
<div v-if="isTestingEncryption" class="flex items-center gap-2">
<div class="animate-spin rounded-full h-3 w-3 border-b-2 border-primary"></div>
<span>Testing...</span>
</div>
<div v-else class="flex items-center gap-2">
<span>Test NIP-04 Encryption</span>
</div>
</Button>
<p v-if="encryptionTestResult" class="text-xs mt-1" :class="{
'text-green-600': encryptionTestResult === 'success',
'text-red-600': encryptionTestResult === 'error'
}">
{{ encryptionTestResult === 'success' ? '✓ Encryption test passed' : '✗ Encryption test failed' }}
</p>
</div>
</div>
<Button
@click="handleCheckout"
:disabled="!canProceedToCheckout || isPlacingOrder"
variant="default"
class="w-full"
size="lg"
>
<div v-if="isPlacingOrder" class="flex items-center space-x-2">
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<span>Placing Order...</span>
</div>
<div v-else class="flex items-center space-x-2">
<Lock class="w-4 h-4" />
<span>Place Order</span>
</div>
</Button>
<p v-if="error" class="text-sm text-red-600 mt-2 text-center">
{{ error }}
</p>
</div>
</div>
<!-- Order Summary Sidebar -->
<div class="lg:col-span-1">
<div class="sticky top-8">
<CartSummary
:stall-id="checkoutCart.id"
:cart-items="checkoutCart.products"
:subtotal="checkoutCart.subtotal"
:currency="checkoutCart.currency"
:available-shipping-zones="availableShippingZones"
:selected-shipping-zone="selectedShippingZone || undefined"
@shipping-zone-selected="selectShippingZone"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useMarketStore } from '@/stores/market'
import { nostrOrders } from '@/composables/useNostrOrders'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Store, Lock, Wifi, WifiOff } from 'lucide-vue-next'
import CartSummary from '@/components/market/CartSummary.vue'
import type { ShippingZone, ContactInfo } from '@/stores/market'
import { useAuth } from '@/composables/useAuth'
const route = useRoute()
const router = useRouter()
const marketStore = useMarketStore()
const auth = useAuth()
// Route parameters
const stallId = route.params.stallId as string
// Local state
const contactInfo = ref<ContactInfo>({
email: '',
message: '',
address: ''
})
const paymentMethod = ref<'lightning' | 'btc_onchain'>('lightning')
const selectedShippingZone = ref<ShippingZone | null>(null)
const error = ref<string | null>(null)
const isPlacingOrder = ref(false)
const isTestingEncryption = ref(false)
const encryptionTestResult = ref<'success' | 'error' | null>(null)
// Computed properties
const checkoutCart = computed(() => marketStore.checkoutCart)
const checkoutStall = computed(() => marketStore.checkoutStall)
const availableShippingZones = computed(() => {
if (!checkoutStall.value) return []
return checkoutStall.value.shipping || []
})
const isReady = computed(() => {
return checkoutCart.value && checkoutStall.value
})
const requiresPhysicalShipping = computed(() => {
return selectedShippingZone.value?.requiresPhysicalShipping || false
})
const canProceedToCheckout = computed(() => {
return selectedShippingZone.value && (requiresPhysicalShipping.value ? contactInfo.value.address : true)
})
// Methods
const selectShippingZone = (shippingZone: ShippingZone) => {
selectedShippingZone.value = shippingZone
if (checkoutCart.value) {
marketStore.setShippingZone(checkoutCart.value.id, shippingZone)
}
}
const handleCheckout = async () => {
// Validate required fields
if (!selectedShippingZone.value) {
error.value = 'Please select a shipping zone'
return
}
if (requiresPhysicalShipping.value && !contactInfo.value.address) {
error.value = 'Please provide a shipping address'
return
}
try {
// Clear any previous errors
error.value = null
isPlacingOrder.value = true
// Create the order
const order = await marketStore.createAndPlaceOrder({
cartId: checkoutCart.value!.id,
stallId: checkoutCart.value!.id,
buyerPubkey: auth.currentUser?.value?.pubkey || '', // Get from authenticated user
sellerPubkey: checkoutStall.value!.pubkey,
status: 'pending',
items: checkoutCart.value!.products.map(item => ({
productId: item.product.id,
productName: item.product.name,
quantity: item.quantity,
price: item.product.price,
currency: item.product.currency
})),
contactInfo: contactInfo.value,
shippingZone: selectedShippingZone.value,
paymentMethod: paymentMethod.value,
subtotal: checkoutCart.value!.subtotal,
shippingCost: selectedShippingZone.value.cost,
total: checkoutCart.value!.subtotal + selectedShippingZone.value.cost,
currency: checkoutCart.value!.currency
})
// Show success message
console.log('Order placed successfully:', order)
// TODO: Navigate to payment page or show payment modal
// For now, redirect to cart with success message
router.push({
path: '/cart',
query: { orderSuccess: 'true', orderId: order.id }
})
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to place order'
console.error('Order placement failed:', err)
} finally {
isPlacingOrder.value = false
}
}
const testEncryption = async () => {
try {
isTestingEncryption.value = true
encryptionTestResult.value = null
const success = await nostrOrders.testEncryption()
encryptionTestResult.value = success ? 'success' : 'error'
if (success) {
console.log('NIP-04 encryption test passed!')
} else {
console.error('NIP-04 encryption test failed!')
}
} catch (error) {
console.error('Encryption test error:', error)
encryptionTestResult.value = 'error'
} finally {
isTestingEncryption.value = false
}
}
const formatPrice = (price: number, currency: string) => {
if (currency === 'sats' || currency === 'sat') {
return `${price.toLocaleString()} sats`
}
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.toUpperCase()
}).format(price)
}
// Initialize checkout
onMounted(() => {
if (!stallId) {
error.value = 'No stall ID provided'
return
}
// Set the checkout cart for this stall
marketStore.setCheckoutCart(stallId)
// Auto-select shipping zone if only one available
if (availableShippingZones.value.length === 1) {
selectShippingZone(availableShippingZones.value[0])
}
})
</script>