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:
parent
8cf62076fd
commit
b25e502c17
5 changed files with 319 additions and 28 deletions
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
221
src/modules/market/services/nostrmarketAPI.ts
Normal file
221
src/modules/market/services/nostrmarketAPI.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue