Refactor authentication and async operation handling in useAuth composable

- Introduce useMultiAsyncOperation to manage multiple async operations in useAuth, enhancing error handling and loading state management.
- Replace manual loading and error state management with standardized async operation patterns for initialize, login, register, and logout functions.
- Update related components to utilize the new async operation structure, improving code clarity and maintainability.
- Add useAsyncOperation to other composables (useChat, useTicketPurchase, useMarket) for consistent async handling across the application.
This commit is contained in:
padreug 2025-09-05 06:08:08 +02:00
parent e0443742c5
commit 7c439361b7
5 changed files with 298 additions and 133 deletions

View file

@ -1,31 +1,45 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { lnbitsAPI, type User, type LoginCredentials, type RegisterData } from '@/lib/api/lnbits' import { lnbitsAPI, type User, type LoginCredentials, type RegisterData } from '@/lib/api/lnbits'
import { useMultiAsyncOperation } from '@/core/composables/useAsyncOperation'
const currentUser = ref<User | null>(null) const currentUser = ref<User | null>(null)
const isLoading = ref(false)
const error = ref<string | null>(null) // Shared async operations for auth actions
const authOperations = useMultiAsyncOperation<{
initialize: User | null
login: User
register: User
logout: void
}>()
export function useAuth() { export function useAuth() {
const isAuthenticated = computed(() => !!currentUser.value) const isAuthenticated = computed(() => !!currentUser.value)
// Get operation instances
const initializeOp = authOperations.createOperation('initialize')
const loginOp = authOperations.createOperation('login')
const registerOp = authOperations.createOperation('register')
const logoutOp = authOperations.createOperation('logout')
/** /**
* Initialize authentication on app start * Initialize authentication on app start
*/ */
async function initialize(): Promise<void> { async function initialize(): Promise<void> {
try { try {
isLoading.value = true await initializeOp.execute(async () => {
error.value = null if (lnbitsAPI.isAuthenticated()) {
const user = await lnbitsAPI.getCurrentUser()
if (lnbitsAPI.isAuthenticated()) { currentUser.value = user
const user = await lnbitsAPI.getCurrentUser() return user
currentUser.value = user }
} return null
} catch (err) { }, {
error.value = err instanceof Error ? err.message : 'Failed to initialize authentication' errorMessage: 'Failed to initialize authentication',
// Clear invalid token showToast: false // Don't show toast for initialization errors
})
} catch {
// Clear invalid token on error
await logout() await logout()
} finally {
isLoading.value = false
} }
} }
@ -33,88 +47,77 @@ export function useAuth() {
* Login with username and password * Login with username and password
*/ */
async function login(credentials: LoginCredentials): Promise<void> { async function login(credentials: LoginCredentials): Promise<void> {
try { await loginOp.execute(async () => {
isLoading.value = true
error.value = null
await lnbitsAPI.login(credentials) await lnbitsAPI.login(credentials)
// Get user details // Get user details
const user = await lnbitsAPI.getCurrentUser() const user = await lnbitsAPI.getCurrentUser()
currentUser.value = user currentUser.value = user
} catch (err) { return user
error.value = err instanceof Error ? err.message : 'Login failed' }, {
throw err errorMessage: 'Login failed'
} finally { })
isLoading.value = false
}
} }
/** /**
* Register new user * Register new user
*/ */
async function register(data: RegisterData): Promise<void> { async function register(data: RegisterData): Promise<void> {
try { await registerOp.execute(async () => {
isLoading.value = true
error.value = null
await lnbitsAPI.register(data) await lnbitsAPI.register(data)
// Get user details // Get user details
const user = await lnbitsAPI.getCurrentUser() const user = await lnbitsAPI.getCurrentUser()
currentUser.value = user currentUser.value = user
} catch (err) { return user
error.value = err instanceof Error ? err.message : 'Registration failed' }, {
throw err errorMessage: 'Registration failed'
} finally { })
isLoading.value = false
}
} }
/** /**
* Logout and clear user data * Logout and clear user data
*/ */
async function logout(): Promise<void> { async function logout(): Promise<void> {
// Clear local state await logoutOp.execute(async () => {
lnbitsAPI.logout() // Clear local state
currentUser.value = null lnbitsAPI.logout()
error.value = null currentUser.value = null
// Clear all auth operation states
authOperations.clearAll()
}, {
showToast: false // Don't show toast for logout
})
} }
/** /**
* Update user password * Update user password
*/ */
async function updatePassword(currentPassword: string, newPassword: string): Promise<void> { async function updatePassword(currentPassword: string, newPassword: string): Promise<void> {
try { const updatePasswordOp = authOperations.createOperation('updatePassword' as any)
isLoading.value = true
error.value = null return await updatePasswordOp.execute(async () => {
const updatedUser = await lnbitsAPI.updatePassword(currentPassword, newPassword) const updatedUser = await lnbitsAPI.updatePassword(currentPassword, newPassword)
currentUser.value = updatedUser currentUser.value = updatedUser
} catch (err) { return updatedUser
error.value = err instanceof Error ? err.message : 'Failed to update password' }, {
throw err errorMessage: 'Failed to update password'
} finally { })
isLoading.value = false
}
} }
/** /**
* Update user profile * Update user profile
*/ */
async function updateProfile(data: Partial<User>): Promise<void> { async function updateProfile(data: Partial<User>): Promise<void> {
try { const updateProfileOp = authOperations.createOperation('updateProfile' as any)
isLoading.value = true
error.value = null return await updateProfileOp.execute(async () => {
const updatedUser = await lnbitsAPI.updateProfile(data) const updatedUser = await lnbitsAPI.updateProfile(data)
currentUser.value = updatedUser currentUser.value = updatedUser
} catch (err) { return updatedUser
error.value = err instanceof Error ? err.message : 'Failed to update profile' }, {
throw err errorMessage: 'Failed to update profile'
} finally { })
isLoading.value = false
}
} }
/** /**
@ -145,8 +148,9 @@ export function useAuth() {
// State // State
currentUser: computed(() => currentUser.value), currentUser: computed(() => currentUser.value),
isAuthenticated, isAuthenticated,
isLoading, isLoading: computed(() => authOperations.isAnyLoading()),
error, error: computed(() => authOperations.hasAnyError() ?
(initializeOp.error.value || loginOp.error.value || registerOp.error.value || logoutOp.error.value) : null),
userDisplay, userDisplay,
// Actions // Actions

View file

@ -0,0 +1,164 @@
import { ref, type Ref } from 'vue'
import { toast } from 'vue-sonner'
export interface AsyncOperationOptions {
successMessage?: string
errorMessage?: string
showToast?: boolean
showSuccessToast?: boolean
showErrorToast?: boolean
}
export interface AsyncOperationState<T> {
isLoading: Ref<boolean>
error: Ref<string | null>
data: Ref<T | null>
}
export interface AsyncOperationReturn<T> extends AsyncOperationState<T> {
execute: (operation: () => Promise<T>, options?: AsyncOperationOptions) => Promise<T | null>
reset: () => void
clear: () => void
}
/**
* Composable for standardized async operation handling
* Eliminates duplicate loading/error/success patterns across modules
*
* @example
* ```typescript
* const { isLoading, error, data, execute } = useAsyncOperation<OrderData>()
*
* const handleOrder = async () => {
* await execute(async () => {
* return await createOrder(orderData)
* }, {
* successMessage: 'Order created successfully!',
* errorMessage: 'Failed to create order'
* })
* }
* ```
*/
export function useAsyncOperation<T = any>(): AsyncOperationReturn<T> {
const isLoading = ref(false)
const error = ref<string | null>(null)
const data = ref(null) as Ref<T | null>
/**
* Execute an async operation with standardized error handling and loading states
*/
const execute = async (
operation: () => Promise<T>,
options: AsyncOperationOptions = {}
): Promise<T | null> => {
const {
successMessage,
errorMessage = 'Operation failed',
showToast = true,
showSuccessToast = showToast,
showErrorToast = showToast
} = options
try {
isLoading.value = true
error.value = null
const result = await operation()
data.value = result
// Show success toast if configured
if (showSuccessToast && successMessage) {
toast.success(successMessage)
}
return result
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err)
error.value = errorMsg
// Show error toast if configured
if (showErrorToast) {
toast.error(errorMessage, {
description: errorMsg !== errorMessage ? errorMsg : undefined
})
}
// Re-throw to allow caller to handle if needed
throw err
} finally {
isLoading.value = false
}
}
/**
* Reset the operation state (clear error, keep data)
*/
const reset = (): void => {
isLoading.value = false
error.value = null
}
/**
* Clear all state (error, data, loading)
*/
const clear = (): void => {
isLoading.value = false
error.value = null
data.value = null
}
return {
// State
isLoading,
error,
data,
// Methods
execute,
reset,
clear
}
}
/**
* Specialized version for operations that don't return data
*/
export function useAsyncAction(): Omit<AsyncOperationReturn<void>, 'data'> {
const { data, ...rest } = useAsyncOperation<void>()
return rest
}
/**
* Multiple async operations manager
* Useful when you need to track multiple independent operations
*/
export function useMultiAsyncOperation<T extends Record<string, any>>() {
const operations = ref<Record<keyof T, AsyncOperationReturn<any>>>({} as any)
const createOperation = <K extends keyof T>(key: K): AsyncOperationReturn<T[K]> => {
if (!operations.value[key]) {
operations.value[key] = useAsyncOperation<T[K]>()
}
return operations.value[key] as AsyncOperationReturn<T[K]>
}
const isAnyLoading = (): boolean => {
return Object.values(operations.value).some((op: any) => op.isLoading.value)
}
const hasAnyError = (): boolean => {
return Object.values(operations.value).some((op: any) => op.error.value !== null)
}
const clearAll = (): void => {
Object.values(operations.value).forEach((op: any) => op.clear())
}
return {
operations,
createOperation,
isAnyLoading,
hasAnyError,
clearAll
}
}

View file

@ -2,6 +2,7 @@ import { ref, computed } from 'vue'
import { injectService } from '@/core/di-container' import { injectService } from '@/core/di-container'
import type { ChatService } from '../services/chat-service' import type { ChatService } from '../services/chat-service'
import type { ChatPeer } from '../types' import type { ChatPeer } from '../types'
import { useMultiAsyncOperation } from '@/core/composables/useAsyncOperation'
// Service token for chat service // Service token for chat service
export const CHAT_SERVICE_TOKEN = Symbol('chatService') export const CHAT_SERVICE_TOKEN = Symbol('chatService')
@ -14,8 +15,15 @@ export function useChat() {
} }
const selectedPeer = ref<string | null>(null) const selectedPeer = ref<string | null>(null)
const isLoading = ref(false)
const error = ref<string | null>(null) // Async operations
const asyncOps = useMultiAsyncOperation<{
sendMessage: void
refreshPeers: void
}>()
const sendMessageOp = asyncOps.createOperation('sendMessage')
const refreshPeersOp = asyncOps.createOperation('refreshPeers')
// Computed properties // Computed properties
const peers = computed(() => chatService.allPeers.value) const peers = computed(() => chatService.allPeers.value)
@ -41,17 +49,11 @@ export function useChat() {
return return
} }
isLoading.value = true return await sendMessageOp.execute(async () => {
error.value = null await chatService.sendMessage(selectedPeer.value!, content.trim())
}, {
try { errorMessage: 'Failed to send message'
await chatService.sendMessage(selectedPeer.value, content.trim()) })
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to send message'
console.error('Send message error:', err)
} finally {
isLoading.value = false
}
} }
const addPeer = (pubkey: string, name?: string): ChatPeer => { const addPeer = (pubkey: string, name?: string): ChatPeer => {
@ -63,23 +65,22 @@ export function useChat() {
} }
const refreshPeers = async () => { const refreshPeers = async () => {
isLoading.value = true return await refreshPeersOp.execute(async () => {
error.value = null
try {
await chatService.refreshPeers() await chatService.refreshPeers()
} catch (err) { }, {
error.value = err instanceof Error ? err.message : 'Failed to refresh peers' errorMessage: 'Failed to refresh peers'
console.error('Failed to refresh peers:', err) })
} finally {
isLoading.value = false
}
} }
return { return {
// State // State
selectedPeer, selectedPeer,
isLoading, isSendingMessage: sendMessageOp.isLoading,
error, sendMessageError: sendMessageOp.error,
isRefreshingPeers: refreshPeersOp.isLoading,
refreshPeersError: refreshPeersOp.error,
isLoading: computed(() => asyncOps.isAnyLoading()),
error: computed(() => sendMessageOp.error.value || refreshPeersOp.error.value),
// Computed // Computed
peers, peers,

View file

@ -2,13 +2,15 @@ import { ref, computed, onUnmounted } from 'vue'
import { purchaseTicket, checkPaymentStatus, payInvoiceWithWallet } from '@/lib/api/events' import { purchaseTicket, checkPaymentStatus, payInvoiceWithWallet } from '@/lib/api/events'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { useAsyncOperation } from '@/core/composables/useAsyncOperation'
export function useTicketPurchase() { export function useTicketPurchase() {
const { isAuthenticated, currentUser } = useAuth() const { isAuthenticated, currentUser } = useAuth()
// Async operations
const purchaseOperation = useAsyncOperation()
// State // State
const isLoading = ref(false)
const error = ref<string | null>(null)
const paymentHash = ref<string | null>(null) const paymentHash = ref<string | null>(null)
const paymentRequest = ref<string | null>(null) const paymentRequest = ref<string | null>(null)
const qrCode = ref<string | null>(null) const qrCode = ref<string | null>(null)
@ -50,7 +52,7 @@ export function useTicketPurchase() {
qrCode.value = dataUrl qrCode.value = dataUrl
} catch (err) { } catch (err) {
console.error('Error generating QR code:', err) console.error('Error generating QR code:', err)
error.value = 'Failed to generate QR code' // Note: error handling is now managed by the purchaseOperation
} }
} }
@ -98,16 +100,15 @@ export function useTicketPurchase() {
throw new Error('User must be authenticated to purchase tickets') throw new Error('User must be authenticated to purchase tickets')
} }
isLoading.value = true return await purchaseOperation.execute(async () => {
error.value = null // Clear previous state
paymentHash.value = null paymentHash.value = null
paymentRequest.value = null paymentRequest.value = null
qrCode.value = null qrCode.value = null
ticketQRCode.value = null ticketQRCode.value = null
purchasedTicketId.value = null purchasedTicketId.value = null
showTicketQR.value = false showTicketQR.value = false
try {
// Get the invoice // Get the invoice
const invoice = await purchaseTicket(eventId) const invoice = await purchaseTicket(eventId)
paymentHash.value = invoice.payment_hash paymentHash.value = invoice.payment_hash
@ -133,12 +134,11 @@ export function useTicketPurchase() {
// No wallet balance, proceed with manual payment // No wallet balance, proceed with manual payment
await startPaymentStatusCheck(eventId, invoice.payment_hash) await startPaymentStatusCheck(eventId, invoice.payment_hash)
} }
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to purchase ticket' return invoice
console.error('Error purchasing ticket:', err) }, {
} finally { errorMessage: 'Failed to purchase ticket'
isLoading.value = false })
}
} }
// Start payment status check // Start payment status check
@ -184,8 +184,7 @@ export function useTicketPurchase() {
// Reset payment state // Reset payment state
function resetPaymentState() { function resetPaymentState() {
isLoading.value = false purchaseOperation.clear()
error.value = null
paymentHash.value = null paymentHash.value = null
paymentRequest.value = null paymentRequest.value = null
qrCode.value = null qrCode.value = null
@ -215,8 +214,8 @@ export function useTicketPurchase() {
return { return {
// State // State
isLoading, isLoading: purchaseOperation.isLoading,
error, error: purchaseOperation.error,
paymentHash, paymentHash,
paymentRequest, paymentRequest,
qrCode, qrCode,

View file

@ -5,6 +5,7 @@ import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import { config } from '@/lib/config' import { config } from '@/lib/config'
import { nostrmarketService } from '../services/nostrmarketService' import { nostrmarketService } from '../services/nostrmarketService'
import { nip04 } from 'nostr-tools' import { nip04 } from 'nostr-tools'
import { useAsyncOperation } from '@/core/composables/useAsyncOperation'
// Nostr event kinds for market functionality // Nostr event kinds for market functionality
const MARKET_EVENT_KINDS = { const MARKET_EVENT_KINDS = {
@ -44,9 +45,11 @@ export function useMarket() {
} }
} }
// Async operations
const marketOperation = useAsyncOperation()
const connectionOperation = useAsyncOperation()
// State // State
const isLoading = ref(false)
const error = ref<Error | null>(null)
const isConnected = ref(false) const isConnected = ref(false)
const activeMarket = computed(() => marketStore.activeMarket) const activeMarket = computed(() => marketStore.activeMarket)
const markets = computed(() => marketStore.markets) const markets = computed(() => marketStore.markets)
@ -56,20 +59,15 @@ export function useMarket() {
// Connection state // Connection state
const connectionStatus = computed(() => { const connectionStatus = computed(() => {
if (connectionOperation.isLoading.value) return 'connecting'
if (isConnected.value) return 'connected' if (isConnected.value) return 'connected'
if (nostrStore.isConnecting) return 'connecting' if (connectionOperation.error.value || nostrStore.error) return 'error'
if (nostrStore.error) return 'error'
return 'disconnected' return 'disconnected'
}) })
// Load market from naddr // Load market from naddr
const loadMarket = async (naddr: string) => { const loadMarket = async (naddr: string) => {
try { return await marketOperation.execute(async () => {
isLoading.value = true
error.value = null
// Load market from naddr
// Parse naddr to get market data // Parse naddr to get market data
// TODO: Confirm if this should use nostrStore.account?.pubkey or authService.user.value?.pubkey // TODO: Confirm if this should use nostrStore.account?.pubkey or authService.user.value?.pubkey
const marketData = { const marketData = {
@ -82,13 +80,10 @@ export function useMarket() {
} }
await loadMarketData(marketData) await loadMarketData(marketData)
return marketData
} catch (err) { }, {
error.value = err instanceof Error ? err : new Error('Failed to load market') errorMessage: 'Failed to load market'
throw err })
} finally {
isLoading.value = false
}
} }
// Load market data from Nostr events // Load market data from Nostr events
@ -560,7 +555,7 @@ export function useMarket() {
// Connect to market // Connect to market
const connectToMarket = async () => { const connectToMarket = async () => {
try { return await connectionOperation.execute(async () => {
console.log('🛒 Checking RelayHub connection...') console.log('🛒 Checking RelayHub connection...')
// Use existing relay hub connection (should already be connected by base module) // Use existing relay hub connection (should already be connected by base module)
isConnected.value = relayHub.isConnected.value isConnected.value = relayHub.isConnected.value
@ -602,18 +597,18 @@ export function useMarket() {
// Note: Order-related DMs are now handled by chat service forwarding // Note: Order-related DMs are now handled by chat service forwarding
// No need for separate subscription // No need for separate subscription
} catch (err) { return { isConnected: isConnected.value }
console.error('🛒 Failed to connect to market:', err) }, {
error.value = err instanceof Error ? err : new Error('Failed to connect to market') errorMessage: 'Failed to connect to market'
throw err })
}
} }
// Disconnect from market // Disconnect from market
const disconnectFromMarket = () => { const disconnectFromMarket = () => {
isConnected.value = false isConnected.value = false
error.value = null marketOperation.clear()
connectionOperation.clear()
// Market disconnected // Market disconnected
} }
@ -631,8 +626,10 @@ export function useMarket() {
return { return {
// State // State
isLoading: readonly(isLoading), isLoading: readonly(marketOperation.isLoading),
error: readonly(error), error: readonly(marketOperation.error),
isConnecting: readonly(connectionOperation.isLoading),
connectionError: readonly(connectionOperation.error),
isConnected: readonly(isConnected), isConnected: readonly(isConnected),
connectionStatus: readonly(connectionStatus), connectionStatus: readonly(connectionStatus),
activeMarket: readonly(activeMarket), activeMarket: readonly(activeMarket),