Refines project documentation to reflect recent architectural changes and coding standards. Adds detailed explanations of the BaseService pattern, module structure, and JavaScript best practices to enhance developer understanding and consistency. Clarifies CSS styling guidelines, emphasizing semantic classes for theme-aware styling. Includes critical bug prevention techniques related to JavaScript falsy values and correct usage of the nullish coalescing operator. Updates build configuration details, environment variable requirements, and mobile browser workaround strategies.
858 lines
32 KiB
Markdown
858 lines
32 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Development Commands
|
|
|
|
**Development**
|
|
- `npm run dev` - Start development server with Vite (includes --host flag)
|
|
- `npm run build` - Build for production (includes TypeScript check with vue-tsc -b)
|
|
- `npm run preview` - Preview production build locally
|
|
- `npm run analyze` - Build with bundle analysis (opens visualization)
|
|
|
|
**Electron Development**
|
|
- `npm run electron:dev` - Run both Vite dev server and Electron concurrently
|
|
- `npm run electron:build` - Full build and package for Electron
|
|
- `npm run start` - Start Electron using Forge
|
|
- `npm run package` - Package Electron app with Forge
|
|
- `npm run make` - Create distributables with Electron Forge
|
|
|
|
## Architecture Overview
|
|
|
|
This is a modular Vue 3 + TypeScript + Vite application with Electron support, featuring a Nostr protocol client and Lightning Network integration for events/ticketing.
|
|
|
|
### **Modular Architecture**
|
|
|
|
The application uses a plugin-based modular architecture with dependency injection for service management:
|
|
|
|
**Core Modules:**
|
|
- **Base Module** (`src/modules/base/`) - Core infrastructure (Nostr, Auth, PWA, Image Upload)
|
|
- **Wallet Module** (`src/modules/wallet/`) - Lightning wallet management with real-time balance updates
|
|
- **Nostr Feed Module** (`src/modules/nostr-feed/`) - Social feed functionality
|
|
- **Chat Module** (`src/modules/chat/`) - Encrypted Nostr chat
|
|
- **Events Module** (`src/modules/events/`) - Event ticketing with Lightning payments
|
|
- **Market Module** (`src/modules/market/`) - Nostr marketplace functionality
|
|
|
|
**IMPORTANT - Market Event Publishing Strategy:**
|
|
- **LNbits "nostrmarket" extension handles ALL market event publishing** (merchants, stalls, products) to Nostr relays
|
|
- **Web-app does NOT publish** merchant/stall/product events - only processes incoming events from relays
|
|
- **Exception: Checkout/Order events** - Web-app publishes order events directly to Nostr during checkout process
|
|
- This division ensures consistency and prevents duplicate publishing while allowing real-time order placement
|
|
|
|
**Module Configuration:**
|
|
- Modules are configured in `src/app.config.ts`
|
|
- Each module can be enabled/disabled and configured independently
|
|
- Modules have dependencies (e.g., all modules depend on 'base')
|
|
|
|
**Plugin Manager:**
|
|
- `src/core/plugin-manager.ts` handles module lifecycle
|
|
- Registers, installs, and manages module dependencies
|
|
- Handles route registration from modules
|
|
|
|
### **Dependency Injection Pattern**
|
|
|
|
**CRITICAL**: Always use the dependency injection pattern for accessing shared services:
|
|
|
|
**Service Registration (Base Module):**
|
|
```typescript
|
|
// src/modules/base/index.ts
|
|
import { container, SERVICE_TOKENS } from '@/core/di-container'
|
|
|
|
container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
|
|
container.provide(SERVICE_TOKENS.AUTH_SERVICE, auth)
|
|
```
|
|
|
|
**Service Consumption (Other Modules):**
|
|
```typescript
|
|
// In any module's composables or services
|
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
|
|
|
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
|
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE)
|
|
```
|
|
|
|
**❌ NEVER do this:**
|
|
```typescript
|
|
// DON'T import services directly - breaks modular architecture
|
|
import { relayHubComposable } from '@/composables/useRelayHub'
|
|
```
|
|
|
|
**✅ Always do this:**
|
|
```typescript
|
|
// DO use dependency injection for loose coupling
|
|
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
|
```
|
|
|
|
**Available Services:**
|
|
- `SERVICE_TOKENS.RELAY_HUB` - Centralized Nostr relay management
|
|
- `SERVICE_TOKENS.AUTH_SERVICE` - Authentication services
|
|
- `SERVICE_TOKENS.PAYMENT_SERVICE` - Lightning payment and wallet management
|
|
- `SERVICE_TOKENS.VISIBILITY_SERVICE` - App visibility and connection management
|
|
- `SERVICE_TOKENS.WALLET_SERVICE` - Wallet operations (send, receive, transactions)
|
|
- `SERVICE_TOKENS.WALLET_WEBSOCKET_SERVICE` - Real-time wallet balance updates via WebSocket
|
|
- `SERVICE_TOKENS.STORAGE_SERVICE` - Local storage management
|
|
- `SERVICE_TOKENS.TOAST_SERVICE` - Toast notification system
|
|
- `SERVICE_TOKENS.INVOICE_SERVICE` - Lightning invoice creation and management
|
|
- `SERVICE_TOKENS.LNBITS_API` - LNbits API client
|
|
- `SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE` - Image upload to pictrs server
|
|
- `SERVICE_TOKENS.NOSTR_METADATA_SERVICE` - Nostr user metadata (NIP-01 kind 0)
|
|
|
|
**Core Stack:**
|
|
- Vue 3 with Composition API (`<script setup>` style)
|
|
- TypeScript throughout
|
|
- Vite build system with PWA support
|
|
- Electron for desktop app packaging
|
|
- Pinia for state management
|
|
- Vue Router for navigation
|
|
- TailwindCSS v4 with Shadcn/ui components
|
|
- Vue-i18n for internationalization
|
|
|
|
**Key Features:**
|
|
- Nostr protocol client for decentralized social networking
|
|
- Lightning Network integration for event ticketing
|
|
- **Real-time wallet balance updates via WebSocket** - Automatic UI updates when payments are sent/received
|
|
- PWA capabilities with service worker
|
|
- Theme switching and language switching
|
|
- Real-time connection status monitoring
|
|
|
|
**Directory Structure:**
|
|
- `src/components/` - Vue components organized by feature
|
|
- `ui/` - Shadcn/ui component library
|
|
- `nostr/` - Nostr-specific components
|
|
- `events/` - Event/ticketing components
|
|
- `layout/` - App layout components
|
|
- `src/composables/` - Vue composables for reusable logic
|
|
- `src/stores/` - Pinia stores for state management
|
|
- `src/lib/` - Core business logic
|
|
- `nostr/` - Nostr client implementation
|
|
- `api/` - API integrations
|
|
- `types/` - TypeScript type definitions
|
|
- `src/pages/` - Route pages
|
|
- `src/modules/` - Modular feature implementations
|
|
- `src/core/` - Core infrastructure (DI, BaseService, plugin manager)
|
|
- `electron/` - Electron main process code
|
|
|
|
**Lightning Wallet Integration:**
|
|
The app integrates with LNbits for Lightning Network wallet functionality with real-time balance updates:
|
|
|
|
**Core Wallet Services:**
|
|
- `src/core/services/PaymentService.ts` - Centralized payment processing and wallet balance management
|
|
- `src/modules/wallet/services/WalletService.ts` - Wallet operations (send, receive, transactions, pay links)
|
|
- `src/modules/wallet/services/WalletWebSocketService.ts` - **Real-time balance updates via WebSocket**
|
|
|
|
**WebSocket Real-Time Features:**
|
|
- **Automatic balance updates** when payments are sent or received
|
|
- **Live transaction notifications** with toast messages
|
|
- **Connection management** with automatic reconnection and exponential backoff
|
|
- **Battery optimization** via VisibilityService integration (pauses when app not visible)
|
|
- **Unit conversion handling** between sats and millisats for different LNbits WebSocket behaviors
|
|
|
|
**WebSocket Configuration** (in `app.config.ts`):
|
|
```typescript
|
|
websocket: {
|
|
enabled: true, // Enable/disable WebSocket functionality
|
|
reconnectDelay: 2000, // Initial reconnection delay (ms)
|
|
maxReconnectAttempts: 3, // Maximum reconnection attempts
|
|
fallbackToPolling: true, // Enable polling fallback when WebSocket fails
|
|
pollingInterval: 10000 // Polling interval (ms)
|
|
}
|
|
```
|
|
|
|
**How WebSocket Balance Updates Work:**
|
|
- Connects to LNbits WebSocket: `wss://your-lnbits/api/v1/ws/{walletInkey}`
|
|
- Handles both incoming and outgoing payment notifications
|
|
- **Incoming payments**: Uses balance as-is (post-payment balance from LNbits)
|
|
- **Outgoing payments**: Adjusts balance by subtracting payment amount (pre-payment balance from LNbits)
|
|
- Updates stored in `PaymentService.updateWalletBalance()` for consistency
|
|
- Triggers Vue reactivity to update UI components immediately
|
|
|
|
**Nostr Integration:**
|
|
The app connects to Nostr relays using a custom NostrClient class built on nostr-tools. Key files:
|
|
- `src/lib/nostr/client.ts` - Core Nostr client implementation
|
|
- `src/composables/useNostr.ts` - Vue composable for Nostr connection management
|
|
- `src/stores/nostr.ts` - Pinia store for Nostr state
|
|
|
|
## Development Guidelines
|
|
|
|
### **Modular Architecture Patterns**
|
|
|
|
**Module Structure:**
|
|
```
|
|
src/modules/[module-name]/
|
|
├── index.ts # Module plugin definition
|
|
├── components/ # Module-specific components
|
|
├── composables/ # Module composables
|
|
├── services/ # Module services
|
|
├── stores/ # Module-specific stores
|
|
├── types/ # Module type definitions
|
|
└── views/ # Module pages/views
|
|
```
|
|
|
|
**Module Plugin Pattern:**
|
|
```typescript
|
|
export const myModule: ModulePlugin = {
|
|
name: 'my-module',
|
|
version: '1.0.0',
|
|
dependencies: ['base'], // Always depend on base for core services
|
|
|
|
async install(app: App, options?: { config?: MyModuleConfig }) {
|
|
// Module installation logic
|
|
// Register components, initialize services, etc.
|
|
},
|
|
|
|
routes: [/* module routes */],
|
|
components: {/* exported components */},
|
|
composables: {/* exported composables */}
|
|
}
|
|
```
|
|
|
|
**Service Integration:**
|
|
- All modules MUST use dependency injection for shared services
|
|
- NEVER import services directly across module boundaries
|
|
- Base module provides core infrastructure services
|
|
- Modules can register their own services in the DI container
|
|
|
|
**⚠️ CRITICAL - WebSocket Connection Management:**
|
|
- **ALWAYS integrate with VisibilityService for any module that uses WebSocket connections**
|
|
- All services extending `BaseService` have automatic access to `this.visibilityService`
|
|
- Register visibility callbacks during service initialization: `this.visibilityService.registerService(name, onResume, onPause)`
|
|
- Implement proper connection recovery in `onResume()` handler (check health, reconnect if needed, restore subscriptions)
|
|
- Implement battery-conscious pausing in `onPause()` handler (stop heartbeats, queue operations)
|
|
- **Mobile browsers suspend WebSocket connections when app loses visibility** - visibility management is essential for reliable real-time features
|
|
- See `docs/VisibilityService.md` and `docs/VisibilityService-Integration.md` for comprehensive integration guides
|
|
- Future modules will likely ALL depend on WebSocket connections - plan for visibility management from the start
|
|
|
|
### **Centralized Infrastructure**
|
|
|
|
**Nostr Relay Management:**
|
|
- Single RelayHub manages all Nostr connections
|
|
- All modules use the same relay configuration from `VITE_NOSTR_RELAYS`
|
|
- No module should create separate relay connections
|
|
|
|
**Authentication:**
|
|
- Centralized auth service handles all authentication
|
|
- Modules access auth state through dependency injection
|
|
- Router guards use the shared auth service
|
|
|
|
**Configuration:**
|
|
- Environment variables prefixed with `VITE_`
|
|
- Module configs in `src/app.config.ts`
|
|
- Centralized config parsing and validation
|
|
|
|
### **BaseService Pattern**
|
|
|
|
All services MUST extend `BaseService` (`src/core/base/BaseService.ts`) for standardized initialization and dependency management:
|
|
|
|
**Service Implementation Pattern:**
|
|
|
|
```typescript
|
|
import { BaseService } from '@/core/base/BaseService'
|
|
|
|
export class MyService extends BaseService {
|
|
// 1. REQUIRED: Declare metadata with dependencies
|
|
protected readonly metadata = {
|
|
name: 'MyService',
|
|
version: '1.0.0',
|
|
dependencies: ['AuthService', 'RelayHub', 'VisibilityService']
|
|
}
|
|
|
|
// 2. REQUIRED: Implement onInitialize
|
|
protected async onInitialize(): Promise<void> {
|
|
// Dependencies are auto-injected based on metadata.dependencies
|
|
// Available: this.authService, this.relayHub, this.visibilityService, etc.
|
|
|
|
// Register with VisibilityService if using WebSockets
|
|
if (this.visibilityService) {
|
|
this.visibilityService.registerService(
|
|
this.metadata.name,
|
|
this.onResume.bind(this),
|
|
this.onPause.bind(this)
|
|
)
|
|
}
|
|
|
|
// Your initialization logic
|
|
await this.setupConnections()
|
|
}
|
|
|
|
// 3. Implement visibility handlers for WebSocket services
|
|
private async onResume(): Promise<void> {
|
|
// Reconnect and restore state when app becomes visible
|
|
}
|
|
|
|
private async onPause(): Promise<void> {
|
|
// Pause operations when app loses visibility
|
|
}
|
|
|
|
// 4. Optional: Cleanup logic
|
|
protected async onDispose(): Promise<void> {
|
|
// Cleanup connections, subscriptions, etc.
|
|
}
|
|
}
|
|
```
|
|
|
|
**BaseService Features:**
|
|
- **Automatic dependency injection** based on `metadata.dependencies`
|
|
- **Retry logic** with configurable retries and delays
|
|
- **Reactive state** via `isInitialized`, `isInitializing`, `initError`
|
|
- **Event emission** for service lifecycle events
|
|
- **Error handling** with consistent logging
|
|
- **Debug helpers** for development
|
|
|
|
**Service Initialization:**
|
|
```typescript
|
|
// In module's index.ts
|
|
const myService = new MyService()
|
|
container.provide(SERVICE_TOKENS.MY_SERVICE, myService)
|
|
|
|
// Initialize with options
|
|
await myService.initialize({
|
|
waitForDependencies: true, // Wait for dependencies before initializing
|
|
maxRetries: 3, // Retry on failure
|
|
retryDelay: 1000 // Delay between retries (ms)
|
|
})
|
|
```
|
|
|
|
**Available Dependencies:**
|
|
When you list these in `metadata.dependencies`, they'll be auto-injected:
|
|
- `'RelayHub'` → `this.relayHub`
|
|
- `'AuthService'` → `this.authService`
|
|
- `'VisibilityService'` → `this.visibilityService`
|
|
- `'StorageService'` → `this.storageService`
|
|
- `'ToastService'` → `this.toastService`
|
|
- `'LnbitsAPI'` → `this.lnbitsAPI`
|
|
|
|
### **Form Implementation Standards**
|
|
|
|
**CRITICAL: Always use Shadcn/UI Form Components with vee-validate**
|
|
|
|
All forms in the application MUST follow the official Shadcn Vue form implementation pattern:
|
|
|
|
**Required Form Setup:**
|
|
```typescript
|
|
import { useForm } from 'vee-validate'
|
|
import { toTypedSchema } from '@vee-validate/zod'
|
|
import * as z from 'zod'
|
|
import {
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from '@/components/ui/form'
|
|
|
|
// 1. Define Zod schema for validation
|
|
const formSchema = toTypedSchema(z.object({
|
|
name: z.string().min(1, "Name is required").max(100, "Name too long"),
|
|
email: z.string().email("Invalid email address").optional(),
|
|
items: z.array(z.string()).min(1, "Select at least one item"),
|
|
}))
|
|
|
|
// 2. Set up form with vee-validate
|
|
const form = useForm({
|
|
validationSchema: formSchema,
|
|
initialValues: {
|
|
name: '',
|
|
email: '',
|
|
items: []
|
|
}
|
|
})
|
|
|
|
// 3. Destructure form methods
|
|
const { setFieldValue, resetForm, values, meta } = form
|
|
|
|
// 4. Create form validation computed
|
|
const isFormValid = computed(() => meta.value.valid)
|
|
|
|
// 5. Create submit handler with form.handleSubmit
|
|
const onSubmit = form.handleSubmit(async (values) => {
|
|
console.log('Form submitted:', values)
|
|
// Handle form submission logic
|
|
})
|
|
```
|
|
|
|
**Required Form Template Structure:**
|
|
```vue
|
|
<template>
|
|
<!-- form.handleSubmit automatically prevents default submission -->
|
|
<form @submit="onSubmit" class="space-y-6">
|
|
<!-- Text Input Field -->
|
|
<FormField v-slot="{ componentField }" name="name">
|
|
<FormItem>
|
|
<FormLabel>Name *</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="Enter name"
|
|
v-bind="componentField"
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>Enter your full name</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<!-- Multiple Checkbox Selection -->
|
|
<FormField name="items">
|
|
<FormItem>
|
|
<div class="mb-4">
|
|
<FormLabel class="text-base">Items *</FormLabel>
|
|
<FormDescription>Select one or more items</FormDescription>
|
|
</div>
|
|
<div v-for="item in availableItems" :key="item.id">
|
|
<FormField
|
|
v-slot="{ value, handleChange }"
|
|
type="checkbox"
|
|
:value="item.id"
|
|
:unchecked-value="false"
|
|
name="items"
|
|
>
|
|
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
|
|
<FormControl>
|
|
<Checkbox
|
|
:model-value="value.includes(item.id)"
|
|
@update:model-value="handleChange"
|
|
/>
|
|
</FormControl>
|
|
<FormLabel class="font-normal">{{ item.name }}</FormLabel>
|
|
</FormItem>
|
|
</FormField>
|
|
</div>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<!-- Submit Button -->
|
|
<Button
|
|
type="submit"
|
|
:disabled="isLoading || !isFormValid"
|
|
>
|
|
{{ isLoading ? 'Submitting...' : 'Submit' }}
|
|
</Button>
|
|
</form>
|
|
</template>
|
|
```
|
|
|
|
**Key Form Requirements:**
|
|
- ✅ **Form validation**: Use `@submit="onSubmit"` - form.handleSubmit automatically prevents page refresh
|
|
- ✅ **Button state**: Disable submit button with `!isFormValid` until all required fields are valid
|
|
- ✅ **Error display**: Use `<FormMessage />` for automatic error display
|
|
- ✅ **Field binding**: Use `v-bind="componentField"` for proper form field integration
|
|
- ✅ **Checkbox arrays**: Use nested FormField pattern for multiple checkbox selection
|
|
- ✅ **Type safety**: Zod schema provides full TypeScript type safety
|
|
|
|
**⚠️ CRITICAL: Checkbox Component Binding**
|
|
|
|
For Shadcn/ui Checkbox components, you MUST use the correct Vue.js binding pattern:
|
|
|
|
```vue
|
|
<!-- ✅ CORRECT: Use model-value and @update:model-value for custom components -->
|
|
<FormField v-slot="{ value, handleChange }" name="active">
|
|
<FormItem>
|
|
<div class="flex items-center space-x-2">
|
|
<FormControl>
|
|
<Checkbox
|
|
:model-value="value"
|
|
@update:model-value="handleChange"
|
|
:disabled="isCreating"
|
|
/>
|
|
</FormControl>
|
|
<FormLabel>Make product active</FormLabel>
|
|
</div>
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<!-- ❌ WRONG: Don't use :checked for custom components -->
|
|
<Checkbox
|
|
:checked="value"
|
|
@update:checked="handleChange"
|
|
/>
|
|
```
|
|
|
|
**Key Points:**
|
|
- ✅ **Custom Components**: Use `:model-value` and `@update:model-value` for Shadcn/ui components
|
|
- ✅ **Native HTML**: Use `:checked` and `@change` only for native `<input type="checkbox">` elements
|
|
- ✅ **Force Re-render**: Use dynamic `:key` if checkbox doesn't reflect initial form values
|
|
- ❌ **Don't Mix**: Never mix checked/model-value patterns - they have different behaviors
|
|
|
|
### **CSS and Styling Guidelines**
|
|
|
|
**CRITICAL: Always use semantic, theme-aware CSS classes**
|
|
|
|
- ✅ **Use semantic classes** that automatically adapt to light/dark themes
|
|
- ❌ **Never use hard-coded colors** like `bg-white`, `text-gray-500`, `border-blue-500`
|
|
|
|
**Preferred Semantic Classes:**
|
|
```css
|
|
/* Background Colors */
|
|
bg-background /* Instead of bg-white */
|
|
bg-card /* Instead of bg-gray-50 */
|
|
bg-muted /* Instead of bg-gray-100 */
|
|
|
|
/* Text Colors */
|
|
text-foreground /* Instead of text-gray-900 */
|
|
text-muted-foreground /* Instead of text-gray-600 */
|
|
text-primary /* For primary theme color */
|
|
text-accent /* For accent theme color */
|
|
|
|
/* Borders */
|
|
border-border /* Instead of border-gray-200 */
|
|
border-input /* Instead of border-gray-300 */
|
|
|
|
/* Focus States */
|
|
focus:ring-ring /* Instead of focus:ring-blue-500 */
|
|
focus:border-ring /* Instead of focus:border-blue-500 */
|
|
|
|
/* Opacity Modifiers */
|
|
bg-primary/10 /* For subtle variations */
|
|
text-muted-foreground/70 /* For transparency */
|
|
```
|
|
|
|
**Why Semantic Classes:**
|
|
- Ensures components work in both light and dark themes
|
|
- Maintains consistency with Shadcn/ui component library
|
|
- Easier to maintain and update theme colors globally
|
|
- Better accessibility
|
|
|
|
### **Vue Reactivity Best Practices**
|
|
|
|
**CRITICAL: Handling Complex Object Reactivity**
|
|
|
|
When working with complex objects from API responses or services, Vue's reactivity system may not always detect changes properly. This is especially common with nested objects or objects from external sources.
|
|
|
|
**Common Reactivity Issues:**
|
|
- Input components not updating when object properties change
|
|
- Template not re-rendering after API responses
|
|
- Computed properties not recalculating
|
|
|
|
**✅ SOLUTIONS:**
|
|
|
|
**1. Force Object Reactivity with Object.assign:**
|
|
```typescript
|
|
// ❌ DON'T: Direct assignment may not trigger reactivity
|
|
createdObject.value = apiResponse
|
|
|
|
// ✅ DO: Create new object reference to ensure reactivity
|
|
createdObject.value = Object.assign({}, apiResponse)
|
|
```
|
|
|
|
**2. Force Component Re-render with Dynamic Keys:**
|
|
```vue
|
|
<!-- ❌ DON'T: Component may not update with new data -->
|
|
<Input :value="object.property" readonly />
|
|
|
|
<!-- ✅ DO: Force re-render with dynamic key -->
|
|
<Input
|
|
:key="`field-${object?.id}`"
|
|
:model-value="object?.property || ''"
|
|
readonly
|
|
/>
|
|
```
|
|
|
|
**3. Use Safe Navigation and Fallbacks:**
|
|
```vue
|
|
<!-- ✅ Prevent errors and ensure consistent data types -->
|
|
<Input
|
|
:model-value="object?.property || ''"
|
|
:key="`field-${object?.uniqueId}`"
|
|
readonly
|
|
/>
|
|
```
|
|
|
|
**When to Apply These Patterns:**
|
|
- ✅ API responses stored in reactive refs
|
|
- ✅ Complex objects from services
|
|
- ✅ Input components showing external data
|
|
- ✅ Any scenario where template doesn't update after data changes
|
|
|
|
### **⚠️ CRITICAL: JavaScript Falsy Value Bug Prevention**
|
|
|
|
**ALWAYS use nullish coalescing (`??`) instead of logical OR (`||`) for numeric defaults:**
|
|
|
|
```typescript
|
|
// ❌ WRONG: Treats 0 as falsy, defaults to 1 even when quantity is validly 0
|
|
quantity: productData.quantity || 1
|
|
|
|
// ✅ CORRECT: Only defaults to 1 when quantity is null or undefined
|
|
quantity: productData.quantity ?? 1
|
|
```
|
|
|
|
**Why this matters:**
|
|
- JavaScript falsy values include: `false`, `0`, `""`, `null`, `undefined`, `NaN`
|
|
- Using `||` for defaults will incorrectly override valid `0` values
|
|
- This caused a critical bug where products with quantity `0` displayed as quantity `1`
|
|
- The `??` operator only triggers for `null` and `undefined`, preserving valid `0` values
|
|
|
|
**Common scenarios where this bug occurs:**
|
|
- Product quantities, prices, counters (any numeric value where 0 is valid)
|
|
- Boolean flags where `false` is a valid state
|
|
- Empty strings that should be preserved vs. undefined strings
|
|
|
|
**Rule of thumb:**
|
|
- Use `||` only when `0`, `false`, or `""` should trigger the default
|
|
- Use `??` when only `null`/`undefined` should trigger the default (most cases)
|
|
|
|
### **Module Development Best Practices**
|
|
|
|
**Module Structure Requirements:**
|
|
1. **Independence**: Modules must be independent of each other - no direct imports between modules
|
|
2. **Base Dependency**: All modules should depend on 'base' module for core infrastructure
|
|
3. **Service Pattern**: All services should extend `BaseService` for standardized initialization
|
|
4. **API Isolation**: Module-specific API calls must be in the module's services folder
|
|
5. **Dependency Injection**: Cross-module communication only through DI container
|
|
|
|
### **📋 MODULE DEVELOPMENT CHECKLIST**
|
|
|
|
Before considering any module complete, verify ALL items:
|
|
|
|
**Service Implementation:**
|
|
- [ ] Service extends `BaseService`
|
|
- [ ] Has `metadata` with `name`, `version`, `dependencies`
|
|
- [ ] Dependencies listed in metadata match actual usage
|
|
- [ ] No manual `injectService()` calls in `onInitialize()`
|
|
- [ ] Registers with VisibilityService if has real-time features
|
|
- [ ] Implements `onResume()` and `onPause()` if using VisibilityService
|
|
- [ ] Uses module config, not direct config imports
|
|
|
|
**Module Plugin:**
|
|
- [ ] Depends on `'base'` module
|
|
- [ ] Creates service instances
|
|
- [ ] Registers services in DI container BEFORE initialization
|
|
- [ ] Calls `service.initialize()` with `waitForDependencies: true`
|
|
- [ ] Registers components AFTER service initialization
|
|
|
|
**Configuration:**
|
|
- [ ] Module added to `app.config.ts`
|
|
- [ ] Has `enabled` flag
|
|
- [ ] Has `config` object with necessary settings
|
|
- [ ] Uses `appConfig.modules.[moduleName].config` in services
|
|
|
|
**Forms (if applicable):**
|
|
- [ ] Uses Shadcn/UI form components
|
|
- [ ] Uses vee-validate with Zod schema
|
|
- [ ] Has proper validation messages
|
|
- [ ] Disables submit button until form is valid
|
|
- [ ] Uses `form.handleSubmit()` for submission
|
|
|
|
**Testing Checklist:**
|
|
- [ ] Service initializes without errors
|
|
- [ ] Dependencies are properly injected
|
|
- [ ] VisibilityService callbacks work (test by switching tabs)
|
|
- [ ] Configuration is properly loaded
|
|
- [ ] Module can be disabled via config
|
|
|
|
**Nostr Integration Rules:**
|
|
1. **NEVER create separate relay connections** - always use the central RelayHub
|
|
2. **Access RelayHub through DI**: `const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)`
|
|
3. **Use RelayHub methods** for all Nostr operations (subscribe, publish, etc.)
|
|
4. **Event kinds** should be module-specific and follow NIP specifications
|
|
|
|
**LNbits API Integration:**
|
|
1. **Create module-specific API service** in `services/[module]API.ts`
|
|
2. **Extend BaseService** for automatic dependency management
|
|
3. **Use authentication headers**: `X-Api-Key: walletKey`
|
|
4. **Base URL from config**: Use `appConfig.modules.[module].config.apiConfig.baseUrl`
|
|
5. **Error handling**: Implement proper error handling with user feedback
|
|
|
|
**Composables Best Practices:**
|
|
```typescript
|
|
export function useMyModule() {
|
|
// Always use DI for services
|
|
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
|
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE)
|
|
const myAPI = injectService(SERVICE_TOKENS.MY_API)
|
|
|
|
// Never import services directly
|
|
// ❌ import { relayHub } from '@/modules/base/nostr/relay-hub'
|
|
// ✅ const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
|
}
|
|
```
|
|
|
|
**WebSocket & Visibility Management:**
|
|
- Services with WebSocket connections MUST integrate with VisibilityService
|
|
- Register visibility callbacks: `this.visibilityService.registerService(name, onResume, onPause)`
|
|
- Handle connection recovery in `onResume()` callback
|
|
- Implement battery-conscious pausing in `onPause()` callback
|
|
|
|
### **Code Conventions:**
|
|
- Use TypeScript interfaces over types for extendability
|
|
- Prefer functional and declarative patterns over classes (except for services)
|
|
- Use Vue Composition API with `<script setup>` syntax
|
|
- Follow naming convention: lowercase-with-dashes for directories
|
|
- Leverage VueUse functions for enhanced reactivity
|
|
- Implement lazy loading for non-critical components
|
|
- Optimize images using WebP format with lazy loading
|
|
- **ALWAYS use dependency injection for cross-module service access**
|
|
- **ALWAYS use Shadcn Form components for all form implementations**
|
|
- **ALWAYS extend BaseService for module services**
|
|
- **NEVER create direct dependencies between modules**
|
|
- **ALWAYS use semantic CSS classes, never hard-coded colors**
|
|
|
|
### **Build Configuration:**
|
|
- Vite config includes PWA, image optimization, and bundle analysis
|
|
- Manual chunking strategy for vendor libraries (vue-vendor, ui-vendor, shadcn)
|
|
- Electron Forge configured for cross-platform packaging
|
|
- TailwindCSS v4 integration via Vite plugin
|
|
|
|
### **Environment Variables:**
|
|
|
|
Required environment variables in `.env`:
|
|
|
|
```bash
|
|
# LNbits server URL for Lightning wallet functionality
|
|
VITE_LNBITS_BASE_URL=http://localhost:5000
|
|
|
|
# Nostr relay configuration (JSON array)
|
|
VITE_NOSTR_RELAYS='["wss://relay1.example.com","wss://relay2.example.com"]'
|
|
|
|
# Image upload server (pictrs)
|
|
VITE_PICTRS_BASE_URL=https://img.mydomain.com
|
|
|
|
# Admin public keys for feed moderation (JSON array)
|
|
VITE_ADMIN_PUBKEYS='["pubkey1","pubkey2"]'
|
|
|
|
# Optional: Disable WebSocket if needed
|
|
VITE_WEBSOCKET_ENABLED=true
|
|
```
|
|
|
|
## Mobile Browser File Input & Form Refresh Issues
|
|
|
|
### **Problem Overview**
|
|
|
|
Mobile browsers (especially Android) have well-documented issues with file inputs that can cause intermittent page refreshes during image upload operations. This is not a bug in our code, but rather a systemic issue with mobile browser memory management and activity lifecycle.
|
|
|
|
### **Root Causes**
|
|
|
|
1. **Memory-Induced Refreshes**: Android browsers may reload pages after file selection due to memory pressure when the camera or file chooser app is opened
|
|
2. **Activity Lifecycle Kills**: Mobile operating systems can kill browser activities in the background during file selection, causing page reloads when the browser activity is restored
|
|
3. **Visibility State Changes**: Screen lock/unlock and app switching can trigger visibility changes that affect authentication state evaluation, causing router guards to redirect
|
|
4. **Android 14/15 Camera Issues**: Chrome on Android 14/15 has broken camera capture functionality, often triggering gallery first before camera
|
|
|
|
### **Defensive Programming Solutions**
|
|
|
|
**Implementation Files:**
|
|
- `src/modules/base/components/ImageUpload.vue` - Multi-layer form submission prevention
|
|
- `src/modules/market/components/CreateProductDialog.vue` - Visibility-based navigation protection
|
|
|
|
**1. Multi-Layer Form Submission Prevention:**
|
|
```javascript
|
|
// DEFENSIVE: Multiple layers of form submission prevention during upload
|
|
const forms = document.querySelectorAll('form')
|
|
forms.forEach(form => {
|
|
// Layer 1: Override onsubmit handler directly
|
|
form.onsubmit = (e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
e.stopImmediatePropagation()
|
|
return false
|
|
}
|
|
|
|
// Layer 2: Add capturing event listener as backup
|
|
form.addEventListener('submit', preventSubmit, true)
|
|
})
|
|
```
|
|
|
|
**2. Window-Level Submit Blocking:**
|
|
```javascript
|
|
// DEFENSIVE: Add temporary form submit blocker at window level
|
|
const submitBlocker = (e: Event) => {
|
|
e.preventDefault()
|
|
e.stopImmediatePropagation()
|
|
return false
|
|
}
|
|
window.addEventListener('submit', submitBlocker, true)
|
|
|
|
// Remove after operation completes
|
|
setTimeout(() => {
|
|
window.removeEventListener('submit', submitBlocker, true)
|
|
}, 500)
|
|
```
|
|
|
|
**3. Visibility Change Protection:**
|
|
```javascript
|
|
// DEFENSIVE: Protect against screen wake/visibility triggered refreshes
|
|
const blockVisibilityRefresh = (_event: Event) => {
|
|
if (isDialogActive && props.isOpen) {
|
|
console.warn('Visibility change detected while form is open', {
|
|
visibilityState: document.visibilityState,
|
|
isOpen: props.isOpen,
|
|
hasData: !!form.values
|
|
})
|
|
}
|
|
}
|
|
|
|
document.addEventListener('visibilitychange', blockVisibilityRefresh)
|
|
```
|
|
|
|
**4. BeforeUnload User Confirmation:**
|
|
```javascript
|
|
// DEFENSIVE: Show user confirmation dialog for navigation attempts
|
|
const blockNavigation = (event: BeforeUnloadEvent) => {
|
|
if (isDialogActive && props.isOpen) {
|
|
event.preventDefault()
|
|
event.returnValue = 'You have unsaved changes. Are you sure you want to leave?'
|
|
return event.returnValue
|
|
}
|
|
}
|
|
window.addEventListener('beforeunload', blockNavigation)
|
|
```
|
|
|
|
**5. Camera Input Separation:**
|
|
```vue
|
|
<!-- CRITICAL: Separate camera input without 'multiple' attribute -->
|
|
<!-- Android browsers require separate inputs for proper camera/gallery handling -->
|
|
<input
|
|
ref="cameraInput"
|
|
type="file"
|
|
@change.stop.prevent="handleFileSelect"
|
|
accept="image/*"
|
|
capture="environment"
|
|
:disabled="disabled"
|
|
tabindex="-1"
|
|
hidden
|
|
/>
|
|
|
|
<!-- Gallery input with multiple support -->
|
|
<input
|
|
ref="galleryInput"
|
|
type="file"
|
|
@change.stop.prevent="handleFileSelect"
|
|
accept="image/*"
|
|
:multiple="multiple"
|
|
:disabled="disabled"
|
|
tabindex="-1"
|
|
hidden
|
|
/>
|
|
```
|
|
|
|
### **Vue.js Event Handling Best Practices**
|
|
|
|
**Use Vue Event Modifiers:**
|
|
```vue
|
|
<!-- ✅ CORRECT: Use Vue event modifiers -->
|
|
<input @change.stop.prevent="handleFileSelect" />
|
|
<button @click.stop="triggerCameraInput">Camera</button>
|
|
|
|
<!-- ❌ WRONG: Manual event handling in methods -->
|
|
<input @change="handleFileSelect" />
|
|
<!-- Then manually calling event.preventDefault() in method -->
|
|
```
|
|
|
|
**Proper Form Submission Handling:**
|
|
```vue
|
|
<!-- ✅ CORRECT: Use form.handleSubmit (automatically prevents default) -->
|
|
<form @submit="onSubmit">
|
|
|
|
<!-- ❌ WRONG: Manual preventDefault -->
|
|
<form @submit.prevent="handleSubmit">
|
|
```
|
|
|
|
### **Key Takeaways**
|
|
|
|
1. **This is a systemic mobile browser issue**, not a bug in our application code
|
|
2. **Multi-layer defensive programming** is the industry-standard solution
|
|
3. **Page Visibility API** is more reliable than beforeunload events on mobile
|
|
4. **User confirmation dialogs** provide the last line of defense against data loss
|
|
5. **Separate camera/gallery inputs** are required for proper Android browser support
|
|
6. **The defensive measures are working correctly** when users can choose to prevent navigation
|
|
|
|
**⚠️ IMPORTANT**: These issues are intermittent by nature. The defensive programming approach ensures that when they do occur, users have the opportunity to save their work instead of losing form data.
|