Updates project documentation
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.
This commit is contained in:
parent
1a38c92db1
commit
b286a0315d
1 changed files with 180 additions and 320 deletions
500
CLAUDE.md
500
CLAUDE.md
|
|
@ -10,7 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
- `npm run preview` - Preview production build locally
|
- `npm run preview` - Preview production build locally
|
||||||
- `npm run analyze` - Build with bundle analysis (opens visualization)
|
- `npm run analyze` - Build with bundle analysis (opens visualization)
|
||||||
|
|
||||||
**Electron Development**
|
**Electron Development**
|
||||||
- `npm run electron:dev` - Run both Vite dev server and Electron concurrently
|
- `npm run electron:dev` - Run both Vite dev server and Electron concurrently
|
||||||
- `npm run electron:build` - Full build and package for Electron
|
- `npm run electron:build` - Full build and package for Electron
|
||||||
- `npm run start` - Start Electron using Forge
|
- `npm run start` - Start Electron using Forge
|
||||||
|
|
@ -26,7 +26,7 @@ This is a modular Vue 3 + TypeScript + Vite application with Electron support, f
|
||||||
The application uses a plugin-based modular architecture with dependency injection for service management:
|
The application uses a plugin-based modular architecture with dependency injection for service management:
|
||||||
|
|
||||||
**Core Modules:**
|
**Core Modules:**
|
||||||
- **Base Module** (`src/modules/base/`) - Core infrastructure (Nostr, Auth, PWA)
|
- **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
|
- **Wallet Module** (`src/modules/wallet/`) - Lightning wallet management with real-time balance updates
|
||||||
- **Nostr Feed Module** (`src/modules/nostr-feed/`) - Social feed functionality
|
- **Nostr Feed Module** (`src/modules/nostr-feed/`) - Social feed functionality
|
||||||
- **Chat Module** (`src/modules/chat/`) - Encrypted Nostr chat
|
- **Chat Module** (`src/modules/chat/`) - Encrypted Nostr chat
|
||||||
|
|
@ -90,6 +90,12 @@ const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
||||||
- `SERVICE_TOKENS.VISIBILITY_SERVICE` - App visibility and connection management
|
- `SERVICE_TOKENS.VISIBILITY_SERVICE` - App visibility and connection management
|
||||||
- `SERVICE_TOKENS.WALLET_SERVICE` - Wallet operations (send, receive, transactions)
|
- `SERVICE_TOKENS.WALLET_SERVICE` - Wallet operations (send, receive, transactions)
|
||||||
- `SERVICE_TOKENS.WALLET_WEBSOCKET_SERVICE` - Real-time wallet balance updates via WebSocket
|
- `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:**
|
**Core Stack:**
|
||||||
- Vue 3 with Composition API (`<script setup>` style)
|
- Vue 3 with Composition API (`<script setup>` style)
|
||||||
|
|
@ -122,6 +128,8 @@ const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
||||||
- `api/` - API integrations
|
- `api/` - API integrations
|
||||||
- `types/` - TypeScript type definitions
|
- `types/` - TypeScript type definitions
|
||||||
- `src/pages/` - Route pages
|
- `src/pages/` - Route pages
|
||||||
|
- `src/modules/` - Modular feature implementations
|
||||||
|
- `src/core/` - Core infrastructure (DI, BaseService, plugin manager)
|
||||||
- `electron/` - Electron main process code
|
- `electron/` - Electron main process code
|
||||||
|
|
||||||
**Lightning Wallet Integration:**
|
**Lightning Wallet Integration:**
|
||||||
|
|
@ -143,8 +151,10 @@ The app integrates with LNbits for Lightning Network wallet functionality with r
|
||||||
```typescript
|
```typescript
|
||||||
websocket: {
|
websocket: {
|
||||||
enabled: true, // Enable/disable WebSocket functionality
|
enabled: true, // Enable/disable WebSocket functionality
|
||||||
reconnectDelay: 1000, // Initial reconnection delay
|
reconnectDelay: 2000, // Initial reconnection delay (ms)
|
||||||
maxReconnectAttempts: 5 // Maximum reconnection attempts
|
maxReconnectAttempts: 3, // Maximum reconnection attempts
|
||||||
|
fallbackToPolling: true, // Enable polling fallback when WebSocket fails
|
||||||
|
pollingInterval: 10000 // Polling interval (ms)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -184,12 +194,12 @@ export const myModule: ModulePlugin = {
|
||||||
name: 'my-module',
|
name: 'my-module',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
dependencies: ['base'], // Always depend on base for core services
|
dependencies: ['base'], // Always depend on base for core services
|
||||||
|
|
||||||
async install(app: App, options?: { config?: MyModuleConfig }) {
|
async install(app: App, options?: { config?: MyModuleConfig }) {
|
||||||
// Module installation logic
|
// Module installation logic
|
||||||
// Register components, initialize services, etc.
|
// Register components, initialize services, etc.
|
||||||
},
|
},
|
||||||
|
|
||||||
routes: [/* module routes */],
|
routes: [/* module routes */],
|
||||||
components: {/* exported components */},
|
components: {/* exported components */},
|
||||||
composables: {/* exported composables */}
|
composables: {/* exported composables */}
|
||||||
|
|
@ -229,6 +239,88 @@ export const myModule: ModulePlugin = {
|
||||||
- Module configs in `src/app.config.ts`
|
- Module configs in `src/app.config.ts`
|
||||||
- Centralized config parsing and validation
|
- 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**
|
### **Form Implementation Standards**
|
||||||
|
|
||||||
**CRITICAL: Always use Shadcn/UI Form Components with vee-validate**
|
**CRITICAL: Always use Shadcn/UI Form Components with vee-validate**
|
||||||
|
|
@ -289,7 +381,7 @@ const onSubmit = form.handleSubmit(async (values) => {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name *</FormLabel>
|
<FormLabel>Name *</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter name"
|
placeholder="Enter name"
|
||||||
v-bind="componentField"
|
v-bind="componentField"
|
||||||
/>
|
/>
|
||||||
|
|
@ -330,7 +422,7 @@ const onSubmit = form.handleSubmit(async (values) => {
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<!-- Submit Button -->
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="isLoading || !isFormValid"
|
:disabled="isLoading || !isFormValid"
|
||||||
>
|
>
|
||||||
|
|
@ -382,35 +474,44 @@ For Shadcn/ui Checkbox components, you MUST use the correct Vue.js binding patte
|
||||||
- ✅ **Force Re-render**: Use dynamic `:key` if checkbox doesn't reflect initial form values
|
- ✅ **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
|
- ❌ **Don't Mix**: Never mix checked/model-value patterns - they have different behaviors
|
||||||
|
|
||||||
**Reference**: [Vue.js Forms Documentation](https://vuejs.org/guide/essentials/forms.html)
|
### **CSS and Styling Guidelines**
|
||||||
|
|
||||||
**❌ NEVER do this:**
|
**CRITICAL: Always use semantic, theme-aware CSS classes**
|
||||||
```vue
|
|
||||||
<!-- Wrong: Manual form handling without vee-validate -->
|
|
||||||
<form @submit.prevent="handleSubmit">
|
|
||||||
|
|
||||||
<!-- Wrong: Direct v-model bypasses form validation -->
|
|
||||||
<Input v-model="myValue" />
|
|
||||||
|
|
||||||
<!-- Wrong: Manual validation instead of using meta.valid -->
|
- ✅ **Use semantic classes** that automatically adapt to light/dark themes
|
||||||
<Button :disabled="!name || !email">Submit</Button>
|
- ❌ **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 */
|
||||||
```
|
```
|
||||||
|
|
||||||
**✅ ALWAYS do this:**
|
**Why Semantic Classes:**
|
||||||
```vue
|
- Ensures components work in both light and dark themes
|
||||||
<!-- Correct: Uses form.handleSubmit for proper form handling -->
|
- Maintains consistency with Shadcn/ui component library
|
||||||
<form @submit="onSubmit">
|
- Easier to maintain and update theme colors globally
|
||||||
|
- Better accessibility
|
||||||
<!-- Correct: Uses FormField with componentField binding -->
|
|
||||||
<FormField v-slot="{ componentField }" name="fieldName">
|
|
||||||
<FormControl>
|
|
||||||
<Input v-bind="componentField" />
|
|
||||||
</FormControl>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<!-- Correct: Uses form meta for validation state -->
|
|
||||||
<Button :disabled="!isFormValid">Submit</Button>
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Vue Reactivity Best Practices**
|
### **Vue Reactivity Best Practices**
|
||||||
|
|
||||||
|
|
@ -463,23 +564,32 @@ createdObject.value = Object.assign({}, apiResponse)
|
||||||
- ✅ Input components showing external data
|
- ✅ Input components showing external data
|
||||||
- ✅ Any scenario where template doesn't update after data changes
|
- ✅ Any scenario where template doesn't update after data changes
|
||||||
|
|
||||||
**Example from Wallet Module:**
|
### **⚠️ CRITICAL: JavaScript Falsy Value Bug Prevention**
|
||||||
|
|
||||||
|
**ALWAYS use nullish coalescing (`??`) instead of logical OR (`||`) for numeric defaults:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Service returns complex invoice object
|
// ❌ WRONG: Treats 0 as falsy, defaults to 1 even when quantity is validly 0
|
||||||
const invoice = await walletService.createInvoice(data)
|
quantity: productData.quantity || 1
|
||||||
|
|
||||||
// Force reactivity for template updates
|
// ✅ CORRECT: Only defaults to 1 when quantity is null or undefined
|
||||||
createdInvoice.value = Object.assign({}, invoice)
|
quantity: productData.quantity ?? 1
|
||||||
```
|
```
|
||||||
|
|
||||||
```vue
|
**Why this matters:**
|
||||||
<!-- Template with forced reactivity -->
|
- JavaScript falsy values include: `false`, `0`, `""`, `null`, `undefined`, `NaN`
|
||||||
<Input
|
- Using `||` for defaults will incorrectly override valid `0` values
|
||||||
:key="`bolt11-${createdInvoice?.payment_hash}`"
|
- This caused a critical bug where products with quantity `0` displayed as quantity `1`
|
||||||
:model-value="createdInvoice?.payment_request || ''"
|
- The `??` operator only triggers for `null` and `undefined`, preserving valid `0` values
|
||||||
readonly
|
|
||||||
/>
|
**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 Development Best Practices**
|
||||||
|
|
||||||
|
|
@ -530,166 +640,6 @@ Before considering any module complete, verify ALL items:
|
||||||
- [ ] Configuration is properly loaded
|
- [ ] Configuration is properly loaded
|
||||||
- [ ] Module can be disabled via config
|
- [ ] Module can be disabled via config
|
||||||
|
|
||||||
**Required Module Structure:**
|
|
||||||
```
|
|
||||||
src/modules/[module-name]/
|
|
||||||
├── index.ts # Module plugin definition (REQUIRED)
|
|
||||||
├── components/ # Module-specific components
|
|
||||||
├── composables/ # Module composables (use DI for services)
|
|
||||||
├── services/ # Module services (extend BaseService)
|
|
||||||
│ ├── [module]Service.ts # Core module service
|
|
||||||
│ └── [module]API.ts # LNbits API integration
|
|
||||||
├── stores/ # Module-specific Pinia stores
|
|
||||||
├── types/ # Module type definitions
|
|
||||||
└── views/ # Module pages/views
|
|
||||||
```
|
|
||||||
|
|
||||||
**Service Implementation Pattern:**
|
|
||||||
|
|
||||||
**⚠️ CRITICAL SERVICE REQUIREMENTS - MUST FOLLOW EXACTLY:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ CORRECT: Proper BaseService implementation
|
|
||||||
export class MyModuleService extends BaseService {
|
|
||||||
// 1. REQUIRED: Declare metadata with dependencies
|
|
||||||
protected readonly metadata = {
|
|
||||||
name: 'MyModuleService',
|
|
||||||
version: '1.0.0',
|
|
||||||
dependencies: ['PaymentService', 'AuthService'] // List ALL service dependencies by name
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. REQUIRED: DO NOT manually inject services in onInitialize
|
|
||||||
protected async onInitialize(): Promise<void> {
|
|
||||||
// ❌ WRONG: Manual injection
|
|
||||||
// this.paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE)
|
|
||||||
|
|
||||||
// ✅ CORRECT: BaseService auto-injects based on metadata.dependencies
|
|
||||||
// this.paymentService is already available here!
|
|
||||||
|
|
||||||
// 3. REQUIRED: Register with VisibilityService if you have ANY real-time features
|
|
||||||
if (this.hasRealTimeFeatures()) {
|
|
||||||
this.visibilityService.registerService(
|
|
||||||
this.metadata.name,
|
|
||||||
this.onResume.bind(this),
|
|
||||||
this.onPause.bind(this)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Initialize your module-specific logic
|
|
||||||
await this.loadInitialData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. REQUIRED: Implement visibility handlers for connection management
|
|
||||||
private async onResume(): Promise<void> {
|
|
||||||
// Restore connections, refresh data when app becomes visible
|
|
||||||
await this.checkConnectionHealth()
|
|
||||||
await this.refreshData()
|
|
||||||
}
|
|
||||||
|
|
||||||
private async onPause(): Promise<void> {
|
|
||||||
// Pause expensive operations for battery efficiency
|
|
||||||
this.pausePolling()
|
|
||||||
}
|
|
||||||
|
|
||||||
private hasRealTimeFeatures(): boolean {
|
|
||||||
// Return true if your service uses WebSockets, polling, or real-time updates
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// API services for LNbits integration
|
|
||||||
export class MyModuleAPI extends BaseService {
|
|
||||||
private baseUrl: string
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
// ❌ WRONG: Direct config import
|
|
||||||
// import { config } from '@/lib/config'
|
|
||||||
|
|
||||||
// ✅ CORRECT: Use module configuration
|
|
||||||
const moduleConfig = appConfig.modules.myModule.config
|
|
||||||
this.baseUrl = moduleConfig.apiConfig.baseUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
// API methods here
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**❌ COMMON MISTAKES TO AVOID:**
|
|
||||||
1. **Manual service injection** in onInitialize - BaseService handles this
|
|
||||||
2. **Direct config imports** - Always use module configuration
|
|
||||||
3. **Missing metadata.dependencies** - Breaks automatic dependency injection
|
|
||||||
4. **No VisibilityService integration** - Causes connection issues on mobile
|
|
||||||
5. **Not using proper initialization options** - Miss dependency waiting
|
|
||||||
|
|
||||||
**Module Plugin Pattern:**
|
|
||||||
|
|
||||||
**⚠️ CRITICAL MODULE INSTALLATION REQUIREMENTS:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export const myModule: ModulePlugin = {
|
|
||||||
name: 'my-module',
|
|
||||||
version: '1.0.0',
|
|
||||||
dependencies: ['base'], // ALWAYS depend on 'base' for core infrastructure
|
|
||||||
|
|
||||||
async install(app: App, options?: { config?: MyModuleConfig }) {
|
|
||||||
// 1. REQUIRED: Create service instances
|
|
||||||
const myService = new MyModuleService()
|
|
||||||
const myAPI = new MyModuleAPI()
|
|
||||||
|
|
||||||
// 2. REQUIRED: Register in DI container BEFORE initialization
|
|
||||||
container.provide(SERVICE_TOKENS.MY_SERVICE, myService)
|
|
||||||
container.provide(SERVICE_TOKENS.MY_API, myAPI)
|
|
||||||
|
|
||||||
// 3. CRITICAL: Initialize services with proper options
|
|
||||||
await myService.initialize({
|
|
||||||
waitForDependencies: true, // REQUIRED: Wait for dependencies
|
|
||||||
maxRetries: 3, // RECOMMENDED: Retry on failure
|
|
||||||
timeout: 5000 // OPTIONAL: Timeout for initialization
|
|
||||||
})
|
|
||||||
|
|
||||||
// Initialize API service if it needs initialization
|
|
||||||
if (myAPI.initialize) {
|
|
||||||
await myAPI.initialize({
|
|
||||||
waitForDependencies: true,
|
|
||||||
maxRetries: 3
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Register components AFTER services are initialized
|
|
||||||
app.component('MyComponent', MyComponent)
|
|
||||||
|
|
||||||
// 5. OPTIONAL: Export for testing/debugging
|
|
||||||
return {
|
|
||||||
service: myService,
|
|
||||||
api: myAPI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**MODULE CONFIGURATION IN app.config.ts:**
|
|
||||||
```typescript
|
|
||||||
// REQUIRED: Add module configuration
|
|
||||||
export default {
|
|
||||||
modules: {
|
|
||||||
'my-module': {
|
|
||||||
enabled: true,
|
|
||||||
config: {
|
|
||||||
apiConfig: {
|
|
||||||
baseUrl: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000'
|
|
||||||
},
|
|
||||||
// Module-specific configuration
|
|
||||||
features: {
|
|
||||||
realTimeUpdates: true,
|
|
||||||
offlineSupport: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nostr Integration Rules:**
|
**Nostr Integration Rules:**
|
||||||
1. **NEVER create separate relay connections** - always use the central RelayHub
|
1. **NEVER create separate relay connections** - always use the central RelayHub
|
||||||
2. **Access RelayHub through DI**: `const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)`
|
2. **Access RelayHub through DI**: `const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)`
|
||||||
|
|
@ -710,7 +660,7 @@ export function useMyModule() {
|
||||||
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
||||||
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE)
|
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE)
|
||||||
const myAPI = injectService(SERVICE_TOKENS.MY_API)
|
const myAPI = injectService(SERVICE_TOKENS.MY_API)
|
||||||
|
|
||||||
// Never import services directly
|
// Never import services directly
|
||||||
// ❌ import { relayHub } from '@/modules/base/nostr/relay-hub'
|
// ❌ import { relayHub } from '@/modules/base/nostr/relay-hub'
|
||||||
// ✅ const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
// ✅ const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
||||||
|
|
@ -735,44 +685,34 @@ export function useMyModule() {
|
||||||
- **ALWAYS use Shadcn Form components for all form implementations**
|
- **ALWAYS use Shadcn Form components for all form implementations**
|
||||||
- **ALWAYS extend BaseService for module services**
|
- **ALWAYS extend BaseService for module services**
|
||||||
- **NEVER create direct dependencies between modules**
|
- **NEVER create direct dependencies between modules**
|
||||||
|
- **ALWAYS use semantic CSS classes, never hard-coded colors**
|
||||||
|
|
||||||
### **⚠️ CRITICAL: JavaScript Falsy Value Bug Prevention**
|
### **Build Configuration:**
|
||||||
|
|
||||||
**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)
|
|
||||||
|
|
||||||
**Build Configuration:**
|
|
||||||
- Vite config includes PWA, image optimization, and bundle analysis
|
- Vite config includes PWA, image optimization, and bundle analysis
|
||||||
- Manual chunking strategy for vendor libraries (vue-vendor, ui-vendor, shadcn)
|
- Manual chunking strategy for vendor libraries (vue-vendor, ui-vendor, shadcn)
|
||||||
- Electron Forge configured for cross-platform packaging
|
- Electron Forge configured for cross-platform packaging
|
||||||
- TailwindCSS v4 integration via Vite plugin
|
- TailwindCSS v4 integration via Vite plugin
|
||||||
|
|
||||||
**Environment:**
|
### **Environment Variables:**
|
||||||
- Nostr relay configuration via `VITE_NOSTR_RELAYS` environment variable
|
|
||||||
- PWA manifest configured for standalone app experience
|
Required environment variables in `.env`:
|
||||||
- Service worker with automatic updates every hour
|
|
||||||
|
```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
|
## Mobile Browser File Input & Form Refresh Issues
|
||||||
|
|
||||||
|
|
@ -906,86 +846,6 @@ window.addEventListener('beforeunload', blockNavigation)
|
||||||
<form @submit.prevent="handleSubmit">
|
<form @submit.prevent="handleSubmit">
|
||||||
```
|
```
|
||||||
|
|
||||||
### **Android 14/15 Camera Workarounds**
|
|
||||||
|
|
||||||
**Non-Standard MIME Type Workaround:**
|
|
||||||
```html
|
|
||||||
<!-- Add non-standard MIME type to force camera access -->
|
|
||||||
<input type="file" accept="image/*,android/allowCamera" capture="environment" />
|
|
||||||
```
|
|
||||||
|
|
||||||
**Plain File Input Fallback:**
|
|
||||||
```html
|
|
||||||
<!-- Fallback: Plain file input shows both camera and gallery options -->
|
|
||||||
<input type="file" accept="image/*" />
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Industry-Standard Patterns**
|
|
||||||
|
|
||||||
**1. Page Visibility API (Primary Solution):**
|
|
||||||
```javascript
|
|
||||||
// Modern browsers: Use Page Visibility API instead of beforeunload
|
|
||||||
document.addEventListener('visibilitychange', function() {
|
|
||||||
if (document.visibilityState === 'visible') {
|
|
||||||
// Resume critical operations, restore connections
|
|
||||||
resumeOperations()
|
|
||||||
} else {
|
|
||||||
// Save state, pause operations for battery conservation
|
|
||||||
saveStateAndPause()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Conditional BeforeUnload Protection:**
|
|
||||||
```javascript
|
|
||||||
// Only add beforeunload listeners when user has unsaved changes
|
|
||||||
const addFormProtection = (hasUnsavedChanges) => {
|
|
||||||
if (hasUnsavedChanges) {
|
|
||||||
window.addEventListener('beforeunload', preventUnload)
|
|
||||||
} else {
|
|
||||||
window.removeEventListener('beforeunload', preventUnload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Session Recovery Pattern:**
|
|
||||||
```javascript
|
|
||||||
// Save form state on visibility change
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.visibilityState === 'hidden') {
|
|
||||||
localStorage.setItem('formDraft', JSON.stringify(formData))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Restore on page load
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
const draft = localStorage.getItem('formDraft')
|
|
||||||
if (draft) restoreFormData(JSON.parse(draft))
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Testing & Debugging**
|
|
||||||
|
|
||||||
**Reproduction Steps:**
|
|
||||||
1. Open form with file upload on mobile device
|
|
||||||
2. Select camera input during image upload operations
|
|
||||||
3. Turn screen off/on during upload process
|
|
||||||
4. Switch between apps during file selection
|
|
||||||
5. Low memory conditions during camera usage
|
|
||||||
|
|
||||||
**Success Indicators:**
|
|
||||||
- User sees confirmation dialog instead of losing form data
|
|
||||||
- Console warnings show visibility change detection working
|
|
||||||
- Form state preservation during app switching
|
|
||||||
- Camera input properly separates from gallery input
|
|
||||||
|
|
||||||
**Debug Console Messages:**
|
|
||||||
```javascript
|
|
||||||
// Look for these defensive programming console messages
|
|
||||||
console.warn('Form submission blocked during file upload')
|
|
||||||
console.warn('Visibility change detected while form is open')
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Key Takeaways**
|
### **Key Takeaways**
|
||||||
|
|
||||||
1. **This is a systemic mobile browser issue**, not a bug in our application code
|
1. **This is a systemic mobile browser issue**, not a bug in our application code
|
||||||
|
|
@ -995,4 +855,4 @@ console.warn('Visibility change detected while form is open')
|
||||||
5. **Separate camera/gallery inputs** are required for proper Android browser support
|
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
|
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.
|
**⚠️ 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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue