Enhance shipping zone functionality in CheckoutPage and market types

- Add 'shipping_zones' property to the Stall interface to support LNbits format.
- Update ShippingZone interface to include optional 'countries' property for better zone coverage representation.
- Modify CheckoutPage.vue to display shipping zone details, including a message for zones that do not require physical shipping.
- Improve user feedback by updating messages related to shipping address requirements based on selected shipping zone.
- Refactor logic to auto-select shipping zones and handle cases where no shipping zones are available, enhancing user experience during checkout.
This commit is contained in:
padreug 2025-09-05 03:45:40 +02:00
parent dc6a9ed283
commit 143c8afcc3
2 changed files with 77 additions and 18 deletions

View file

@ -21,6 +21,7 @@ export interface Stall {
logo?: string logo?: string
categories?: string[] categories?: string[]
shipping?: ShippingZone[] shipping?: ShippingZone[]
shipping_zones?: ShippingZone[] // LNbits format
currency: string currency: string
nostrEventId?: string nostrEventId?: string
} }
@ -94,6 +95,7 @@ export interface ShippingZone {
name: string name: string
cost: number cost: number
currency: string currency: string
countries?: string[] // Countries/regions this zone covers
description?: string description?: string
estimatedDays?: string estimatedDays?: string
requiresPhysicalShipping?: boolean requiresPhysicalShipping?: boolean

View file

@ -110,7 +110,15 @@
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div> <div>
<h3 class="font-medium text-foreground">{{ zone.name }}</h3> <h3 class="font-medium text-foreground">{{ zone.name }}</h3>
<p class="text-sm text-muted-foreground">{{ zone.countries?.join(', ') || 'Available' }}</p> <p class="text-sm text-muted-foreground">
{{ zone.countries?.join(', ') || 'Available' }}
<span v-if="!zone.requiresPhysicalShipping" class="ml-2 text-blue-600">
No shipping required
</span>
</p>
<p v-if="zone.description" class="text-xs text-muted-foreground mt-1">
{{ zone.description }}
</p>
</div> </div>
<div class="text-right"> <div class="text-right">
<p class="font-semibold text-foreground"> <p class="font-semibold text-foreground">
@ -123,14 +131,15 @@
</div> </div>
<div v-else class="text-center py-6"> <div v-else class="text-center py-6">
<p class="text-muted-foreground">No shipping zones available</p> <p class="text-muted-foreground">This merchant hasn't configured shipping zones yet.</p>
<p class="text-sm text-muted-foreground">Please contact the merchant for shipping information.</p>
</div> </div>
<!-- Confirm Order Button --> <!-- Confirm Order Button -->
<div class="mt-6"> <div class="mt-6">
<Button <Button
@click="confirmOrder" @click="confirmOrder"
:disabled="!selectedShippingZone" :disabled="availableShippingZones.length > 0 && !selectedShippingZone"
class="w-full" class="w-full"
size="lg" size="lg"
> >
@ -172,14 +181,22 @@
</div> </div>
<div> <div>
<Label for="address">Shipping Address</Label> <Label for="address">
{{ selectedShippingZone?.requiresPhysicalShipping !== false ? 'Shipping Address' : 'Contact Address (optional)' }}
</Label>
<Textarea <Textarea
id="address" id="address"
v-model="contactData.address" v-model="contactData.address"
placeholder="Full shipping address..." :placeholder="selectedShippingZone?.requiresPhysicalShipping !== false
? 'Full shipping address...'
: 'Contact address (optional)...'"
rows="3" rows="3"
/> />
<p class="text-xs text-muted-foreground mt-1">Required for physical shipping</p> <p class="text-xs text-muted-foreground mt-1">
{{ selectedShippingZone?.requiresPhysicalShipping !== false
? 'Required for physical delivery'
: 'Optional for digital items or pickup' }}
</p>
</div> </div>
<div> <div>
@ -216,15 +233,16 @@
<div class="pt-4 border-t border-border"> <div class="pt-4 border-t border-border">
<Button <Button
@click="placeOrder" @click="placeOrder"
:disabled="isPlacingOrder || !contactData.address" :disabled="isPlacingOrder || (selectedShippingZone?.requiresPhysicalShipping !== false && !contactData.address)"
class="w-full" class="w-full"
size="lg" size="lg"
> >
<span v-if="isPlacingOrder" class="animate-spin mr-2"></span> <span v-if="isPlacingOrder" class="animate-spin mr-2"></span>
Place Order - {{ formatPrice(orderTotal, checkoutCart.currency) }} Place Order - {{ formatPrice(orderTotal, checkoutCart.currency) }}
</Button> </Button>
<p v-if="!contactData.address" class="text-xs text-destructive mt-2 text-center"> <p v-if="selectedShippingZone?.requiresPhysicalShipping !== false && !contactData.address"
Shipping address is required class="text-xs text-destructive mt-2 text-center">
Shipping address is required for physical delivery
</p> </p>
</div> </div>
</CardContent> </CardContent>
@ -308,6 +326,11 @@ const stallName = computed(() => {
return stall?.name || 'Unknown Stall' return stall?.name || 'Unknown Stall'
}) })
const currentStall = computed(() => {
if (!checkoutCart.value) return null
return marketStore.stalls.find(s => s.id === checkoutCart.value?.id)
})
const orderSubtotal = computed(() => { const orderSubtotal = computed(() => {
if (!checkoutCart.value?.products) return 0 if (!checkoutCart.value?.products) return 0
return checkoutCart.value.products.reduce((total, item) => return checkoutCart.value.products.reduce((total, item) =>
@ -321,12 +344,36 @@ const orderTotal = computed(() => {
return subtotal + shipping return subtotal + shipping
}) })
// Mock shipping zones (in real app, would come from stall data) // Get shipping zones from the current stall
const availableShippingZones = computed(() => [ const availableShippingZones = computed(() => {
{ id: 'local', name: 'Local Delivery', cost: 500, countries: ['Local Area'] }, if (!currentStall.value) return []
{ id: 'national', name: 'National Shipping', cost: 2000, countries: ['Domestic'] },
{ id: 'international', name: 'International', cost: 5000, countries: ['Worldwide'] } // Check if stall has shipping_zones (LNbits format) or shipping (nostr-market-app format)
]) const zones = currentStall.value.shipping_zones || currentStall.value.shipping || []
// Ensure zones have required properties and determine shipping requirements
return zones.map(zone => {
const zoneName = zone.name || 'Shipping Zone'
const lowerName = zoneName.toLowerCase()
// Determine if this zone requires physical shipping
const requiresPhysicalShipping = zone.requiresPhysicalShipping !== false &&
!lowerName.includes('digital') &&
!lowerName.includes('pickup') &&
!lowerName.includes('download') &&
zone.cost > 0 // Free usually means digital or pickup
return {
id: zone.id || zoneName.toLowerCase().replace(/\s+/g, '-'),
name: zoneName,
cost: Number(zone.cost) || 0,
countries: zone.countries || [],
currency: zone.currency || currentStall.value?.currency || 'sats',
requiresPhysicalShipping,
description: zone.description
}
})
})
// Methods // Methods
const selectShippingZone = (zone: any) => { const selectShippingZone = (zone: any) => {
@ -334,7 +381,8 @@ const selectShippingZone = (zone: any) => {
} }
const confirmOrder = () => { const confirmOrder = () => {
if (!selectedShippingZone.value) { // Allow proceeding if no shipping zones are available (e.g., for digital goods)
if (availableShippingZones.value.length > 0 && !selectedShippingZone.value) {
error.value = 'Please select a shipping zone' error.value = 'Please select a shipping zone'
return return
} }
@ -342,8 +390,11 @@ const confirmOrder = () => {
} }
const placeOrder = async () => { const placeOrder = async () => {
if (!contactData.value.address) { // Only require shipping address if selected zone requires physical shipping
error.value = 'Shipping address is required' const requiresShippingAddress = selectedShippingZone.value?.requiresPhysicalShipping !== false
if (requiresShippingAddress && !contactData.value.address) {
error.value = 'Shipping address is required for this delivery method'
return return
} }
@ -392,6 +443,12 @@ onMounted(() => {
if (!cart || cart.id !== stallId.value) { if (!cart || cart.id !== stallId.value) {
error.value = 'No checkout data found for this stall' error.value = 'No checkout data found for this stall'
} }
// Auto-select shipping zone if there's only one
if (availableShippingZones.value.length === 1) {
selectedShippingZone.value = availableShippingZones.value[0]
}
isLoading.value = false isLoading.value = false
}) })
</script> </script>