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: {
|
||||
defaultCurrency: 'sats',
|
||||
paymentTimeout: 300000, // 5 minutes
|
||||
maxOrderHistory: 50
|
||||
maxOrderHistory: 50,
|
||||
apiConfig: {
|
||||
baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000'
|
||||
}
|
||||
}
|
||||
},
|
||||
chat: {
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ export const SERVICE_TOKENS = {
|
|||
|
||||
// Nostrmarket services
|
||||
NOSTRMARKET_SERVICE: Symbol('nostrmarketService'),
|
||||
NOSTRMARKET_API: Symbol('nostrmarketAPI'),
|
||||
|
||||
// API services
|
||||
LNBITS_API: Symbol('lnbitsAPI'),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,28 @@
|
|||
<template>
|
||||
<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 -->
|
||||
<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">
|
||||
<User class="w-12 h-12 text-muted-foreground" />
|
||||
</div>
|
||||
|
|
@ -332,7 +353,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMarketStore } from '@/modules/market/stores/market'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
|
@ -355,42 +376,25 @@ import {
|
|||
} from 'lucide-vue-next'
|
||||
import type { OrderStatus } from '@/modules/market/stores/market'
|
||||
import type { NostrmarketService } from '../services/nostrmarketService'
|
||||
import type { NostrmarketAPI, Merchant } from '../services/nostrmarketAPI'
|
||||
import { auth } from '@/composables/useAuthService'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
|
||||
const router = useRouter()
|
||||
const marketStore = useMarketStore()
|
||||
const nostrmarketService = injectService(SERVICE_TOKENS.NOSTRMARKET_SERVICE) as NostrmarketService
|
||||
const nostrmarketAPI = injectService(SERVICE_TOKENS.NOSTRMARKET_API) as NostrmarketAPI
|
||||
|
||||
// Local state
|
||||
const isGeneratingInvoice = ref<string | null>(null)
|
||||
const merchantProfile = ref<Merchant | null>(null)
|
||||
const isLoadingMerchant = ref(false)
|
||||
const merchantCheckError = ref<string | null>(null)
|
||||
|
||||
// Computed properties
|
||||
const userHasMerchantProfile = computed(() => {
|
||||
const currentUserPubkey = auth.currentUser?.value?.pubkey
|
||||
if (!currentUserPubkey) return false
|
||||
|
||||
// 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)
|
||||
// Use the actual API response to determine if user has merchant profile
|
||||
return merchantProfile.value !== null
|
||||
})
|
||||
|
||||
const userHasStalls = computed(() => {
|
||||
|
|
@ -647,9 +651,58 @@ const getFirstWalletName = () => {
|
|||
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
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
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>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { useMarketPreloader } from './composables/useMarketPreloader'
|
|||
// Import services
|
||||
import { NostrmarketService } from './services/nostrmarketService'
|
||||
import { PaymentMonitorService } from './services/paymentMonitor'
|
||||
import { NostrmarketAPI } from './services/nostrmarketAPI'
|
||||
|
||||
export interface MarketModuleConfig {
|
||||
defaultCurrency: string
|
||||
|
|
@ -49,6 +50,9 @@ export const marketModule: ModulePlugin = {
|
|||
const nostrmarketService = new NostrmarketService()
|
||||
container.provide(SERVICE_TOKENS.NOSTRMARKET_SERVICE, nostrmarketService)
|
||||
|
||||
const nostrmarketAPI = new NostrmarketAPI()
|
||||
container.provide(SERVICE_TOKENS.NOSTRMARKET_API, nostrmarketAPI)
|
||||
|
||||
const paymentMonitorService = new PaymentMonitorService()
|
||||
container.provide(SERVICE_TOKENS.PAYMENT_MONITOR, paymentMonitorService)
|
||||
|
||||
|
|
@ -61,6 +65,14 @@ export const marketModule: ModulePlugin = {
|
|||
// 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({
|
||||
waitForDependencies: true,
|
||||
maxRetries: 3
|
||||
|
|
@ -87,6 +99,7 @@ export const marketModule: ModulePlugin = {
|
|||
|
||||
// Clean up services
|
||||
container.remove(SERVICE_TOKENS.NOSTRMARKET_SERVICE)
|
||||
container.remove(SERVICE_TOKENS.NOSTRMARKET_API)
|
||||
container.remove(SERVICE_TOKENS.PAYMENT_MONITOR)
|
||||
|
||||
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