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
484
CLAUDE.md
484
CLAUDE.md
|
|
@ -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:
|
||||
|
||||
**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
|
||||
- **Nostr Feed Module** (`src/modules/nostr-feed/`) - Social feed functionality
|
||||
- **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.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)
|
||||
|
|
@ -122,6 +128,8 @@ const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
|||
- `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:**
|
||||
|
|
@ -143,8 +151,10 @@ The app integrates with LNbits for Lightning Network wallet functionality with r
|
|||
```typescript
|
||||
websocket: {
|
||||
enabled: true, // Enable/disable WebSocket functionality
|
||||
reconnectDelay: 1000, // Initial reconnection delay
|
||||
maxReconnectAttempts: 5 // Maximum reconnection attempts
|
||||
reconnectDelay: 2000, // Initial reconnection delay (ms)
|
||||
maxReconnectAttempts: 3, // Maximum reconnection attempts
|
||||
fallbackToPolling: true, // Enable polling fallback when WebSocket fails
|
||||
pollingInterval: 10000 // Polling interval (ms)
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -229,6 +239,88 @@ export const myModule: ModulePlugin = {
|
|||
- 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**
|
||||
|
|
@ -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
|
||||
- ❌ **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:**
|
||||
```vue
|
||||
<!-- Wrong: Manual form handling without vee-validate -->
|
||||
<form @submit.prevent="handleSubmit">
|
||||
**CRITICAL: Always use semantic, theme-aware CSS classes**
|
||||
|
||||
<!-- Wrong: Direct v-model bypasses form validation -->
|
||||
<Input v-model="myValue" />
|
||||
- ✅ **Use semantic classes** that automatically adapt to light/dark themes
|
||||
- ❌ **Never use hard-coded colors** like `bg-white`, `text-gray-500`, `border-blue-500`
|
||||
|
||||
<!-- Wrong: Manual validation instead of using meta.valid -->
|
||||
<Button :disabled="!name || !email">Submit</Button>
|
||||
**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:**
|
||||
```vue
|
||||
<!-- Correct: Uses form.handleSubmit for proper form handling -->
|
||||
<form @submit="onSubmit">
|
||||
|
||||
<!-- 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>
|
||||
```
|
||||
**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**
|
||||
|
||||
|
|
@ -463,23 +564,32 @@ createdObject.value = Object.assign({}, apiResponse)
|
|||
- ✅ Input components showing external data
|
||||
- ✅ 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
|
||||
// Service returns complex invoice object
|
||||
const invoice = await walletService.createInvoice(data)
|
||||
// ❌ WRONG: Treats 0 as falsy, defaults to 1 even when quantity is validly 0
|
||||
quantity: productData.quantity || 1
|
||||
|
||||
// Force reactivity for template updates
|
||||
createdInvoice.value = Object.assign({}, invoice)
|
||||
// ✅ CORRECT: Only defaults to 1 when quantity is null or undefined
|
||||
quantity: productData.quantity ?? 1
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- Template with forced reactivity -->
|
||||
<Input
|
||||
:key="`bolt11-${createdInvoice?.payment_hash}`"
|
||||
:model-value="createdInvoice?.payment_request || ''"
|
||||
readonly
|
||||
/>
|
||||
```
|
||||
**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**
|
||||
|
||||
|
|
@ -530,166 +640,6 @@ Before considering any module complete, verify ALL items:
|
|||
- [ ] Configuration is properly loaded
|
||||
- [ ] 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:**
|
||||
1. **NEVER create separate relay connections** - always use the central RelayHub
|
||||
2. **Access RelayHub through DI**: `const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)`
|
||||
|
|
@ -735,44 +685,34 @@ export function useMyModule() {
|
|||
- **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**
|
||||
|
||||
### **⚠️ 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)
|
||||
|
||||
**Build Configuration:**
|
||||
### **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:**
|
||||
- Nostr relay configuration via `VITE_NOSTR_RELAYS` environment variable
|
||||
- PWA manifest configured for standalone app experience
|
||||
- Service worker with automatic updates every hour
|
||||
### **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
|
||||
|
||||
|
|
@ -906,86 +846,6 @@ window.addEventListener('beforeunload', blockNavigation)
|
|||
<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**
|
||||
|
||||
1. **This is a systemic mobile browser issue**, not a bug in our application code
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue