Add NostrmarketAPI integration and enhance MerchantStore component

- Introduce NostrmarketAPI service for improved merchant profile management.
- Update MerchantStore component to handle loading and error states during merchant profile checks.
- Implement logic to check for merchant profiles using the new API, enhancing user experience.
- Refactor computed properties and lifecycle methods to accommodate the new API integration.

These changes streamline the process of checking and managing merchant profiles, providing users with real-time feedback and improving overall functionality.
This commit is contained in:
padreug 2025-09-08 13:42:27 +02:00
parent 8cf62076fd
commit b25e502c17
5 changed files with 319 additions and 28 deletions

View file

@ -36,7 +36,10 @@ export const appConfig: AppConfig = {
config: { config: {
defaultCurrency: 'sats', defaultCurrency: 'sats',
paymentTimeout: 300000, // 5 minutes paymentTimeout: 300000, // 5 minutes
maxOrderHistory: 50 maxOrderHistory: 50,
apiConfig: {
baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000'
}
} }
}, },
chat: { chat: {

View file

@ -140,6 +140,7 @@ export const SERVICE_TOKENS = {
// Nostrmarket services // Nostrmarket services
NOSTRMARKET_SERVICE: Symbol('nostrmarketService'), NOSTRMARKET_SERVICE: Symbol('nostrmarketService'),
NOSTRMARKET_API: Symbol('nostrmarketAPI'),
// API services // API services
LNBITS_API: Symbol('lnbitsAPI'), LNBITS_API: Symbol('lnbitsAPI'),

View file

@ -1,7 +1,28 @@
<template> <template>
<div class="space-y-6"> <div class="space-y-6">
<!-- Loading State -->
<div v-if="isLoadingMerchant" class="flex flex-col items-center justify-center py-12">
<div class="w-16 h-16 mx-auto mb-4 bg-muted/50 rounded-full flex items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
<h3 class="text-lg font-medium text-foreground mb-2">Checking Merchant Status</h3>
<p class="text-muted-foreground">Loading your merchant profile...</p>
</div>
<!-- Error State -->
<div v-else-if="merchantCheckError" class="flex flex-col items-center justify-center py-12">
<div class="w-16 h-16 mx-auto mb-4 bg-red-500/10 rounded-full flex items-center justify-center">
<AlertCircle class="w-8 h-8 text-red-500" />
</div>
<h3 class="text-lg font-medium text-foreground mb-2">Error Loading Merchant Status</h3>
<p class="text-muted-foreground mb-4">{{ merchantCheckError }}</p>
<Button @click="checkMerchantProfile" variant="outline">
Try Again
</Button>
</div>
<!-- No Merchant Profile Empty State --> <!-- No Merchant Profile Empty State -->
<div v-if="!userHasMerchantProfile" class="flex flex-col items-center justify-center py-12"> <div v-else-if="!userHasMerchantProfile" class="flex flex-col items-center justify-center py-12">
<div class="w-24 h-24 bg-muted rounded-full flex items-center justify-center mb-6"> <div class="w-24 h-24 bg-muted rounded-full flex items-center justify-center mb-6">
<User class="w-12 h-12 text-muted-foreground" /> <User class="w-12 h-12 text-muted-foreground" />
</div> </div>
@ -332,7 +353,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useMarketStore } from '@/modules/market/stores/market' import { useMarketStore } from '@/modules/market/stores/market'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@ -355,42 +376,25 @@ import {
} from 'lucide-vue-next' } from 'lucide-vue-next'
import type { OrderStatus } from '@/modules/market/stores/market' import type { OrderStatus } from '@/modules/market/stores/market'
import type { NostrmarketService } from '../services/nostrmarketService' import type { NostrmarketService } from '../services/nostrmarketService'
import type { NostrmarketAPI, Merchant } from '../services/nostrmarketAPI'
import { auth } from '@/composables/useAuthService' import { auth } from '@/composables/useAuthService'
import { injectService, SERVICE_TOKENS } from '@/core/di-container' import { injectService, SERVICE_TOKENS } from '@/core/di-container'
const router = useRouter() const router = useRouter()
const marketStore = useMarketStore() const marketStore = useMarketStore()
const nostrmarketService = injectService(SERVICE_TOKENS.NOSTRMARKET_SERVICE) as NostrmarketService const nostrmarketService = injectService(SERVICE_TOKENS.NOSTRMARKET_SERVICE) as NostrmarketService
const nostrmarketAPI = injectService(SERVICE_TOKENS.NOSTRMARKET_API) as NostrmarketAPI
// Local state // Local state
const isGeneratingInvoice = ref<string | null>(null) const isGeneratingInvoice = ref<string | null>(null)
const merchantProfile = ref<Merchant | null>(null)
const isLoadingMerchant = ref(false)
const merchantCheckError = ref<string | null>(null)
// Computed properties // Computed properties
const userHasMerchantProfile = computed(() => { const userHasMerchantProfile = computed(() => {
const currentUserPubkey = auth.currentUser?.value?.pubkey // Use the actual API response to determine if user has merchant profile
if (!currentUserPubkey) return false return merchantProfile.value !== null
// Check multiple indicators that suggest user has a merchant profile:
// 1. User has wallets with admin keys (required for merchant operations)
const userWallets = auth.currentUser?.value?.wallets || []
const hasAdminWallet = userWallets.some(wallet => wallet.adminkey)
// 2. User has stalls (indicates they've set up merchant infrastructure)
const userStalls = marketStore.stalls.filter(stall =>
stall.pubkey === currentUserPubkey
)
const hasStalls = userStalls.length > 0
// 3. User has been a seller in orders (indicates merchant activity)
const hasSellerOrders = Object.values(marketStore.orders).some(order =>
order.sellerPubkey === currentUserPubkey
)
// User is considered to have a merchant profile if they have at least:
// - An admin wallet AND (stalls OR seller orders)
// OR just have stalls (indicates successful merchant setup)
return hasStalls || (hasAdminWallet && hasSellerOrders)
}) })
const userHasStalls = computed(() => { const userHasStalls = computed(() => {
@ -647,9 +651,58 @@ const getFirstWalletName = () => {
return 'N/A' return 'N/A'
} }
// Methods
const checkMerchantProfile = async () => {
const currentUser = auth.currentUser?.value
if (!currentUser) return
const userWallets = currentUser.wallets || []
if (userWallets.length === 0) {
console.warn('No wallets available for merchant check')
return
}
const wallet = userWallets[0] // Use first wallet
if (!wallet.inkey) {
console.warn('Wallet missing invoice key for merchant check')
return
}
isLoadingMerchant.value = true
merchantCheckError.value = null
try {
console.log('Checking for merchant profile...')
const merchant = await nostrmarketAPI.getMerchant(wallet.inkey)
merchantProfile.value = merchant
console.log('Merchant profile check result:', {
hasMerchant: !!merchant,
merchantId: merchant?.id,
active: merchant?.config?.active
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to check merchant profile'
console.error('Error checking merchant profile:', error)
merchantCheckError.value = errorMessage
merchantProfile.value = null
} finally {
isLoadingMerchant.value = false
}
}
// Lifecycle // Lifecycle
onMounted(() => { onMounted(async () => {
console.log('Merchant Store component loaded') console.log('Merchant Store component loaded')
await checkMerchantProfile()
})
// Watch for auth changes and re-check merchant profile
watch(() => auth.currentUser?.value?.pubkey, async (newPubkey, oldPubkey) => {
if (newPubkey !== oldPubkey) {
console.log('User changed, re-checking merchant profile')
await checkMerchantProfile()
}
}) })
</script> </script>

View file

@ -20,6 +20,7 @@ import { useMarketPreloader } from './composables/useMarketPreloader'
// Import services // Import services
import { NostrmarketService } from './services/nostrmarketService' import { NostrmarketService } from './services/nostrmarketService'
import { PaymentMonitorService } from './services/paymentMonitor' import { PaymentMonitorService } from './services/paymentMonitor'
import { NostrmarketAPI } from './services/nostrmarketAPI'
export interface MarketModuleConfig { export interface MarketModuleConfig {
defaultCurrency: string defaultCurrency: string
@ -49,6 +50,9 @@ export const marketModule: ModulePlugin = {
const nostrmarketService = new NostrmarketService() const nostrmarketService = new NostrmarketService()
container.provide(SERVICE_TOKENS.NOSTRMARKET_SERVICE, nostrmarketService) container.provide(SERVICE_TOKENS.NOSTRMARKET_SERVICE, nostrmarketService)
const nostrmarketAPI = new NostrmarketAPI()
container.provide(SERVICE_TOKENS.NOSTRMARKET_API, nostrmarketAPI)
const paymentMonitorService = new PaymentMonitorService() const paymentMonitorService = new PaymentMonitorService()
container.provide(SERVICE_TOKENS.PAYMENT_MONITOR, paymentMonitorService) container.provide(SERVICE_TOKENS.PAYMENT_MONITOR, paymentMonitorService)
@ -61,6 +65,14 @@ export const marketModule: ModulePlugin = {
// Service will auto-initialize when dependencies are available // Service will auto-initialize when dependencies are available
}) })
await nostrmarketAPI.initialize({
waitForDependencies: true,
maxRetries: 3
}).catch(error => {
console.warn('🛒 NostrmarketAPI initialization deferred:', error)
// Service will auto-initialize when dependencies are available
})
await paymentMonitorService.initialize({ await paymentMonitorService.initialize({
waitForDependencies: true, waitForDependencies: true,
maxRetries: 3 maxRetries: 3
@ -87,6 +99,7 @@ export const marketModule: ModulePlugin = {
// Clean up services // Clean up services
container.remove(SERVICE_TOKENS.NOSTRMARKET_SERVICE) container.remove(SERVICE_TOKENS.NOSTRMARKET_SERVICE)
container.remove(SERVICE_TOKENS.NOSTRMARKET_API)
container.remove(SERVICE_TOKENS.PAYMENT_MONITOR) container.remove(SERVICE_TOKENS.PAYMENT_MONITOR)
console.log('✅ Market module uninstalled') console.log('✅ Market module uninstalled')

View file

@ -0,0 +1,221 @@
import { BaseService } from '@/core/base/BaseService'
import appConfig from '@/app.config'
export interface Merchant {
id: string
private_key: string
public_key: string
time?: number
config: {
name?: string
about?: string
picture?: string
event_id?: string
sync_from_nostr?: boolean
active: boolean
restore_in_progress?: boolean
}
}
export interface Stall {
id: string
wallet: string
name: string
currency: string
shipping_zones: Array<{
id: string
name: string
cost: number
countries: string[]
}>
config: {
image_url?: string
description?: string
}
pending: boolean
event_id?: string
event_created_at?: number
}
export interface CreateMerchantRequest {
config: {
name?: string
about?: string
picture?: string
sync_from_nostr?: boolean
active?: boolean
}
}
export interface CreateStallRequest {
wallet: string
name: string
currency: string
shipping_zones: Array<{
id: string
name: string
cost: number
countries: string[]
}>
config: {
image_url?: string
description?: string
}
}
export class NostrmarketAPI extends BaseService {
// Service metadata
protected readonly metadata = {
name: 'NostrmarketAPI',
version: '1.0.0',
dependencies: [] // No dependencies - this is a market-specific service
}
private baseUrl: string
constructor() {
super()
const config = appConfig.modules.market.config
if (!config?.apiConfig?.baseUrl) {
throw new Error('NostrmarketAPI: Missing apiConfig.baseUrl in market module config')
}
this.baseUrl = config.apiConfig.baseUrl
}
/**
* Service-specific initialization (called by BaseService)
*/
protected async onInitialize(): Promise<void> {
this.debug('NostrmarketAPI initialized with base URL:', this.baseUrl)
// Service is ready to use
}
private async request<T>(
endpoint: string,
walletKey: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}/nostrmarket${endpoint}`
this.debug('NostrmarketAPI request:', { endpoint, fullUrl: url })
const headers: HeadersInit = {
'Content-Type': 'application/json',
'X-API-KEY': walletKey,
...options.headers,
}
const response = await fetch(url, {
...options,
headers,
})
if (!response.ok) {
const errorText = await response.text()
this.debug('NostrmarketAPI Error:', {
status: response.status,
statusText: response.statusText,
errorText
})
// If 404, it means no merchant profile exists
if (response.status === 404) {
return null as T
}
throw new Error(`NostrmarketAPI request failed: ${response.status} ${response.statusText}`)
}
const data = await response.json()
return data
}
/**
* Get merchant profile for the current user
* Uses wallet invoice key (inkey) as per the API specification
*/
async getMerchant(walletInkey: string): Promise<Merchant | null> {
try {
const merchant = await this.request<Merchant>(
'/api/v1/merchant',
walletInkey,
{ method: 'GET' }
)
this.debug('Retrieved merchant:', {
exists: !!merchant,
merchantId: merchant?.id,
active: merchant?.config?.active
})
return merchant
} catch (error) {
this.debug('Failed to get merchant:', error)
// Return null instead of throwing - no merchant profile exists
return null
}
}
/**
* Create a new merchant profile
* Uses wallet admin key as per the API specification
*/
async createMerchant(
walletAdminkey: string,
merchantData: CreateMerchantRequest
): Promise<Merchant> {
const merchant = await this.request<Merchant>(
'/api/v1/merchant',
walletAdminkey,
{
method: 'POST',
body: JSON.stringify(merchantData),
}
)
this.debug('Created merchant:', { merchantId: merchant.id })
return merchant
}
/**
* Get stalls for the current merchant
*/
async getStalls(walletInkey: string): Promise<Stall[]> {
try {
const stalls = await this.request<Stall[]>(
'/api/v1/stalls',
walletInkey,
{ method: 'GET' }
)
this.debug('Retrieved stalls:', { count: stalls?.length || 0 })
return stalls || []
} catch (error) {
this.debug('Failed to get stalls:', error)
return []
}
}
/**
* Create a new stall
*/
async createStall(
walletAdminkey: string,
stallData: CreateStallRequest
): Promise<Stall> {
const stall = await this.request<Stall>(
'/api/v1/stall',
walletAdminkey,
{
method: 'POST',
body: JSON.stringify(stallData),
}
)
this.debug('Created stall:', { stallId: stall.id })
return stall
}
}